diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100644 index 00000000..72985e07 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,33 @@ +#!/bin/bash + +# 커밋 메시지 파일 +commit_msg_file=$1 + +# 커밋 메시지 읽기 +commit_msg=$(cat "$commit_msg_file") + +# 정규식 패턴 (feat|fix|docs|refactor|test|chore 등의 prefix 필수) +pattern="^(feat|fix|docs|refactor|test|chore): .+" + +# 메시지 검증 (형식이 맞지 않으면 커밋 차단) +if ! [[ $commit_msg =~ $pattern ]]; then + echo "❌ [ERROR] 커밋 메시지가 잘못되었습니다!" + echo "✅ 올바른 형식: feat: 기능 추가, fix: 버그 수정, docs: 문서 추가 등" + exit 1 +fi + +# 커밋 메시지 패턴에 따라 깃이모지 추가 +case "$commit_msg" in + feat:*) new_msg="✨ $commit_msg" ;; + fix:*) new_msg="🐛 $commit_msg" ;; + docs:*) new_msg="📚 $commit_msg" ;; + refactor:*) new_msg="♻️ $commit_msg" ;; + test:*) new_msg="🧪 $commit_msg" ;; + chore:*) new_msg="🛠️ $commit_msg" ;; + *) new_msg="$commit_msg" ;; # 기본적으로 변경하지 않음 +esac + +# 수정된 메시지를 커밋 메시지 파일에 덮어쓰기 +echo "$new_msg" > "$commit_msg_file" + +exit 0 \ No newline at end of file diff --git a/.github/workflows/dev-cd.yml b/.github/workflows/dev-cd.yml index 1a5fa7e6..2ffbd4af 100644 --- a/.github/workflows/dev-cd.yml +++ b/.github/workflows/dev-cd.yml @@ -45,10 +45,35 @@ jobs: echo "${{ secrets.APPLE_AUTH }}" > ./src/main/resources/AUTHKEY_JUINJAG.p8 shell: bash + # APPLE IN_APP 결제 관련 프로세스 시작 + - name: Create certs and keys directories + run: | + mkdir -p ./src/main/resources/keys + + - name: Create IAP .p8 Key + run: | + echo "${{ secrets.APPLE_IAP_KEY }}" > ./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8 + + # 키 파일 크기 확인 (내용은 로그에 출력하지 않음) + if [ -s "./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8" ]; then + echo "✅ IAP key file created: $(wc -c < ./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8) bytes" + else + echo "❌ IAP key file is empty!" + exit 1 + fi + + # PEM 형식 확인 + if grep -q "BEGIN PRIVATE KEY" ./src/main/resources/keys/SubscriptionKey_Q5646J7W54.p8; then + echo "✅ IAP key file appears to be in PEM format" + else + echo "⚠️ IAP key file may not be in PEM format" + fi + shell: bash + # APPLE IN_APP 결제 관련 프로세스 끝 + - name: Build With Gradle run: ./gradlew build -x test - - name: Login to Docker Hub run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 7eef9171..f39b29e0 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -32,5 +32,11 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Create application-dev.yml + run: | + mkdir -p ./src/main/resources + echo "${{ secrets.PROPERTIES_DEV }}" > ./src/main/resources/application-dev.yml + shell: bash + - name: Build With Gradle run: ./gradlew build -x test diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml index 87a65aab..022c18c3 100644 --- a/.github/workflows/prod-cd.yml +++ b/.github/workflows/prod-cd.yml @@ -45,6 +45,17 @@ jobs: echo "${{ secrets.APPLE_AUTH }}" > ./src/main/resources/AUTHKEY_JUINJAG.p8 shell: bash + # APPLE IN_APP 결제 관련 프로세스 시작 + - name: Create certs and keys directories + run: | + mkdir -p ./src/main/resources/keys + + - name: Create IAP .p8 Key + run: | + echo "${{ secrets.APPLE_IAP_KEY }}" > ./src/main/resources/keys/${{ secrets.APPLE_IAP_KEY_NAME }} + shell: bash + # APPLE IN_APP 결제 관련 프로세스 끝 + - name: Build With Gradle run: ./gradlew build -x test diff --git a/.gitignore b/.gitignore index e610285f..d103fa1a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,12 @@ out/ /application.yml /src/main/resources/application-local.yml +/src/main/resources/application.yml /src/main/resources/application-dev.yml /src/main/resources/application-prod.yml -/src/main/generated/* \ No newline at end of file +/src/main/resources/*.json +/src/main/generated/* +/src/test/java/resources/application-*.yml + +src/main/resources/keys +#src/main/resources/certs diff --git a/Dockerfile-dev b/Dockerfile-dev index fd4f5a42..8b8dec83 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -1,8 +1,11 @@ FROM openjdk:17-jdk ARG JAR_FILE=./build/libs/juinjang-0.0.1-SNAPSHOT.jar +ENV GOOGLE_APPLICATION_CREDENTIALS=/app/config/service-account-key.json COPY ${JAR_FILE} app.jar -ENTRYPOINT [ "java", "-jar", "-Dspring.profiles.active=dev", "/app.jar" ] + +ENTRYPOINT ["java", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/var/heapdumps/juinjang", "-Dspring.profiles.active=dev", "-jar", "/app.jar"] + diff --git a/build.gradle b/build.gradle index d00ed615..0f10cfc0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,94 +1,114 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.2.1' - id 'io.spring.dependency-management' version '1.1.4' + id 'java' + id 'org.springframework.boot' version '3.2.1' + id 'io.spring.dependency-management' version '1.1.4' } group = 'umc.5th' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } ext { - springCloudVersion = "2023.0.0" + springCloudVersion = "2023.0.0" } dependencyManagement { - imports { - mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" - } + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } jar { - enabled = false + enabled = false } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' - implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-actuator' - // - implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' // 4.1.0 + implementation 'org.springframework.retry:spring-retry' + implementation 'org.springframework.boot:spring-boot-starter-aop' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + // + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' // 4.1.0 // implementation 'org.springframework.cloud:spring-cloud-commons:4.1.1' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + + runtimeOnly 'com.mysql:mysql-connector-j' // runtimeOnly 'mysql:mysql-connector-java' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + // h2 + runtimeOnly 'com.h2database:h2' + + //security + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' - // h2 - runtimeOnly 'com.h2database:h2' + //jwt + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - //security - implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - implementation 'org.springframework.boot:spring-boot-starter-security' - testImplementation 'org.springframework.security:spring-security-test' + //S3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' - //jwt - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + // querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" - //S3 - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'commons-codec:commons-codec:1.16.0' // Base64 인코딩을 위한 의존성 - // querydsl - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' - annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" - annotationProcessor "jakarta.annotation:jakarta.annotation-api" - annotationProcessor "jakarta.persistence:jakarta.persistence-api" + // prometheus + implementation 'io.micrometer:micrometer-registry-prometheus' - implementation 'commons-codec:commons-codec:1.16.0' // Base64 인코딩을 위한 의존성 + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' - // prometheus - implementation 'io.micrometer:micrometer-registry-prometheus' + //Safe Search + implementation 'com.google.cloud:google-cloud-vision:3.58.0' + + // Apple + implementation 'com.apple.itunes.storekit:app-store-server-library:3.4.0' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + showStandardStreams = false + } } + def generated = 'src/main/generated' tasks.withType(JavaCompile).configureEach { - options.getGeneratedSourceOutputDirectory().set(file(generated)) + options.getGeneratedSourceOutputDirectory().set(file(generated)) } clean { - delete file(generated) -} \ No newline at end of file + delete file(generated) +} diff --git a/src/main/java/umc/th/juinjang/JuinjangApplication.java b/src/main/java/umc/th/juinjang/JuinjangApplication.java index e174c7c2..848e5fcd 100644 --- a/src/main/java/umc/th/juinjang/JuinjangApplication.java +++ b/src/main/java/umc/th/juinjang/JuinjangApplication.java @@ -3,16 +3,16 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.FeignAutoConfiguration; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication -@EnableJpaAuditing -@EnableAsync +// @EnableAsync @ImportAutoConfiguration({FeignAutoConfiguration.class}) +@EnableScheduling +@EnableRetry public class JuinjangApplication { public static void main(String[] args) { diff --git a/src/main/java/umc/th/juinjang/api/address/service/AddressUpdater.java b/src/main/java/umc/th/juinjang/api/address/service/AddressUpdater.java new file mode 100644 index 00000000..28570500 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/address/service/AddressUpdater.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.address.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.repository.AddressRepository; + +@Component +@RequiredArgsConstructor +public class AddressUpdater { + + private final AddressRepository addressRepository; + + public void save(Address address) { + addressRepository.save(address); + } +} diff --git a/src/main/java/umc/th/juinjang/api/appVersion/controller/AppVersionController.java b/src/main/java/umc/th/juinjang/api/appVersion/controller/AppVersionController.java new file mode 100644 index 00000000..2bfb8626 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/appVersion/controller/AppVersionController.java @@ -0,0 +1,24 @@ +package umc.th.juinjang.api.appVersion.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.appVersion.controller.response.AppVersionResponse; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.config.AppVersionProperties; + +@RestController +@RequestMapping("/api/app/version") +@RequiredArgsConstructor +public class AppVersionController { + + private final AppVersionProperties appVersionProperties; + + @GetMapping("/ios") + public ApiResponse getIOSVersion() { + return ApiResponse.onSuccess(AppVersionResponse.of(appVersionProperties.getIos())); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/appVersion/controller/response/AppVersionResponse.java b/src/main/java/umc/th/juinjang/api/appVersion/controller/response/AppVersionResponse.java new file mode 100644 index 00000000..424152a9 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/appVersion/controller/response/AppVersionResponse.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.api.appVersion.controller.response; + +public record AppVersionResponse( + String version +) { + public static AppVersionResponse of(String version) { + return new AppVersionResponse(version); + } +} diff --git a/src/main/java/umc/th/juinjang/api/apple/controller/AppleController.java b/src/main/java/umc/th/juinjang/api/apple/controller/AppleController.java new file mode 100644 index 00000000..054c9b62 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/apple/controller/AppleController.java @@ -0,0 +1,53 @@ +package umc.th.juinjang.api.apple.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.apple.itunes.storekit.model.Data; +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; +import com.apple.itunes.storekit.model.NotificationTypeV2; +import com.apple.itunes.storekit.model.ResponseBodyV2; +import com.apple.itunes.storekit.model.ResponseBodyV2DecodedPayload; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.apple.service.AppleService; +import umc.th.juinjang.api.pencil.service.PencilCommandService; +import umc.th.juinjang.api.pencil.service.PencilQueryService; + +@RestController +@RequestMapping("/api/apple") +@RequiredArgsConstructor +@Slf4j +public class AppleController { + + private final AppleService appleService; + private final PencilQueryService pencilQueryService; + private final PencilCommandService pencilCommandService; + + @Operation(summary = "애플 서버 알림 API") + @PostMapping("notifications/v2") + public ResponseEntity handleNotificationV2(@RequestBody ResponseBodyV2 requestBody) { + ResponseBodyV2DecodedPayload payload = appleService.getNotificationPayload(requestBody); + NotificationTypeV2 type = payload.getNotificationType(); + log.info("### Notification Type: {}", type); + + Data data = payload.getData(); + JWSTransactionDecodedPayload transactionPayload = + appleService.getSignedTransactionPayload(data); + if (type == NotificationTypeV2.CONSUMPTION_REQUEST) { + log.info("Apple IAP Consumption Request Notification Received."); + String transactionId = transactionPayload.getTransactionId(); + appleService.sendConsumptionData(transactionId, pencilQueryService.getConsumptionRequest(transactionId)); + } else if (type == NotificationTypeV2.REFUND) { + log.info("Apple IAP ReFund Notification Received."); + String transactionId = transactionPayload.getOriginalTransactionId(); + pencilCommandService.handleRefundPurchase(transactionId); + } + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/apple/controller/MockAppleController.java b/src/main/java/umc/th/juinjang/api/apple/controller/MockAppleController.java new file mode 100644 index 00000000..6789b666 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/apple/controller/MockAppleController.java @@ -0,0 +1,47 @@ +package umc.th.juinjang.api.apple.controller; + +import java.util.Map; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.apple.itunes.storekit.model.ConsumptionRequest; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.pencil.service.PencilCommandService; +import umc.th.juinjang.api.pencil.service.PencilQueryService; + +@RestController +@RequestMapping("/api/apple/mock") +@RequiredArgsConstructor +@Slf4j +@Profile("dev") +public class MockAppleController { + + private final PencilQueryService pencilQueryService; + private final PencilCommandService pencilCommandService; + + @PostMapping("/refund") + public ResponseEntity mockRefund( + @RequestBody Map requestBody + ) { + String transactionId = requestBody.get("transactionId"); + pencilCommandService.handleRefundPurchase(transactionId); + return ResponseEntity.ok().build(); + } + + @PostMapping("/consumption/request") + public ResponseEntity mockConsumptionRequest( + @RequestBody Map requestBody + ) { + String transactionId = requestBody.get("transactionId"); + ConsumptionRequest result = pencilQueryService.getConsumptionRequest(transactionId); + return ResponseEntity.ok(result); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/apple/service/AppleService.java b/src/main/java/umc/th/juinjang/api/apple/service/AppleService.java new file mode 100644 index 00000000..3c12e600 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/apple/service/AppleService.java @@ -0,0 +1,271 @@ +package umc.th.juinjang.api.apple.service; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.ClassPathResource; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import com.apple.itunes.storekit.client.APIException; +import com.apple.itunes.storekit.client.AppStoreServerAPIClient; +import com.apple.itunes.storekit.model.ConsumptionRequest; +import com.apple.itunes.storekit.model.Data; +import com.apple.itunes.storekit.model.Environment; +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; +import com.apple.itunes.storekit.model.ResponseBodyV2; +import com.apple.itunes.storekit.model.ResponseBodyV2DecodedPayload; +import com.apple.itunes.storekit.model.TransactionInfoResponse; +import com.apple.itunes.storekit.verification.SignedDataVerifier; +import com.apple.itunes.storekit.verification.VerificationException; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.apple.service.command.AppleTransactionVerifyCommand; +import umc.th.juinjang.api.pencil.service.PencilQueryService; +import umc.th.juinjang.api.pencil.service.response.VerificationResult; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.AppleHandler; + +@Slf4j +@Service +@Profile("!local") +@RequiredArgsConstructor +public class AppleService { + + @Value("${apple.iap.bundle-id}") + private String bundleId; + + @Value("${apple.iap.key-id}") + private String keyId; + + @Value("${apple.iap.issuer-id}") + private String issuerId; + + @Value("${apple.iap.apple-id}") + private String appleIdStr; + + @Value("${apple.iap.environment}") + private String environmentString; // SANDBOX , PRODUCTION + + @Value("${apple.iap.certificate-names}") + private String certificateConfigs; + + @Value("${apple.iap.private-key-path}") + private String privateKeyPath; + + private SignedDataVerifier signedDataVerifier; + private AppStoreServerAPIClient appStoreServerAPIClient; + private PencilQueryService pencilQueryService; + + @PostConstruct + public void init() { + + log.info("Apple IAP 초기화 시작"); + log.info("Bundle ID: {}", bundleId); + log.info("Key ID: {}", keyId); + log.info("Issuer ID: {}", issuerId); + log.info("Environment: {}", environmentString); + log.info("Private Key Path: {}", privateKeyPath); + + Set rootCertificates = loadRootCertificates(); + + Environment environment = Environment.fromValue(environmentString); + Long appleId = Long.valueOf(appleIdStr); + + this.signedDataVerifier = new SignedDataVerifier( + rootCertificates, + bundleId, + appleId, + environment, + true + ); + + String signingKey = loadSigningKey(); + + this.appStoreServerAPIClient = new AppStoreServerAPIClient( + signingKey, + keyId, + issuerId, + bundleId, + environment + ); + + } + + @Retryable( + maxAttempts = 3, + backoff = @Backoff(delay = 1000), + retryFor = {APIException.class, IOException.class, VerificationException.class}) + public JWSTransactionDecodedPayload getTransactionInfo(String transactionId) throws + APIException, + IOException, + VerificationException { + log.info("Executing GetTransactionInfo for TRANSACTION_ID: {} - Thread: {}", + transactionId, Thread.currentThread().getName()); + + TransactionInfoResponse transactionInfo = appStoreServerAPIClient.getTransactionInfo(transactionId); + return signedDataVerifier.verifyAndDecodeTransaction(transactionInfo.getSignedTransactionInfo()); + } + + public VerificationResult verifyAppleTransaction(AppleTransactionVerifyCommand command) { + try { + JWSTransactionDecodedPayload payload = getTransactionInfo(command.getTransactionId()); + + if (!validateTransaction(payload, command)) { + return VerificationResult.ofVerificationError(); + } + + return VerificationResult.ofSuccess(payload); + + } catch (IOException | APIException e) { + log.warn("❌ Apple transaction verification error. transactionId: {}", command.getTransactionId(), e); + return VerificationResult.ofServerError(); + } catch (VerificationException e) { + log.warn("❌ Apple transaction verification error. transactionId: {}", command.getTransactionId(), e); + return VerificationResult.ofVerificationError(); + } + } + + public void sendConsumptionData(String transactionId, ConsumptionRequest request) { + try { + appStoreServerAPIClient.sendConsumptionData(transactionId, request); + } catch (IOException | APIException e) { + throw new AppleHandler(ErrorStatus.APPLE_VERIFICATION_ERROR); + } + + } + + public ResponseBodyV2DecodedPayload getNotificationPayload(ResponseBodyV2 responseBody) { + try { + return signedDataVerifier.verifyAndDecodeNotification( + responseBody.getSignedPayload()); + } catch (VerificationException e) { + throw new AppleHandler(ErrorStatus.APPLE_VERIFICATION_ERROR); + } + } + + public JWSTransactionDecodedPayload getSignedTransactionPayload( + Data data + ) { + try { + return signedDataVerifier.verifyAndDecodeTransaction( + data.getSignedTransactionInfo()); + } catch (VerificationException e) { + throw new AppleHandler(ErrorStatus.APPLE_VERIFICATION_ERROR); + } + } + + private boolean validateTransaction(JWSTransactionDecodedPayload decodedPayload, + AppleTransactionVerifyCommand command) { + // 트랜잭션 아이디가 정상적으로 일치하는 지 여부 + if (!decodedPayload.getTransactionId().equals(command.getTransactionId())) { + log.warn("트랜잭션 아이디 불일치. 애플 PAYLOAD : {}, REQUEST 요청 : {}", decodedPayload.getTransactionId(), + command.getTransactionId()); + return false; + } + + // 1. 환불/취소 여부 확인 + if (decodedPayload.getRevocationDate() != null || decodedPayload.getRevocationReason() != null) { + log.warn("트랜잭션이 취소되었습니다. 트랜잭션 ID: {}, 취소 이유: {}", + decodedPayload.getTransactionId(), decodedPayload.getRevocationReason()); + return false; + } + + // 2. 번들 ID가 앱의 번들 ID와 일치하는지 검증 + if (!bundleId.equals(decodedPayload.getBundleId())) { + log.warn("번들 ID 불일치. 예상: {}, 실제: {}", + bundleId, decodedPayload.getBundleId()); + return false; + } + + // 3. 상품 ID가 요청한 상품과 일치하는지 검증 + if (!command.getProductId().equals(decodedPayload.getProductId())) { + log.warn("상품 ID 불일치. 요청: {}, 응답: {}", + command.getProductId(), decodedPayload.getProductId()); + return false; + } + + // 4. 환경 확인 - 프로덕션에서는 프로덕션, 개발에서는 샌드박스인지 확인 + // boolean isProduction = !"Sandbox".equalsIgnoreCase(decodedPayload.getEnvironment()); + // if (isProduction) { + // log.warn("환경 불일치. 프로덕션 여부: {}, 프로덕션이어야 함: {}", + // isProduction, shouldBeProduction); + // return false; + // } + + // 5. 수량 검증 + if (decodedPayload.getQuantity() <= 0) { + log.warn("유효하지 않은 수량: {}", decodedPayload.getQuantity()); + return false; + } + + // 6. 앱 계정 토큰이 제공된 경우 일치하는지 확인 + if (command.getAppAccountToken() != null && decodedPayload.getAppAccountToken() != null && + !command.getAppAccountToken().equals(decodedPayload.getAppAccountToken())) { + log.warn("앱 계정 토큰 불일치. 요청: {}, 응답: {}", + command.getAppAccountToken(), decodedPayload.getAppAccountToken()); + return false; + } + + // 7. 모든 검증이 완료되었으므로 true 반환 + log.info("Apple IAP Purchase Validation Success. Transaction ID: {}", decodedPayload.getTransactionId()); + return true; + } + + private Set loadRootCertificates() { + try { + Set certificates = new HashSet<>(); + String[] certConfigs = certificateConfigs.split(","); + + for (String name : certConfigs) { + String certPath = "certs/" + name.trim(); + ClassPathResource resource = new ClassPathResource(certPath); + + if (resource.exists()) { + log.info("Loading certificate: {}", certPath); + certificates.add(resource.getInputStream()); + } else { + log.warn("Certificate not found: {}", certPath); + } + } + + if (certificates.isEmpty()) { + log.error("No certificates were loaded"); + throw new RuntimeException("Failed to load any certificates"); + } + + return certificates; + } catch (Exception e) { + log.error("Error loading root certificates: {}", e.getMessage(), e); + throw new RuntimeException("Failed to load root certificates", e); + } + } + + private String loadSigningKey() { + try { + log.info("Loading signing key from: {}", privateKeyPath); + + ClassPathResource resource = new ClassPathResource(privateKeyPath); + String privateKeyContent; + + try (InputStream inputStream = resource.getInputStream()) { + privateKeyContent = new String(inputStream.readAllBytes()); + } + + log.info("Signing key loaded successfully"); + return privateKeyContent; + + } catch (Exception e) { + log.error("Failed to load signing key: {}", e.getMessage(), e); + throw new RuntimeException("Failed to load signing key", e); + } + } + +} diff --git a/src/main/java/umc/th/juinjang/api/apple/service/command/AppleTransactionVerifyCommand.java b/src/main/java/umc/th/juinjang/api/apple/service/command/AppleTransactionVerifyCommand.java new file mode 100644 index 00000000..61fae3e6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/apple/service/command/AppleTransactionVerifyCommand.java @@ -0,0 +1,31 @@ +package umc.th.juinjang.api.apple.service.command; + +import java.util.UUID; + +import lombok.Builder; +import lombok.Getter; +import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest; + +@Getter +public class AppleTransactionVerifyCommand { + + private final String transactionId; + private final String productId; + private final UUID appAccountToken; + + @Builder + private AppleTransactionVerifyCommand(String transactionId, String productId, UUID appAccountToken) { + this.transactionId = transactionId; + this.productId = productId; + this.appAccountToken = appAccountToken; + } + + public static AppleTransactionVerifyCommand fromRequest(AppleIAPPurchaseRequest request) { + return AppleTransactionVerifyCommand.builder() + .transactionId(request.getTransactionId()) + .productId(request.getProductId()) + .appAccountToken(request.getAppAccountToken()) + .build(); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java b/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java new file mode 100644 index 00000000..594e7557 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/auth/controller/OAuthController.java @@ -0,0 +1,212 @@ +package umc.th.juinjang.api.auth.controller; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.micrometer.common.lang.Nullable; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.auth.controller.request.AppleLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.controller.request.KakaoLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.controller.request.WithdrawReasonRequestDto; +import umc.th.juinjang.api.auth.service.OAuthServiceV2; +import umc.th.juinjang.api.auth.service.WithdrawService; +import umc.th.juinjang.api.auth.service.response.LoginResponseDto; +import umc.th.juinjang.api.auth.service.response.LoginResponseVersion2Dto; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.SuccessStatus; +import umc.th.juinjang.domain.member.model.Member; + +@Slf4j +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Validated +public class OAuthController { + + // private final OAuthService oauthService; + private final OAuthServiceV2 oauthService; + private final WithdrawService withdrawService; + + // 카카오 로그인 + // 프론트 측에서 전달해준 사용자 정보로 토큰 발급 + @PostMapping("/kakao/login") + public ApiResponse kakaoLogin(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoLogin(targetId, kakaoReqDto)); + } + + // 카카오 로그인 (회원가입) + @PostMapping("/kakao/signup") + public ApiResponse kakaoSignUp(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoSignUpRequestDto kakaoSignUpReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoSignUp(targetId, kakaoSignUpReqDto)); + } + + //V2 + // 카카오 로그인 + @PostMapping("/v2/kakao/login") + public ApiResponse kakaoLoginVersion2(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoLoginVersion2(targetId, kakaoReqDto)); + } + + // 카카오 로그인 (회원가입) + @PostMapping("/v2/kakao/signup") + public ApiResponse kakaoSignUpVersion2(@RequestHeader("target-id") String kakaoTargetId, + @RequestBody @Validated KakaoSignUpRequestVersion2Dto kakaoSignUpReqDto) { + Long targetId; + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } + + targetId = Long.parseLong(kakaoTargetId); + return ApiResponse.onSuccess(oauthService.kakaoSignUpVersion2(targetId, kakaoSignUpReqDto)); + } + + // refreshToken으로 accessToken 재발급 + // Authorization : Bearer Token에 refreshToken 담기 + @PostMapping("/regenerate-token") + public ApiResponse regenerateAccessToken(HttpServletRequest request) { + String accessToken = request.getHeader("Authorization"); + String refreshToken = request.getHeader("Refresh-Token"); + + if (StringUtils.hasText(accessToken) && accessToken.startsWith("Bearer ") && StringUtils.hasText(refreshToken) + && refreshToken.startsWith("Bearer ")) { + LoginResponseDto result = oauthService.regenerateAccessToken(accessToken.substring(7), + refreshToken.substring(7)); + return ApiResponse.onSuccess(result); + } else + throw new ExceptionHandler(TOKEN_EMPTY); + } + + // 로그아웃 -> refresh 토큰 만료 + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + String token = request.getHeader("Refresh-Token"); + + if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { + String result = oauthService.logout(token.substring(7)); + return ApiResponse.onSuccess(result); + } else + throw new ExceptionHandler(TOKEN_EMPTY); + } + + // 애플 로그인 + // 클라이언트에서 identity token 값 받아오기 + // 사용자가 입력한 정보를 바탕으로 Apple ID servers 에게 Identity Token 발급 요청 (프론트가) -> 이를 우리 서버가 가져오는 것 + // Identity Token 값을 바탕으로 사용자 식별 & refresh, access Token 발급해주고 DB 저장 (로그인하기) + + // 로그인 + @PostMapping("/apple/login") + public ApiResponse appleLogin(@RequestBody @Validated AppleLoginRequestDto appleReqDto) { + if (appleReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleLogin(appleReqDto)); + } + + @PostMapping("/apple/signup") + public ApiResponse appleSignUp(@RequestBody @Validated AppleSignUpRequestDto appleSignUpReqDto) { + if (appleSignUpReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleSignUp(appleSignUpReqDto)); + } + + //V2 + @PostMapping("/v2/apple/login") + public ApiResponse appleLoginVersion2( + @RequestBody @Validated AppleLoginRequestDto appleReqDto) { + if (appleReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleLoginVersion2(appleReqDto)); + } + + @PostMapping("/v2/apple/signup") + public ApiResponse appleSignUpVersion2( + @RequestBody @Validated AppleSignUpRequestVersion2Dto appleSignUpReqDto) { + if (appleSignUpReqDto.getIdentityToken() == null) + throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); + return ApiResponse.onSuccess(oauthService.appleSignUpVersion2(appleSignUpReqDto)); + } + + // 카카오 탈퇴 + @DeleteMapping("/withdraw/kakao") + public ApiResponse kakaoWithdraw(@AuthenticationPrincipal Member member, + @RequestHeader("target-id") String kakaoTargetId, @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto) { + Long targetId; + + if (kakaoTargetId == null) { + throw new ExceptionHandler(EMPTY_TARGET_ID); + } else { + targetId = Long.parseLong(kakaoTargetId); + if (!targetId.equals(member.getKakaoTargetId())) { + throw new ExceptionHandler(UNCORRECTED_TARGET_ID); + } + } + + // 카카오 계정 연결 끊기 + // TODO : 해당 로직을 스케쥴러로 이동할 필요가 있음. + boolean isUnlink = oauthService.kakaoWithdraw(member, targetId); + + // 탈퇴 사유 추가 + if (withdrawReasonReqDto.getWithdrawReason() != null) { + withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); + } + + // 사용자 정보 삭제 (DB) + if (!isUnlink) { + throw new ExceptionHandler(NOT_UNLINK_KAKAO); + } + + return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); + } + + // 애플 탈퇴 + @DeleteMapping("/withdraw/apple") + public ApiResponse withdraw(@AuthenticationPrincipal Member member, + @Nullable @RequestHeader("X-Apple-Code") final String code, + @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto) { + oauthService.appleWithdraw(member, code); + + // 탈퇴 사유 추가 + if (withdrawReasonReqDto.getWithdrawReason() != null) { + withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); + } + + return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); + } + +} diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleInfo.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleInfo.java similarity index 85% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleInfo.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/AppleInfo.java index 8f0cf017..8e0b6d2d 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleInfo.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleInfo.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.api.auth.controller.request; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleLoginRequestDto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleLoginRequestDto.java similarity index 86% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleLoginRequestDto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/AppleLoginRequestDto.java index f867efd4..a44b8267 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleLoginRequestDto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleLoginRequestDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import lombok.AccessLevel; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleSignUpRequestDto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleSignUpRequestDto.java similarity index 85% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleSignUpRequestDto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/AppleSignUpRequestDto.java index 4dcf9d83..55b9da93 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleSignUpRequestDto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleSignUpRequestDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import lombok.AccessLevel; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleSignUpRequestVersion2Dto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleSignUpRequestVersion2Dto.java similarity index 87% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleSignUpRequestVersion2Dto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/AppleSignUpRequestVersion2Dto.java index 786e9bf8..0f48eb78 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleSignUpRequestVersion2Dto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleSignUpRequestVersion2Dto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import lombok.AccessLevel; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleTokenRequest.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleTokenRequest.java similarity index 83% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleTokenRequest.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/AppleTokenRequest.java index a214dc59..9b44f6bd 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleTokenRequest.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/AppleTokenRequest.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.api.auth.controller.request; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoLoginRequestDto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoLoginRequestDto.java similarity index 85% rename from src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoLoginRequestDto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoLoginRequestDto.java index b5fed16e..b0edd626 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoLoginRequestDto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoLoginRequestDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.kakao; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import lombok.AccessLevel; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoSignUpRequestDto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoSignUpRequestDto.java similarity index 88% rename from src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoSignUpRequestDto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoSignUpRequestDto.java index dc1cc211..74a21922 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoSignUpRequestDto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoSignUpRequestDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.kakao; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoSignUpRequestVersion2Dto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoSignUpRequestVersion2Dto.java similarity index 88% rename from src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoSignUpRequestVersion2Dto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoSignUpRequestVersion2Dto.java index a3fe3057..6f555bcc 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/kakao/KakaoSignUpRequestVersion2Dto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/KakaoSignUpRequestVersion2Dto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.kakao; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import lombok.AccessLevel; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/WithdrawReasonRequestDto.java b/src/main/java/umc/th/juinjang/api/auth/controller/request/WithdrawReasonRequestDto.java similarity index 84% rename from src/main/java/umc/th/juinjang/model/dto/auth/WithdrawReasonRequestDto.java rename to src/main/java/umc/th/juinjang/api/auth/controller/request/WithdrawReasonRequestDto.java index 1cfed88d..3afc37f1 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/WithdrawReasonRequestDto.java +++ b/src/main/java/umc/th/juinjang/api/auth/controller/request/WithdrawReasonRequestDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth; +package umc.th.juinjang.api.auth.controller.request; import jakarta.validation.constraints.NotEmpty; import lombok.AccessLevel; diff --git a/src/main/java/umc/th/juinjang/api/auth/service/OAuthService.java b/src/main/java/umc/th/juinjang/api/auth/service/OAuthService.java new file mode 100644 index 00000000..cdde265a --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/auth/service/OAuthService.java @@ -0,0 +1,624 @@ +package umc.th.juinjang.api.auth.service; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.auth.controller.request.AppleInfo; +import umc.th.juinjang.api.auth.controller.request.AppleLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.controller.request.KakaoLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.service.response.LoginResponseDto; +import umc.th.juinjang.api.auth.service.response.LoginResponseVersion2Dto; +import umc.th.juinjang.auth.jwt.JwtService; +import umc.th.juinjang.auth.jwt.TokenDto; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.domain.checklist.repository.ChecklistAnswerRepository; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.image.repository.ImageRepository; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.repository.LimjangPriceRepository; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.domain.record.model.Record; +import umc.th.juinjang.domain.record.repository.RecordRepository; +import umc.th.juinjang.domain.report.repository.ReportRepository; +import umc.th.juinjang.domain.scrap.repository.ScrapRepository; +import umc.th.juinjang.event.publisher.MemberEventPublisher; +import umc.th.juinjang.external.openfeign.apple.AppleClientSecretGenerator; +import umc.th.juinjang.external.openfeign.apple.AppleOAuthProvider; +import umc.th.juinjang.external.openfeign.kakao.KakaoUnlinkClient; +import umc.th.juinjang.external.s3.S3Service; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class OAuthService { + + private final MemberRepository memberRepository; + private final JwtService jwtService; + private final AppleClientSecretGenerator appleClientSecretGenerator; + private final AppleOAuthProvider appleOAuthProvider; + private final ScrapRepository scrapRepository; + private final LimjangRepository limjangRepository; + private final ChecklistAnswerRepository checklistAnswerRepository; + private final RecordRepository recordRepository; + private final ImageRepository imageRepository; + private final ReportRepository reportRepository; + private final S3Service s3Service; + private final LimjangPriceRepository limjangPriceRepository; + private final MemberEventPublisher memberEventPublisher; + + @Autowired + private KakaoUnlinkClient kakaoUnlinkClient; + + @Value("${security.oauth2.client.registration.kakao.admin-key}") + private String kakaoAdminKey; + + // 카카오 로그인 (회원가입된 경우) + // 프론트에서 받은 사용자 정보로 accessToken, refreshToken 발급 + @Transactional + public LoginResponseDto kakaoLogin(Long targetId, KakaoLoginRequestDto kakaoReqDto) { + String email = kakaoReqDto.getEmail(); + log.info(kakaoReqDto.getEmail()); + + if (email == null) + throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); + + Optional getMemberByEmail = memberRepository.findByEmail(email); + Optional getMemberByTargetId = memberRepository.findByKakaoTargetId(targetId); + Member member = null; + + if (getMemberByEmail.isPresent() && getMemberByTargetId.isEmpty()) { + if (!getMemberByEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } else { // 잘못된 target_id가 들어왔을때(db에 없는) + throw new MemberHandler(UNCORRECTED_TARGET_ID); + } + } else if (getMemberByEmail.isPresent() && getMemberByTargetId.isPresent()) { // 이미 회원가입한 회원인 경우 + if (!getMemberByEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } else if (getMemberByEmail.get().getMemberId() != getMemberByTargetId.get().getMemberId()) { + throw new MemberHandler(FAILED_TO_LOGIN); + } + member = getMemberByEmail.get(); + } else if (getMemberByEmail.isEmpty() + && getMemberByTargetId.isEmpty()) { // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 + throw new MemberHandler(MEMBER_NOT_FOUND); + } + + if (member == null) { + throw new MemberHandler(FAILED_TO_LOGIN); + } + + // accessToken, refreshToken 발급 후 반환 + return createToken(member); + } + + // 카카오 로그인 (회원가입 해야하는 경우) + @Transactional + public LoginResponseDto kakaoSignUp(Long targetId, KakaoSignUpRequestDto kakaoSignUpReqDto) { + String email = kakaoSignUpReqDto.getEmail(); + log.info(kakaoSignUpReqDto.getEmail()); + + if (email == null) + throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); + + Optional getMember = memberRepository.findByEmail(email); + Optional getTargetId = memberRepository.findByKakaoTargetId(targetId); + + Member member = null; + + if (getMember.isPresent() && getTargetId.isEmpty() && getMember.get() + .getProvider() + .equals(MemberProvider.APPLE)) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } else if (getMember.isPresent() && getTargetId.isPresent()) { + // if(!getMember.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + // throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + // } else + if ((getTargetId.get().getMemberId() != getMember.get().getMemberId())) { + throw new MemberHandler(FAILED_TO_LOGIN); + } else if (getMember.get().getProvider().equals(MemberProvider.KAKAO)) { + throw new MemberHandler(ALREADY_MEMBER); + } + } else if (getMember.isPresent() || getTargetId.isPresent()) { // 둘 중 하나만 존재할 때 실행될 코드 + throw new MemberHandler(FAILED_TO_LOGIN); + } else if (!getMember.isPresent() + && !getTargetId.isPresent()) { // 두 값 모두 존재하지 않을 때 실행될 코드, 아직 회원가입 하지 않은 회원인 경우 + // member = memberRepository.save( + // Member.builder() + // .email(email) + // .provider(MemberProvider.KAKAO) + // .kakaoTargetId(targetId) + // .nickname(kakaoSignUpReqDto.getNickname()) + // .refreshToken("") + // .refreshTokenExpiresAt(LocalDateTime.now()) + // .build() + // ); + member = Member.createKakaoMember( + email, + targetId, + kakaoSignUpReqDto.getNickname(), + null + ); + } + + if (member == null) { + throw new MemberHandler(FAILED_TO_SIGNUP); + } + + // accessToken, refreshToken 발급 후 반환 + publishDiscordAlert(member); + return createToken(member); + } + + private void publishDiscordAlert(Member member) { + memberEventPublisher.publishSignUpEvent(member); + } + + // accessToken, refreshToken 발급 + @Transactional + public LoginResponseDto createToken(Member member) { + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); + String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); + + // DB에 refreshToken 저장 + member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + return new LoginResponseDto(newAccessToken, newRefreshToken, member.getEmail()); + } + + //ver2 + // accessToken, refreshToken 발급 + @Transactional + public LoginResponseVersion2Dto createTokenVersion2(Member member) { + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); + String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); + + // DB에 refreshToken 저장 + member.updateRefreshToken(newRefreshToken); + + return new LoginResponseVersion2Dto(newAccessToken, newRefreshToken, member.getEmail(), + member.getAgreeVersion()); + } + + // refreshToken으로 accessToken 발급하기 + @Transactional + public LoginResponseDto regenerateAccessToken(String accessToken, String refreshToken) { + if (jwtService.validateTokenBoolean(accessToken)) // access token 유효성 검사 + throw new ExceptionHandler(ACCESS_TOKEN_AUTHORIZED); + + if (!jwtService.validateTokenBoolean(refreshToken)) // refresh token 유효성 검사 + throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + Long memberId = jwtService.getMemberIdFromJwtToken(refreshToken); + + Optional getMember = memberRepository.findById(memberId); + if (getMember.isEmpty()) + throw new MemberHandler(MEMBER_NOT_FOUND); + + Member member = getMember.get(); + if (!refreshToken.equals(member.getRefreshToken())) + throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + String newRefreshToken = jwtService.encodeJwtRefreshToken(memberId); + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(memberId)); + + member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + return new LoginResponseDto(newAccessToken, newRefreshToken, member.getNickname()); + } + + // 로그아웃 + @Transactional + public String logout(String refreshToken) { + Optional getMember = memberRepository.findByRefreshToken(refreshToken); + if (getMember.isEmpty()) + throw new MemberHandler(MEMBER_NOT_FOUND); + + Member member = getMember.get(); + if (member.getRefreshToken().equals("")) + throw new MemberHandler(ALREADY_LOGOUT); + + member.refreshTokenExpires(); + memberRepository.save(member); + + return "로그아웃 성공"; + } + + // 애플 로그인 (회원가입된 경우) + @Transactional + public LoginResponseDto appleLogin(AppleLoginRequestDto appleLoginRequest) { + // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find + // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token + // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) + // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) + // 4. db에 email, sub값 둘 다 없으면 회원가입 + // 탈퇴 처리는 추후에 + log.info("Oauth service 까지 들어옴" + appleLoginRequest.getIdentityToken()); + AppleInfo appleInfo = jwtService.getAppleAccountId(appleLoginRequest.getIdentityToken().replaceAll("\\n", "")); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional findSub = memberRepository.findByAppleSub(sub); + Optional findEmail = memberRepository.findByEmail(email); + + Member member = null; + if (findSub.isEmpty() && findEmail.isPresent() && findEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Apple 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } else if (findSub.isPresent() && findEmail.isPresent()) { // 재로그인 + if (!findEmail.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입했지만 apple이 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } else if (findSub.get().getMemberId() != findEmail.get().getMemberId()) { + throw new MemberHandler(FAILED_TO_LOGIN); + } + member = findEmail.get(); + } else if (!findSub.isPresent() && !findEmail.isPresent()) { // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 + throw new MemberHandler(MEMBER_NOT_FOUND); + } + + // accessToken, refreshToken 발급 + if (member == null) + throw new MemberHandler(MEMBER_NOT_FOUND); + return createToken(member); + } + + // 애플 로그인 (회원가입 해야하는 경우) + @Transactional + public LoginResponseDto appleSignUp(AppleSignUpRequestDto appleSignUpRequestDto) { + // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find + // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token + // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) + // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) + // 4. db에 email, sub값 둘 다 없으면 회원가입 + // 탈퇴 처리는 추후에 + + AppleInfo appleInfo = jwtService.getAppleAccountId(appleSignUpRequestDto.getIdentityToken()); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional findSub = memberRepository.findByAppleSub(sub); + Optional findEmail = memberRepository.findByEmail(email); + + Member member = null; + if (findSub.isPresent() && findEmail.isPresent() && (findSub.get().getMemberId() == findEmail.get() + .getMemberId()) + && findSub.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입한 회원인 경우 -> 에러 발생 + throw new MemberHandler(ALREADY_MEMBER); + } else if (!findSub.isPresent() && findEmail.isPresent() && findEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } else if (!findSub.isPresent() && !findEmail.isPresent()) { + member = memberRepository.save( + Member.builder() + .email(email) + .nickname(appleSignUpRequestDto.getNickname()) + .provider(MemberProvider.APPLE) + .appleSub(sub) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now()) + .build() + ); + System.out.println("member id : " + member.getMemberId()); + System.out.println("member email : " + member.getEmail()); + } + + // accessToken, refreshToken 발급 + if (member == null) + throw new MemberHandler(FAILED_TO_LOGIN); + + publishDiscordAlert(member); + return createToken(member); + } + + // 카카오 탈퇴 (카카오 연결 끊기) + @Transactional + public boolean kakaoWithdraw(Member member, Long kakaoTargetId) { + ResponseEntity response = kakaoUnlinkClient.unlinkUser("KakaoAK " + kakaoAdminKey, "user_id", + kakaoTargetId); + + if (response.getStatusCode().is2xxSuccessful()) { // 성공 처리 로직 + log.info("카카오 탈퇴 성공"); + log.info("member id :: " + member.getMemberId()); + + deleteMemberData(member); + + return true; + } else { // 실패 처리 로직 + return false; + } + } + + @Transactional + public void appleWithdraw(Member member, String code) { + + if (member.getProvider() != MemberProvider.APPLE) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } + try { + String clientSecret = appleClientSecretGenerator.generateClientSecret(); + String refreshToken = appleOAuthProvider.getAppleRefreshToken(code, clientSecret); + appleOAuthProvider.requestRevoke(refreshToken, clientSecret); + } catch (Exception e) { + throw new MemberHandler(FAILED_TO_LOAD_PRIVATE_KEY); + } + log.info("애플 탈퇴 성공"); + log.info("member id :: " + member.getMemberId()); + + deleteMemberData(member); + } + + @Transactional + public void deleteAllByLimjangId(Limjang limjang) { + scrapRepository.deleteByLimjangId(limjang.getLimjangId()); + checklistAnswerRepository.deleteByLimjangId(limjang.getLimjangId()); + limjangPriceRepository.deleteAllByLimjang(limjang); + List imageList = limjang.getImageList() + .stream() + .map(Image::getImageUrl) + .collect(Collectors.toList()); + List recordList = limjang.getRecordList() + .stream() + .map(Record::getRecordUrl) + .collect(Collectors.toList()); + + deleteFromS3(imageList); + deleteFromS3(recordList); + + imageRepository.deleteByLimjangId(limjang.getLimjangId()); + recordRepository.deleteByLimjangId(limjang.getLimjangId()); + reportRepository.deleteByLimjangId(limjang.getLimjangId()); + + } + + @Transactional + public void deleteMemberData(Member member) { + List limjangList = limjangRepository.findLimjangByMemberIdIgnoreDeleted(member.getMemberId()); + + for (Limjang limjang : limjangList) { + deleteAllByLimjangId(limjang); + } + + if (member.getImageUrl() != null) { + deleteFromS3(Collections.singletonList(member.getImageUrl())); + } + + limjangRepository.deleteAllByMemberId(member.getMemberId()); + memberRepository.deleteById(member.getMemberId()); + } + + @Transactional + public void deleteFromS3(List urlList) { + for (String url : urlList) { + s3Service.deleteFile(url); + } + } + + // V2 + // 카카오 + @Transactional + public LoginResponseVersion2Dto kakaoLoginVersion2(Long targetId, KakaoLoginRequestDto kakaoReqDto) { + String email = kakaoReqDto.getEmail(); + log.info(kakaoReqDto.getEmail()); + + if (email == null) + throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); + + Optional getMemberByEmail = memberRepository.findByEmail(email); + Optional getMemberByTargetId = memberRepository.findByKakaoTargetId(targetId); + Member member = null; + + if (getMemberByEmail.isPresent() && getMemberByTargetId.isEmpty()) { + if (!getMemberByEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } else { // 잘못된 target_id가 들어왔을때(db에 없는) + throw new MemberHandler(UNCORRECTED_TARGET_ID); + } + } else if (getMemberByEmail.isPresent() && getMemberByTargetId.isPresent()) { // 이미 회원가입한 회원인 경우 + if (!getMemberByEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } else if (getMemberByEmail.get().getMemberId() != getMemberByTargetId.get().getMemberId()) { + throw new MemberHandler(FAILED_TO_LOGIN); + } + member = getMemberByEmail.get(); + } else if (getMemberByEmail.isEmpty() + && getMemberByTargetId.isEmpty()) { // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 + throw new MemberHandler(MEMBER_NOT_FOUND); + } + + if (member == null) { + throw new MemberHandler(FAILED_TO_LOGIN); + } + + // accessToken, refreshToken 발급 후 반환 + return createTokenVersion2(member); + } + + // 카카오 로그인 (회원가입 해야하는 경우) + @Transactional + public LoginResponseVersion2Dto kakaoSignUpVersion2(Long targetId, + KakaoSignUpRequestVersion2Dto kakaoSignUpReqDto) { + String email = kakaoSignUpReqDto.getEmail(); + log.info(kakaoSignUpReqDto.getEmail()); + + if (email == null) + throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); + + Optional getMember = memberRepository.findByEmail(email); + Optional getTargetId = memberRepository.findByKakaoTargetId(targetId); + + Member member = null; + + if (getMember.isPresent() && getTargetId.isEmpty() && getMember.get() + .getProvider() + .equals(MemberProvider.APPLE)) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } else if (getMember.isPresent() && getTargetId.isPresent()) { + // if(!getMember.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 + // throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + // } else + if ((getTargetId.get().getMemberId() != getMember.get().getMemberId())) { + throw new MemberHandler(FAILED_TO_LOGIN); + } else if (getMember.get().getProvider().equals(MemberProvider.KAKAO)) { + throw new MemberHandler(ALREADY_MEMBER); + } + } else if (getMember.isPresent() || getTargetId.isPresent()) { // 둘 중 하나만 존재할 때 실행될 코드 + throw new MemberHandler(FAILED_TO_LOGIN); + } else if (!getMember.isPresent() + && !getTargetId.isPresent()) { // 두 값 모두 존재하지 않을 때 실행될 코드, 아직 회원가입 하지 않은 회원인 경우 + member = memberRepository.save( + Member.createKakaoMember( + email, + targetId, + kakaoSignUpReqDto.getNickname(), + kakaoSignUpReqDto.getAgreeVersion() + ) + ); + } + + if (member == null) { + throw new MemberHandler(FAILED_TO_SIGNUP); + } + + // accessToken, refreshToken 발급 후 반환 + publishDiscordAlert(member); + return createTokenVersion2(member); + } + + // 애플 + public LoginResponseVersion2Dto appleLoginVersion2(AppleLoginRequestDto appleLoginRequest) { + // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find + // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token + // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) + // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) + // 4. db에 email, sub값 둘 다 없으면 회원가입 + + AppleInfo appleInfo = jwtService.getAppleAccountId(appleLoginRequest.getIdentityToken().replaceAll("\\n", "")); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional findSub = memberRepository.findByAppleSub(sub); + Optional findEmail = memberRepository.findByEmail(email); + + Member member = null; + if (findSub.isEmpty() && findEmail.isPresent() && findEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Apple 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } else if (findSub.isPresent() && findEmail.isPresent()) { // 재로그인 + if (!findEmail.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입했지만 apple이 아닌 다른 소셜 로그인 사용 + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } else if (findSub.get().getMemberId() != findEmail.get().getMemberId()) { + throw new MemberHandler(FAILED_TO_LOGIN); + } + member = findEmail.get(); + } else if (!findSub.isPresent() && !findEmail.isPresent()) { // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 + throw new MemberHandler(MEMBER_NOT_FOUND); + } + + // accessToken, refreshToken 발급 + if (member == null) + throw new MemberHandler(MEMBER_NOT_FOUND); + return createTokenVersion2(member); + + } + + @Transactional + public LoginResponseVersion2Dto appleSignUpVersion2( + AppleSignUpRequestVersion2Dto appleSignUpRequestDto) { + // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find + // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token + // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) + // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) + // 4. db에 email, sub값 둘 다 없으면 회원가입 + // 탈퇴 처리는 추후에 + + AppleInfo appleInfo = jwtService.getAppleAccountId(appleSignUpRequestDto.getIdentityToken()); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional findSub = memberRepository.findByAppleSub(sub); + Optional findEmail = memberRepository.findByEmail(email); + + Member member = null; + if (findSub.isPresent() && findEmail.isPresent() && (findSub.get().getMemberId() == findEmail.get() + .getMemberId()) + && findSub.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입한 회원인 경우 -> 에러 발생 + throw new MemberHandler(ALREADY_MEMBER); + } else if (!findSub.isPresent() && findEmail.isPresent() && findEmail.get() + .getProvider() + .equals(MemberProvider.KAKAO)) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } else if (!findSub.isPresent() && !findEmail.isPresent()) { + // member = memberRepository.save( + // Member.builder() + // .email(email) + // .nickname(appleSignUpRequestDto.getNickname()) + // .provider(MemberProvider.APPLE) + // .appleSub(sub) + // .refreshToken("") + // .refreshTokenExpiresAt(LocalDateTime.now()) + // .agreeVersion(appleSignUpRequestDto.getAgreeVersion()) + // .build() + // ); + member = Member.createAppleMember( + email, + sub, + appleSignUpRequestDto.getNickname(), + appleSignUpRequestDto.getAgreeVersion() + ); + } + + // accessToken, refreshToken 발급 + if (member == null) + throw new MemberHandler(FAILED_TO_LOGIN); + + publishDiscordAlert(member); + return createTokenVersion2(member); + } +} diff --git a/src/main/java/umc/th/juinjang/api/auth/service/OAuthServiceV2.java b/src/main/java/umc/th/juinjang/api/auth/service/OAuthServiceV2.java new file mode 100644 index 00000000..5081f066 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/auth/service/OAuthServiceV2.java @@ -0,0 +1,404 @@ +package umc.th.juinjang.api.auth.service; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.auth.controller.request.AppleInfo; +import umc.th.juinjang.api.auth.controller.request.AppleLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.AppleSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.controller.request.KakaoLoginRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestDto; +import umc.th.juinjang.api.auth.controller.request.KakaoSignUpRequestVersion2Dto; +import umc.th.juinjang.api.auth.service.response.LoginResponseDto; +import umc.th.juinjang.api.auth.service.response.LoginResponseVersion2Dto; +import umc.th.juinjang.auth.jwt.JwtService; +import umc.th.juinjang.auth.jwt.TokenDto; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; +import umc.th.juinjang.domain.member.model.MemberStatus; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.event.publisher.MemberEventPublisher; +import umc.th.juinjang.external.openfeign.apple.AppleClientSecretGenerator; +import umc.th.juinjang.external.openfeign.apple.AppleOAuthProvider; +import umc.th.juinjang.external.openfeign.kakao.KakaoUnlinkClient; + +@Slf4j +@Service +@RequiredArgsConstructor +public class OAuthServiceV2 { + + private final MemberRepository memberRepository; + private final JwtService jwtService; + private final AppleClientSecretGenerator appleClientSecretGenerator; + private final AppleOAuthProvider appleOAuthProvider; + private final MemberEventPublisher memberEventPublisher; + + @Autowired + private KakaoUnlinkClient kakaoUnlinkClient; + + @Value("${security.oauth2.client.registration.kakao.admin-key}") + private String kakaoAdminKey; + + @Transactional + public LoginResponseDto kakaoLogin(Long targetId, KakaoLoginRequestDto dto) { + Member member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + dto.getEmail(), + targetId, + MemberStatus.ACTIVE + ).orElseThrow(() -> handleKakaoLoginError(dto.getEmail(), targetId)); + + return createToken(member); + } + + @Transactional + public LoginResponseDto kakaoSignUp(Long targetId, KakaoSignUpRequestDto kakaoLoginRequestDto) { + Optional member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + kakaoLoginRequestDto.getEmail(), + targetId, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createKakaoMember( + kakaoLoginRequestDto.getEmail(), + targetId, + kakaoLoginRequestDto.getNickname(), + null + ) + ); + + publishDiscordAlert(newMember); + return createToken(newMember); + } + } + + @Transactional + public LoginResponseVersion2Dto kakaoLoginVersion2(Long targetId, KakaoLoginRequestDto dto) { + Member member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + dto.getEmail(), + targetId, + MemberStatus.ACTIVE + ).orElseThrow(() -> handleKakaoLoginError(dto.getEmail(), targetId)); + + return createTokenVersion2(member); + } + + @Transactional + public LoginResponseVersion2Dto kakaoSignUpVersion2(Long targetId, KakaoSignUpRequestVersion2Dto dto) { + Optional member = + memberRepository.findByEmailAndKakaoTargetIdAndStatus( + dto.getEmail(), + targetId, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createKakaoMember( + dto.getEmail(), + targetId, + dto.getNickname(), + dto.getAgreeVersion() + ) + ); + + publishDiscordAlert(newMember); + return createTokenVersion2(newMember); + } + } + + @Transactional + public String logout(String refreshToken) { + Optional getMember = memberRepository.findByRefreshToken(refreshToken); + if (getMember.isEmpty()) + throw new MemberHandler(MEMBER_NOT_FOUND); + + Member member = getMember.get(); + if (member.getRefreshToken().equals("")) + throw new MemberHandler(ALREADY_LOGOUT); + + member.refreshTokenExpires(); + memberRepository.save(member); + + return "로그아웃 성공"; + } + + @Transactional + public LoginResponseDto regenerateAccessToken(String accessToken, String refreshToken) { + if (jwtService.validateTokenBoolean(accessToken)) // access token 유효성 검사 + throw new ExceptionHandler(ACCESS_TOKEN_AUTHORIZED); + + if (!jwtService.validateTokenBoolean(refreshToken)) // refresh token 유효성 검사 + throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + Long memberId = jwtService.getMemberIdFromJwtToken(refreshToken); + + Optional getMember = memberRepository.findById(memberId); + if (getMember.isEmpty()) + throw new MemberHandler(MEMBER_NOT_FOUND); + + Member member = getMember.get(); + if (!refreshToken.equals(member.getRefreshToken())) + throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); + + String newRefreshToken = jwtService.encodeJwtRefreshToken(memberId); + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(memberId)); + + member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + return new LoginResponseDto(newAccessToken, newRefreshToken, member.getNickname()); + } + + @Transactional + public LoginResponseDto createToken(Member member) { + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); + String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); + + // DB에 refreshToken 저장 + member.updateRefreshToken(newRefreshToken); + memberRepository.save(member); + + return new LoginResponseDto(newAccessToken, newRefreshToken, member.getEmail()); + } + + @Transactional + public LoginResponseVersion2Dto createTokenVersion2(Member member) { + String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); + String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); + + // DB에 refreshToken 저장 + member.updateRefreshToken(newRefreshToken); + + return new LoginResponseVersion2Dto(newAccessToken, newRefreshToken, member.getEmail(), + member.getAgreeVersion()); + } + + private void publishDiscordAlert(Member member) { + memberEventPublisher.publishSignUpEvent(member); + } + + @Transactional + public LoginResponseDto appleLogin(AppleLoginRequestDto request) { + log.info("Oauth service 까지 들어옴{}", request.getIdentityToken()); + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken().replaceAll("\\n", "")); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + Member member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ).orElseThrow(() -> handleAppleLoginError(email, sub)); + + return createToken(member); + } + + @Transactional + public LoginResponseDto appleSignUp(AppleSignUpRequestDto request) { + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken()); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + Optional member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createAppleMember( + email, + sub, + request.getNickname(), + null + ) + ); + + publishDiscordAlert(newMember); + return createToken(newMember); + } + } + + @Transactional + public LoginResponseVersion2Dto appleLoginVersion2(AppleLoginRequestDto request) { + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken().replaceAll("\\n", "")); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Member member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ).orElseThrow(() -> handleAppleLoginError(email, sub)); + + return createTokenVersion2(member); + } + + @Transactional + public LoginResponseVersion2Dto appleSignUpVersion2(AppleSignUpRequestVersion2Dto request) { + AppleInfo appleInfo = jwtService.getAppleAccountId(request.getIdentityToken()); + String email = appleInfo.getEmail(); + String sub = appleInfo.getSub(); + + if (email == null || sub == null) + throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); + + Optional member = + memberRepository.findByEmailAndAppleSubAndStatus( + email, + sub, + MemberStatus.ACTIVE + ); + + if (member.isPresent()) { + throw new MemberHandler(ALREADY_MEMBER); + } else { + Member newMember = memberRepository.save( + Member.createAppleMember( + email, + sub, + request.getNickname(), + request.getAgreeVersion() + ) + ); + + publishDiscordAlert(newMember); + return createTokenVersion2(newMember); + } + } + + @Transactional + public boolean kakaoWithdraw(Member member, Long targetId) { + ResponseEntity response = kakaoUnlinkClient.unlinkUser("KakaoAK " + kakaoAdminKey, "user_id", + targetId); + + if (response.getStatusCode().is2xxSuccessful()) { // 성공 처리 로직 + log.info("카카오 탈퇴 성공"); + log.info("member id :: {}", member.getMemberId()); + + Member withdrawMember = (Member)memberRepository.findByMemberIdAndStatus(member.getMemberId(), + MemberStatus.ACTIVE) + .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); + + withdrawMember.kakaoWithdraw(); + + return true; + } else { // 실패 처리 로직 + return false; + } + } + + @Transactional + public void appleWithdraw(Member member, String code) { + if (member.getProvider() != MemberProvider.APPLE) { + throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } + try { + String clientSecret = appleClientSecretGenerator.generateClientSecret(); + String refreshToken = appleOAuthProvider.getAppleRefreshToken(code, clientSecret); + appleOAuthProvider.requestRevoke(refreshToken, clientSecret); + } catch (Exception e) { + throw new MemberHandler(FAILED_TO_LOAD_PRIVATE_KEY); + } + log.info("애플 탈퇴 성공"); + log.info("member id :: {}", member.getMemberId()); + + Member withdrawMember = (Member)memberRepository.findByMemberIdAndStatus(member.getMemberId(), + MemberStatus.ACTIVE) + .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); + + withdrawMember.appleWithdraw(); + } + + private MemberHandler handleKakaoLoginError(String email, Long targetId) { + if (email == null || email.trim().isEmpty()) { + return new MemberHandler(MEMBER_EMAIL_NOT_FOUND); + } + + Optional getMemberByEmail = memberRepository.findByEmail(email); + Optional getMemberByTargetId = memberRepository.findByKakaoTargetId(targetId); + + if (getMemberByEmail.isPresent()) { + Member foundMember = getMemberByEmail.get(); + + if (!foundMember.getProvider().equals(MemberProvider.KAKAO)) { + return new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } + } + + if (getMemberByTargetId.isPresent()) { + Member foundMember = getMemberByTargetId.get(); + if (!foundMember.getProvider().equals(MemberProvider.KAKAO)) { + return new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); + } + } + + return new MemberHandler(MEMBER_NOT_FOUND); + } + + private MemberHandler handleAppleLoginError(String email, String sub) { + if (email == null || email.trim().isEmpty()) { + return new MemberHandler(MEMBER_EMAIL_NOT_FOUND); + } + + if (sub == null || sub.trim().isEmpty()) { + return new MemberHandler(INVALID_APPLE_ID_TOKEN); + } + + Optional getMemberByEmail = memberRepository.findByEmail(email); + Optional getMemberBySub = memberRepository.findByAppleSub(sub); + + if (getMemberByEmail.isPresent()) { + Member foundMember = getMemberByEmail.get(); + + if (!foundMember.getProvider().equals(MemberProvider.APPLE)) { + return new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } + } + + if (getMemberBySub.isPresent()) { + Member foundMember = getMemberBySub.get(); + + if (!foundMember.getProvider().equals(MemberProvider.APPLE)) { + return new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); + } + + if (!foundMember.getEmail().equals(email)) { + return new MemberHandler(FAILED_TO_LOGIN); + } + } + + // 둘 다 찾아지지 않는 경우 - 회원가입이 필요 + return new MemberHandler(MEMBER_NOT_FOUND); + } +} diff --git a/src/main/java/umc/th/juinjang/service/withdraw/WithdrawService.java b/src/main/java/umc/th/juinjang/api/auth/service/WithdrawService.java similarity index 72% rename from src/main/java/umc/th/juinjang/service/withdraw/WithdrawService.java rename to src/main/java/umc/th/juinjang/api/auth/service/WithdrawService.java index d39a1514..9039ece2 100644 --- a/src/main/java/umc/th/juinjang/service/withdraw/WithdrawService.java +++ b/src/main/java/umc/th/juinjang/api/auth/service/WithdrawService.java @@ -1,14 +1,14 @@ -package umc.th.juinjang.service.withdraw; +package umc.th.juinjang.api.auth.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.model.entity.Withdraw; -import umc.th.juinjang.model.entity.enums.WithdrawReason; -import umc.th.juinjang.repository.withdraw.WithdrawRepository; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.domain.withdraw.model.Withdraw; +import umc.th.juinjang.domain.withdraw.model.WithdrawReason; +import umc.th.juinjang.domain.withdraw.repository.WithdrawRepository; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/LoginResponseDto.java b/src/main/java/umc/th/juinjang/api/auth/service/response/LoginResponseDto.java similarity index 90% rename from src/main/java/umc/th/juinjang/model/dto/auth/LoginResponseDto.java rename to src/main/java/umc/th/juinjang/api/auth/service/response/LoginResponseDto.java index 6b33ad38..b31340ba 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/LoginResponseDto.java +++ b/src/main/java/umc/th/juinjang/api/auth/service/response/LoginResponseDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth; +package umc.th.juinjang.api.auth.service.response; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/LoginResponseVersion2Dto.java b/src/main/java/umc/th/juinjang/api/auth/service/response/LoginResponseVersion2Dto.java similarity index 92% rename from src/main/java/umc/th/juinjang/model/dto/auth/LoginResponseVersion2Dto.java rename to src/main/java/umc/th/juinjang/api/auth/service/response/LoginResponseVersion2Dto.java index 4d31e5d1..e269e9ec 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/LoginResponseVersion2Dto.java +++ b/src/main/java/umc/th/juinjang/api/auth/service/response/LoginResponseVersion2Dto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth; +package umc.th.juinjang.api.auth.service.response; import lombok.AccessLevel; import lombok.Builder; diff --git a/src/main/java/umc/th/juinjang/controller/ChecklistController.java b/src/main/java/umc/th/juinjang/api/checklist/controller/ChecklistController.java similarity index 80% rename from src/main/java/umc/th/juinjang/controller/ChecklistController.java rename to src/main/java/umc/th/juinjang/api/checklist/controller/ChecklistController.java index 06b2d54a..1944b6ad 100644 --- a/src/main/java/umc/th/juinjang/controller/ChecklistController.java +++ b/src/main/java/umc/th/juinjang/api/checklist/controller/ChecklistController.java @@ -1,13 +1,16 @@ -package umc.th.juinjang.controller; +package umc.th.juinjang.api.checklist.controller; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.model.dto.checklist.*; -import umc.th.juinjang.service.checklist.ChecklistCommandService; -import umc.th.juinjang.service.checklist.ChecklistQueryService; +import umc.th.juinjang.api.checklist.controller.request.ChecklistAnswerRequestDTO; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerAndReportResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportResponseDTO; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.checklist.service.ChecklistCommandService; +import umc.th.juinjang.api.checklist.service.ChecklistQueryService; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/api/checklist/controller/ChecklistControllerV2.java b/src/main/java/umc/th/juinjang/api/checklist/controller/ChecklistControllerV2.java new file mode 100644 index 00000000..a20bf0cd --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/controller/ChecklistControllerV2.java @@ -0,0 +1,58 @@ +package umc.th.juinjang.api.checklist.controller; + +import java.util.List; + +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.checklist.controller.request.ChecklistAnswerRequestDTO; +import umc.th.juinjang.api.checklist.service.ChecklistCommandServiceV2; +import umc.th.juinjang.api.checklist.service.ChecklistQueryServiceV2; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerAndReportResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportWithLimjangResponseDTO; +import umc.th.juinjang.api.dto.ApiResponse; + +@RestController +@RequestMapping("/api/v2") +@RequiredArgsConstructor +@Validated +public class ChecklistControllerV2 { + + private final ChecklistQueryServiceV2 checklistQueryService; + private final ChecklistCommandServiceV2 checklistCommandService; + + @CrossOrigin + @Operation(summary = "체크리스트 답변 조회") + @GetMapping("/checklist/{limjangId}") + public ApiResponse> getChecklistAnswer( + @PathVariable(name = "limjangId") Long noteId) { + return ApiResponse.onSuccess(checklistQueryService.getChecklistAnswerListByLimjang(noteId)); + } + + @CrossOrigin + @Operation(summary = "리포트 조회 V2") + @GetMapping("/report/{noteId}") + public ApiResponse getReport( + @PathVariable(name = "noteId") Long noteId) { + return ApiResponse.onSuccess(checklistQueryService.getReportByNoteId(noteId)); + } + + @CrossOrigin + @Operation(summary = "체크리스트 답변 생성/수정") + @PostMapping("/checklist/{limjangId}") + public ApiResponse postChecklistAnswer( + @PathVariable(name = "limjangId") Long limjangId, + @RequestBody List answerDtos) { + return ApiResponse.onSuccess(checklistCommandService.saveChecklistAnswerList(limjangId, answerDtos)); + } + +} diff --git a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerRequestDTO.java b/src/main/java/umc/th/juinjang/api/checklist/controller/request/ChecklistAnswerRequestDTO.java similarity index 81% rename from src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerRequestDTO.java rename to src/main/java/umc/th/juinjang/api/checklist/controller/request/ChecklistAnswerRequestDTO.java index c01f52f3..d0bb4763 100644 --- a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerRequestDTO.java +++ b/src/main/java/umc/th/juinjang/api/checklist/controller/request/ChecklistAnswerRequestDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.checklist; +package umc.th.juinjang.api.checklist.controller.request; import lombok.*; diff --git a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistQuestionDTO.java b/src/main/java/umc/th/juinjang/api/checklist/controller/request/ChecklistQuestionDTO.java similarity index 94% rename from src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistQuestionDTO.java rename to src/main/java/umc/th/juinjang/api/checklist/controller/request/ChecklistQuestionDTO.java index 0cb09dcf..e44ad620 100644 --- a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistQuestionDTO.java +++ b/src/main/java/umc/th/juinjang/api/checklist/controller/request/ChecklistQuestionDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.checklist; +package umc.th.juinjang.api.checklist.controller.request; import lombok.*; diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistAnswerFinder.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistAnswerFinder.java new file mode 100644 index 00000000..5ec9b94b --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistAnswerFinder.java @@ -0,0 +1,37 @@ +package umc.th.juinjang.api.checklist.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.checklist.repository.ChecklistAnswerRepository; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; + +@Component +@RequiredArgsConstructor +public class ChecklistAnswerFinder { + + private final ChecklistAnswerRepository checklistAnswerRepository; + private final LimjangRepository limjangRepository; + + public List findByLimjangId(Long noteId) { + Limjang limjang = limjangRepository.findByLimjangIdAndDeletedIsFalse(noteId) + .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + + List answerList = checklistAnswerRepository.findChecklistAnswerByLimjangId(limjang); + return ChecklistAnswerResponseDTO.AnswerDto.fromEntityList(answerList); + } + + public List findEntitiesByLimjangId(Long limjangId) { + Limjang limjang = limjangRepository.findByLimjangIdAndDeletedIsFalse(limjangId) + .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + + return checklistAnswerRepository.findChecklistAnswerByLimjangId(limjang); + } +} diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandService.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandService.java new file mode 100644 index 00000000..b85e7c87 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandService.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.api.checklist.service; + +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerAndReportResponseDTO; +import umc.th.juinjang.api.checklist.controller.request.ChecklistAnswerRequestDTO; + +import java.util.List; + +public interface ChecklistCommandService { + public ChecklistAnswerAndReportResponseDTO saveChecklistAnswerList(Long limjangId, List answerDtoList); +} diff --git a/src/main/java/umc/th/juinjang/service/checklist/ChecklistCommandServiceImpl.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandServiceImpl.java similarity index 86% rename from src/main/java/umc/th/juinjang/service/checklist/ChecklistCommandServiceImpl.java rename to src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandServiceImpl.java index 3b760313..5d20a8ae 100644 --- a/src/main/java/umc/th/juinjang/service/checklist/ChecklistCommandServiceImpl.java +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandServiceImpl.java @@ -1,25 +1,25 @@ -package umc.th.juinjang.service.checklist; +package umc.th.juinjang.api.checklist.service; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.ChecklistHandler; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.converter.checklist.ChecklistAnswerAndReportConverter; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerAndReportResponseDTO; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerRequestDTO; -import umc.th.juinjang.model.entity.ChecklistAnswer; -import umc.th.juinjang.model.entity.ChecklistQuestionShort; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Report; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionCategory; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionType; -import umc.th.juinjang.repository.checklist.ChecklistAnswerRepository; -import umc.th.juinjang.repository.checklist.ChecklistQuestionRepository; -import umc.th.juinjang.repository.checklist.ReportRepository; -import umc.th.juinjang.repository.limjang.LimjangRepository; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.api.checklist.service.converter.ChecklistAnswerAndReportConverter; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerAndReportResponseDTO; +import umc.th.juinjang.api.checklist.controller.request.ChecklistAnswerRequestDTO; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionShort; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionCategory; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionType; +import umc.th.juinjang.domain.checklist.repository.ChecklistAnswerRepository; +import umc.th.juinjang.domain.checklist.repository.ChecklistQuestionRepository; +import umc.th.juinjang.domain.report.repository.ReportRepository; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; import java.util.List; import java.util.Map; diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandServiceV2.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandServiceV2.java new file mode 100644 index 00000000..842884ff --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistCommandServiceV2.java @@ -0,0 +1,195 @@ +package umc.th.juinjang.api.checklist.service; + +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.checklist.controller.request.ChecklistAnswerRequestDTO; +import umc.th.juinjang.api.checklist.service.converter.ChecklistAnswerAndReportConverter; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerAndReportResponseDTO; +import umc.th.juinjang.api.limjang.service.NoteFinder; +import umc.th.juinjang.api.limjang.service.NoteQueryServiceV2; +import umc.th.juinjang.api.limjang.service.NoteUpdater; +import umc.th.juinjang.api.limjang.service.response.ChecklistConditionResponse; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionCategory; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionShort; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionType; +import umc.th.juinjang.domain.checklist.repository.ChecklistAnswerRepository; +import umc.th.juinjang.domain.checklist.repository.ChecklistQuestionRepository; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; +import umc.th.juinjang.domain.report.repository.ReportRepository; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChecklistCommandServiceV2 { + private final ChecklistAnswerRepository checklistAnswerRepository; + private final ChecklistQuestionRepository checklistQuestionRepository; + private final NoteFinder noteFinder; + private final NoteQueryServiceV2 noteQueryService; + private final ReportRepository reportRepository; + private final NoteUpdater noteUpdater; + + @Transactional + public ChecklistAnswerAndReportResponseDTO saveChecklistAnswerList(Long limjangId, + List answerDtoList) { + Limjang note = noteFinder.getNoteByIdWhereDeletedIsFalse(limjangId); + if (!checklistAnswerRepository.findChecklistAnswerByLimjangId(note).isEmpty()) { + checklistAnswerRepository.deleteAllByLimjangId(note); + checklistAnswerRepository.flush(); + } + + List answerList = createAnswerList(note, answerDtoList); + answerList = checklistAnswerRepository.saveAll(answerList); + + //ChecklistQuestion의 Category로 그룹을 지어줌 + //categorizedAnswers는 ChecklistQuestionCategory를 키로, 해당 카테고리에 해당하는 ChecklistAnswer 리스트를 값으로 가지는 Map + Map> categorizedAnswers = answerList.stream() + .collect(Collectors.groupingBy(answer -> answer.getQuestionId().getCategory())); + + Report report = findOrCreateReport(note); + reportRepository.save(calculateAndSetCategoryRates(report, categorizedAnswers)); + + ChecklistConditionResponse checklistConditionResponse = noteQueryService.checkLimjangChecklistSatisfaction( + note.getLimjangId()); + try { + note.updateSharable(checklistConditionResponse.isTotalSatisfied()); + noteUpdater.save(note); + } catch (Exception e) { + throw new LimjangHandler(ErrorStatus.LIMJANG_UPDATE_FAILED); + } + + return ChecklistAnswerAndReportConverter.toDto(answerList, report, note); + } + + private List createAnswerList(Limjang limjang, + List answerDtoList) { + return answerDtoList.stream() + .map(dto -> { + ChecklistQuestionShort question = checklistQuestionRepository.findById(dto.getQuestionId()) + .orElseThrow(() -> new ChecklistHandler(ErrorStatus.CHECKLIST_NOTFOUND_ERROR)); + + return ChecklistAnswer.builder() + .questionId(question) + .limjangId(limjang) + .answer(dto.getAnswer()) + .build(); + }) + .collect(Collectors.toList()); + } + + private Report findOrCreateReport(Limjang limjang) { + return reportRepository.findByLimjangId(limjang) + .orElseGet(() -> Report.builder() + .limjangId(limjang) + .build()); + } + + private Report calculateAndSetCategoryRates(Report report, + Map> categorizedAnswers) { + float totalRate = 0F; + + //데드라인 : {answer 모음들}, 입지여건 : {answer 모음들}, 공용공간 ~~ + + ChecklistQuestionCategory[] categories = ChecklistQuestionCategory.values(); + int categoryCount = categories.length; + for (ChecklistQuestionCategory category : categories) { + if (category == ChecklistQuestionCategory.DEADLINE) { + categoryCount -= 1; + continue; + } + List answers = categorizedAnswers.get(category); + Float categoryRate = calculateAverage(answers); + + if (categoryRate == null) { + categoryRate = 0F; + } + String keyword = setRandomKeyword(categoryRate); + + System.out.println(categoryRate); + System.out.println(keyword); + if (categoryRate == 0f) { + categoryCount -= 1; + } + switch (category) { + case INDOOR: + report.setIndoorRate(categoryRate); + report.setIndoorKeyword(keyword); + break; + case PUBLIC_SPACE: + report.setPublicSpaceRate(categoryRate); + report.setPublicSpaceKeyword(keyword); + break; + case LOCATION_CONDITION: + report.setLocationConditionsRate(categoryRate); + report.setLocationConditionsKeyword(keyword); + break; + + } + + totalRate += categoryRate; + } + if (categoryCount <= 0) { + report.setTotalRate(totalRate); + } else { + report.setTotalRate(totalRate / categoryCount); + } + return report; + } + + private Float calculateAverage(List answers) { + if (answers == null || answers.isEmpty()) { + return 0f; + } + Float total = 0f; + int count = 0; + for (ChecklistAnswer answer : answers) { + if (answer.getQuestionId().getAnswerType() == ChecklistQuestionType.SCORE) { + System.out.println( + "questionId : " + answer.getQuestionId().getQuestionId() + " answerType : " + answer.getQuestionId() + .getAnswerType()); + total += Float.parseFloat(answer.getAnswer()); + count++; + } + } + if (count == 0) { + return 0f; + } + return total / count; + } + + public String setRandomKeyword(Float rate) { + + String[] oneToTwo = {"불안한", "불안정한", "불쾌한"}; + String[] twoToThree = {"평균적인", "보통의", "나쁘지 않은"}; + String[] threeToFour = {"좋은", "좋은 편인", "훌륭한", "쾌적한"}; + String[] fourToFive = {"최상의", "최고의", "상당히 좋은", "상당히 쾌적한"}; + String defaultKeyword = "아직 미평가된"; + + Random random = new Random(); + + if (rate >= 1 && rate < 2) { + return oneToTwo[random.nextInt(oneToTwo.length)]; + } else if (rate >= 2 && rate < 3) { + return twoToThree[random.nextInt(twoToThree.length)]; + } else if (rate >= 3 && rate < 4) { + return threeToFour[random.nextInt(threeToFour.length)]; + } else if (rate >= 4 && rate <= 5) { + return fourToFive[random.nextInt(fourToFive.length)]; + } + + return defaultKeyword; + + } +} diff --git a/src/main/java/umc/th/juinjang/service/checklist/ChecklistQueryService.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryService.java similarity index 68% rename from src/main/java/umc/th/juinjang/service/checklist/ChecklistQueryService.java rename to src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryService.java index e48a225d..3e5d3f22 100644 --- a/src/main/java/umc/th/juinjang/service/checklist/ChecklistQueryService.java +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryService.java @@ -1,7 +1,7 @@ -package umc.th.juinjang.service.checklist; +package umc.th.juinjang.api.checklist.service; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerResponseDTO; -import umc.th.juinjang.model.dto.checklist.ReportResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportResponseDTO; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/service/checklist/ChecklistQueryServiceImpl.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryServiceImpl.java similarity index 82% rename from src/main/java/umc/th/juinjang/service/checklist/ChecklistQueryServiceImpl.java rename to src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryServiceImpl.java index 984c405f..bdf82e33 100644 --- a/src/main/java/umc/th/juinjang/service/checklist/ChecklistQueryServiceImpl.java +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryServiceImpl.java @@ -1,21 +1,21 @@ -package umc.th.juinjang.service.checklist; +package umc.th.juinjang.api.checklist.service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.ChecklistHandler; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.converter.checklist.ChecklistAnswerAndReportConverter; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerResponseDTO; -import umc.th.juinjang.model.dto.checklist.ReportResponseDTO; -import umc.th.juinjang.model.entity.ChecklistAnswer; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Report; -import umc.th.juinjang.repository.checklist.ChecklistAnswerRepository; -import umc.th.juinjang.repository.checklist.ChecklistQuestionRepository; -import umc.th.juinjang.repository.checklist.ReportRepository; -import umc.th.juinjang.repository.limjang.LimjangRepository; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.api.checklist.service.converter.ChecklistAnswerAndReportConverter; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportResponseDTO; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; +import umc.th.juinjang.domain.checklist.repository.ChecklistAnswerRepository; +import umc.th.juinjang.domain.checklist.repository.ChecklistQuestionRepository; +import umc.th.juinjang.domain.report.repository.ReportRepository; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryServiceV2.java b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryServiceV2.java new file mode 100644 index 00000000..befeeebf --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ChecklistQueryServiceV2.java @@ -0,0 +1,35 @@ +package umc.th.juinjang.api.checklist.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportGetResponse; +import umc.th.juinjang.api.checklist.service.response.ReportWithLimjangResponseDTO; +import umc.th.juinjang.api.limjang.service.NoteFinder; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailGetResponse; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChecklistQueryServiceV2 { + + private final ChecklistAnswerFinder checklistAnswerFinder; + private final ReportFinder reportFinder; + private final NoteFinder noteFinder; + + public List getChecklistAnswerListByLimjang(Long noteId) { + return checklistAnswerFinder.findByLimjangId(noteId); + } + + public ReportWithLimjangResponseDTO getReportByNoteId(Long noteId) { + Limjang note = noteFinder.getNoteByIdWithAddressAndNotePriceWhereDeletedIsFalse(noteId); + Report report = reportFinder.findReportByNote(note); + return new ReportWithLimjangResponseDTO(ReportGetResponse.of(report), LimjangDetailGetResponse.of(note)); + } +} diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/ReportFinder.java b/src/main/java/umc/th/juinjang/api/checklist/service/ReportFinder.java new file mode 100644 index 00000000..9ac64997 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/ReportFinder.java @@ -0,0 +1,22 @@ +package umc.th.juinjang.api.checklist.service; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Component; + +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; +import umc.th.juinjang.domain.report.repository.ReportRepository; + +@Component +@RequiredArgsConstructor +public class ReportFinder { + private final ReportRepository reportRepository; + + public Report findReportByNote(Limjang limjang) { + return reportRepository.findByLimjangId(limjang) + .orElseThrow(() -> new ChecklistHandler(ErrorStatus.REPORT_NOTFOUND_ERROR)); + } +} diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistAnswerAndReportConverter.java b/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistAnswerAndReportConverter.java new file mode 100644 index 00000000..d5cbeb74 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistAnswerAndReportConverter.java @@ -0,0 +1,61 @@ +package umc.th.juinjang.api.checklist.service.converter; + +import java.util.List; +import java.util.stream.Collectors; + +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerAndReportResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportResponseDTO; +import umc.th.juinjang.api.limjang.service.converter.LimjangDetailConverter; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailResponseDTO; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; + +public class ChecklistAnswerAndReportConverter { + + public static ChecklistAnswerAndReportResponseDTO toDto(List answerList, Report report, + Limjang limjang) { + List answerDtoList = answerList.stream() + .map(answer -> new ChecklistAnswerResponseDTO.AnswerDto( + answer.getAnswerId(), + answer.getQuestionId().getQuestionId(), + answer.getQuestionId().getCategory(), + answer.getLimjangId().getLimjangId(), + answer.getAnswer(), + answer.getQuestionId().getAnswerType())) + .collect(Collectors.toList()); + + ReportResponseDTO.ReportDTO reportDto = new ReportResponseDTO.ReportDTO( + report.getReportId(), + report.getIndoorKeyword(), + report.getPublicSpaceKeyword(), + report.getLocationConditionsKeyword(), + report.getIndoorRate(), + report.getPublicSpaceRate(), + report.getLocationConditionsRate(), + report.getTotalRate()); + + LimjangDetailResponseDTO.DetailDto detailDto = LimjangDetailConverter.toDetail(limjang, + limjang.getLimjangPrice()); + + return new ChecklistAnswerAndReportResponseDTO(answerDtoList, new ReportResponseDTO(reportDto, detailDto)); + } + + public static ReportResponseDTO toReportDto(Report report, Limjang limjang) { + ReportResponseDTO.ReportDTO reportDTO = ReportResponseDTO.ReportDTO.builder() + .reportId(report.getReportId()) + .indoorKeyWord(report.getIndoorKeyword()) + .publicSpaceKeyWord(report.getPublicSpaceKeyword()) + .locationConditionsWord(report.getLocationConditionsKeyword()) + .indoorRate(report.getIndoorRate()) + .publicSpaceRate(report.getPublicSpaceRate()) + .locationConditionsRate(report.getLocationConditionsRate()) + .totalRate(report.getTotalRate()) + .build(); + LimjangDetailResponseDTO.DetailDto detailDto = LimjangDetailConverter.toDetail(limjang, + limjang.getLimjangPrice()); + return new ReportResponseDTO(reportDTO, detailDto); + } + +} diff --git a/src/main/java/umc/th/juinjang/converter/checklist/ChecklistAnswerConverter.java b/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistAnswerConverter.java similarity index 76% rename from src/main/java/umc/th/juinjang/converter/checklist/ChecklistAnswerConverter.java rename to src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistAnswerConverter.java index d0d6096b..1fb1f389 100644 --- a/src/main/java/umc/th/juinjang/converter/checklist/ChecklistAnswerConverter.java +++ b/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistAnswerConverter.java @@ -1,8 +1,7 @@ -package umc.th.juinjang.converter.checklist; +package umc.th.juinjang.api.checklist.service.converter; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerRequestDTO; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerResponseDTO; -import umc.th.juinjang.model.entity.ChecklistAnswer; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/umc/th/juinjang/converter/checklist/ChecklistQuestionConverter.java b/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistQuestionConverter.java similarity index 88% rename from src/main/java/umc/th/juinjang/converter/checklist/ChecklistQuestionConverter.java rename to src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistQuestionConverter.java index 10b9f7a4..caae2d76 100644 --- a/src/main/java/umc/th/juinjang/converter/checklist/ChecklistQuestionConverter.java +++ b/src/main/java/umc/th/juinjang/api/checklist/service/converter/ChecklistQuestionConverter.java @@ -1,13 +1,6 @@ -package umc.th.juinjang.converter.checklist; +package umc.th.juinjang.api.checklist.service.converter; -import umc.th.juinjang.model.dto.checklist.ChecklistQuestionDTO; -import umc.th.juinjang.model.entity.ChecklistQuestionShort; - - -import java.util.List; -import java.util.stream.Collectors; - public class ChecklistQuestionConverter { // public static List toChecklistQuestionListDTO(List checklistQuestionShorts) { // return checklistQuestionShorts.stream() diff --git a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerAndReportResponseDTO.java b/src/main/java/umc/th/juinjang/api/checklist/service/response/ChecklistAnswerAndReportResponseDTO.java similarity index 81% rename from src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerAndReportResponseDTO.java rename to src/main/java/umc/th/juinjang/api/checklist/service/response/ChecklistAnswerAndReportResponseDTO.java index d9b5cddd..e8f599f6 100644 --- a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerAndReportResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/checklist/service/response/ChecklistAnswerAndReportResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.checklist; +package umc.th.juinjang.api.checklist.service.response; import lombok.*; diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/response/ChecklistAnswerResponseDTO.java b/src/main/java/umc/th/juinjang/api/checklist/service/response/ChecklistAnswerResponseDTO.java new file mode 100644 index 00000000..8de9ab68 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/response/ChecklistAnswerResponseDTO.java @@ -0,0 +1,43 @@ +package umc.th.juinjang.api.checklist.service.response; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionCategory; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionType; + +public class ChecklistAnswerResponseDTO { + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class AnswerDto { + private Long answerId; + private Long questionId; + private ChecklistQuestionCategory category; + private Long limjangId; + private String answer; + private ChecklistQuestionType answerType; + + public static AnswerDto fromEntity(ChecklistAnswer entity) { + return AnswerDto.builder() + .answerId(entity.getAnswerId()) + .questionId(entity.getQuestionId().getQuestionId()) + .category(entity.getQuestionId().getCategory()) + .limjangId(entity.getLimjangId().getLimjangId()) + .answer(entity.getAnswer()) + .answerType(entity.getQuestionId().getAnswerType()) + .build(); + } + + public static List fromEntityList(List entities) { + return entities.stream() + .map(AnswerDto::fromEntity) + .toList(); // Java 17+ 지원 + } + } +} diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportGetResponse.java b/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportGetResponse.java new file mode 100644 index 00000000..57e5caa0 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportGetResponse.java @@ -0,0 +1,27 @@ +package umc.th.juinjang.api.checklist.service.response; + +import umc.th.juinjang.domain.report.model.Report; + +public record ReportGetResponse( + Long reportId, + String indoorKeyWord, + String publicSpaceKeyWord, + String locationConditionsWord, + Float indoorRate, + Float publicSpaceRate, + Float locationConditionsRate, + Float totalRate +) { + public static ReportGetResponse of(Report report) { + return new ReportGetResponse( + report.getReportId(), + report.getIndoorKeyword(), + report.getPublicSpaceKeyword(), + report.getLocationConditionsKeyword(), + report.getIndoorRate(), + report.getPublicSpaceRate(), + report.getLocationConditionsRate(), + report.getTotalRate() + ); + } +} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportResponseDTO.java b/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportResponseDTO.java new file mode 100644 index 00000000..efd090c5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportResponseDTO.java @@ -0,0 +1,32 @@ +package umc.th.juinjang.api.checklist.service.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailResponseDTO; + +@AllArgsConstructor +@Getter +@Setter +public class ReportResponseDTO { + private ReportDTO reportDTO; + private LimjangDetailResponseDTO.DetailDto limjangDto; + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ReportDTO { + private Long reportId; + private String indoorKeyWord; + private String publicSpaceKeyWord; + private String locationConditionsWord; + private Float indoorRate; + private Float publicSpaceRate; + private Float locationConditionsRate; + private Float totalRate; + } +} diff --git a/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportWithLimjangResponseDTO.java b/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportWithLimjangResponseDTO.java new file mode 100644 index 00000000..d33c390a --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/checklist/service/response/ReportWithLimjangResponseDTO.java @@ -0,0 +1,14 @@ +package umc.th.juinjang.api.checklist.service.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailGetResponse; + +@Getter +@Setter +@AllArgsConstructor +public class ReportWithLimjangResponseDTO { + private ReportGetResponse reportDTO; + private LimjangDetailGetResponse limjangDto; +} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/config/SwaggerConfig.java b/src/main/java/umc/th/juinjang/api/config/SwaggerConfig.java similarity index 97% rename from src/main/java/umc/th/juinjang/config/SwaggerConfig.java rename to src/main/java/umc/th/juinjang/api/config/SwaggerConfig.java index ff0f70d6..34157c05 100644 --- a/src/main/java/umc/th/juinjang/config/SwaggerConfig.java +++ b/src/main/java/umc/th/juinjang/api/config/SwaggerConfig.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.config; +package umc.th.juinjang.api.config; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; diff --git a/src/main/java/umc/th/juinjang/apiPayload/ApiResponse.java b/src/main/java/umc/th/juinjang/api/dto/ApiResponse.java similarity index 87% rename from src/main/java/umc/th/juinjang/apiPayload/ApiResponse.java rename to src/main/java/umc/th/juinjang/api/dto/ApiResponse.java index fe905166..8842b041 100644 --- a/src/main/java/umc/th/juinjang/apiPayload/ApiResponse.java +++ b/src/main/java/umc/th/juinjang/api/dto/ApiResponse.java @@ -1,13 +1,13 @@ -package umc.th.juinjang.apiPayload; +package umc.th.juinjang.api.dto; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import lombok.AllArgsConstructor; import lombok.Getter; -import umc.th.juinjang.apiPayload.code.BaseCode; -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.code.status.SuccessStatus; +import umc.th.juinjang.common.code.BaseCode; +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.code.status.SuccessStatus; @Getter @AllArgsConstructor diff --git a/src/main/java/umc/th/juinjang/api/flag/controller/FlagController.java b/src/main/java/umc/th/juinjang/api/flag/controller/FlagController.java new file mode 100644 index 00000000..bdf71d52 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/flag/controller/FlagController.java @@ -0,0 +1,31 @@ +package umc.th.juinjang.api.flag.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.flag.controller.request.FlagSharedNotePostRequest; +import umc.th.juinjang.api.flag.service.FlagSharedNoteCommandService; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2") +@RequiredArgsConstructor +public class FlagController { + + private final FlagSharedNoteCommandService flagSharedNoteCommandService; + + @Operation(summary = "노트 신고하기 API") + @PostMapping("/reports/shared-note") + public ApiResponse findUsersSharedNotes(@AuthenticationPrincipal Member member, + @RequestBody FlagSharedNotePostRequest flagSharedNotePostRequest + ) { + flagSharedNoteCommandService.createSharedNoteFlag(member, flagSharedNotePostRequest); + return ApiResponse.onSuccess(null); + } +} diff --git a/src/main/java/umc/th/juinjang/api/flag/controller/request/FlagSharedNotePostRequest.java b/src/main/java/umc/th/juinjang/api/flag/controller/request/FlagSharedNotePostRequest.java new file mode 100644 index 00000000..94537a0d --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/flag/controller/request/FlagSharedNotePostRequest.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.api.flag.controller.request; + +import umc.th.juinjang.domain.flag.model.FlagSharedNoteType; + +public record FlagSharedNotePostRequest( + Long sharedNoteId, + FlagSharedNoteType type +) { +} diff --git a/src/main/java/umc/th/juinjang/api/flag/service/FlagSharedNoteCommandService.java b/src/main/java/umc/th/juinjang/api/flag/service/FlagSharedNoteCommandService.java new file mode 100644 index 00000000..189f9b54 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/flag/service/FlagSharedNoteCommandService.java @@ -0,0 +1,43 @@ +package umc.th.juinjang.api.flag.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.flag.controller.request.FlagSharedNotePostRequest; +import umc.th.juinjang.api.note.shared.service.SharedNoteFinder; +import umc.th.juinjang.domain.flag.model.FlagSharedNote; +import umc.th.juinjang.domain.flag.model.FlagSharedNoteType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.event.publisher.FlagSharedNoteEventPublisher; + +@Service +@Slf4j +@RequiredArgsConstructor +public class FlagSharedNoteCommandService { + + private final SharedNoteFinder sharedNoteFinder; + private final FlagSharedNoteUpdater flagSharedNoteUpdater; + private final FlagSharedNoteEventPublisher flagSharedNoteEventPublisher; + + @Transactional + public void createSharedNoteFlag(Member flaggedBy, FlagSharedNotePostRequest flagSharedNotePostRequest) { + SharedNote flaggedsharedNote = sharedNoteFinder.getByIdWhereDeletedAtIsNull( + flagSharedNotePostRequest.sharedNoteId()); + + flagSharedNoteUpdater.save( + createFlagSharedNote(flaggedBy, flagSharedNotePostRequest.type(), flaggedsharedNote)); + flagSharedNoteEventPublisher.publishFlagSharedNoteEvent(flaggedBy.getMemberId(), + flaggedsharedNote.getSharedNoteId(), + flaggedsharedNote.getMember().getMemberId(), + flagSharedNotePostRequest.type()); + } + + private FlagSharedNote createFlagSharedNote(Member flaggedBy, FlagSharedNoteType type, + SharedNote flaggedsharedNote) { + return FlagSharedNote.create(type, flaggedBy.getMemberId(), + flaggedsharedNote.getSharedNoteId()); + } +} diff --git a/src/main/java/umc/th/juinjang/api/flag/service/FlagSharedNoteUpdater.java b/src/main/java/umc/th/juinjang/api/flag/service/FlagSharedNoteUpdater.java new file mode 100644 index 00000000..94e0f846 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/flag/service/FlagSharedNoteUpdater.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.flag.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.flag.model.FlagSharedNote; +import umc.th.juinjang.domain.flag.repository.FlagSharedNoteRepository; + +@Component +@RequiredArgsConstructor +public class FlagSharedNoteUpdater { + + private final FlagSharedNoteRepository flagSharedNoteRepository; + + void save(FlagSharedNote flagSharedNote) { + flagSharedNoteRepository.save(flagSharedNote); + } +} diff --git a/src/main/java/umc/th/juinjang/controller/ImageController.java b/src/main/java/umc/th/juinjang/api/image/controller/ImageController.java similarity index 53% rename from src/main/java/umc/th/juinjang/controller/ImageController.java rename to src/main/java/umc/th/juinjang/api/image/controller/ImageController.java index 39f04a0e..9dcaaa9e 100644 --- a/src/main/java/umc/th/juinjang/controller/ImageController.java +++ b/src/main/java/umc/th/juinjang/api/image/controller/ImageController.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.controller; +package umc.th.juinjang.api.image.controller; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -16,13 +15,12 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.apiPayload.code.status.SuccessStatus; -import umc.th.juinjang.model.dto.image.ImageDeleteRequestDTO; -import umc.th.juinjang.model.dto.image.ImageListResponseDTO; -import umc.th.juinjang.service.image.ImageCommandService; -import umc.th.juinjang.service.image.ImageQueryService; -import umc.th.juinjang.service.limjang.LimjangCommandService; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.common.code.status.SuccessStatus; +import umc.th.juinjang.api.image.controller.request.ImageDeleteRequestDTO; +import umc.th.juinjang.api.image.service.response.ImagesGetResponse; +import umc.th.juinjang.api.image.service.ImageCommandService; +import umc.th.juinjang.api.image.service.ImageQueryService; @RestController @RequestMapping("/api/limjang/image") @@ -30,38 +28,26 @@ @Validated public class ImageController { - private final LimjangCommandService limjangCommandService; private final ImageCommandService imageCommandService; private final ImageQueryService imageQueryService; - @CrossOrigin - @Operation(summary = "사진 생성 API", description = "사진 업로드 api입니다.") - @PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, - produces = MediaType.APPLICATION_JSON_VALUE) - public ApiResponse uploadImages( - @RequestParam(name = "limjangId") Long limjangId, @RequestPart(name = "images") List images) - { - imageCommandService.uploadImages(limjangId ,images); - return ApiResponse.onSuccess(SuccessStatus.IMAGE_UPDATE); - } + @Operation(summary = "사진 생성 API", description = "사진 업로드 api입니다.") + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public ApiResponse uploadImages(@RequestParam(name = "limjangId") Long limjangId, @RequestPart(name = "images") List images) { + imageCommandService.createImages(limjangId, images); + return ApiResponse.onSuccess(SuccessStatus.IMAGE_UPDATE); + } - @CrossOrigin @Operation(summary = "사진 조회 API", description = "사진을 조회하는 api입니다.") - @GetMapping(value = "{limjangId}") - public ApiResponse uploadImages( - @PathVariable(name = "limjangId") @Valid Long limjangId) - { + @GetMapping(value = "/{limjangId}") + public ApiResponse uploadImages(@PathVariable(name = "limjangId") @Valid Long limjangId) { return ApiResponse.onSuccess(imageQueryService.getImageList(limjangId)); } - @CrossOrigin @Operation(summary = "이미지 선택 삭제", description = "이미지 게시글을 여러 개 선택해서 삭제하는 api입니다.") @PostMapping("/delete") - public ApiResponse deleteImage(@RequestBody @Valid ImageDeleteRequestDTO.DeleteDto deleteIds - ){ - + public ApiResponse deleteImage(@RequestBody @Valid ImageDeleteRequestDTO.DeleteDto deleteIds) { imageCommandService.deleteImages(deleteIds); return ApiResponse.onSuccess(SuccessStatus.IMAGE_DELETE); } - } diff --git a/src/main/java/umc/th/juinjang/model/dto/image/ImageDeleteRequestDTO.java b/src/main/java/umc/th/juinjang/api/image/controller/request/ImageDeleteRequestDTO.java similarity index 80% rename from src/main/java/umc/th/juinjang/model/dto/image/ImageDeleteRequestDTO.java rename to src/main/java/umc/th/juinjang/api/image/controller/request/ImageDeleteRequestDTO.java index 11ce6b93..0a6a2e1b 100644 --- a/src/main/java/umc/th/juinjang/model/dto/image/ImageDeleteRequestDTO.java +++ b/src/main/java/umc/th/juinjang/api/image/controller/request/ImageDeleteRequestDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.image; +package umc.th.juinjang.api.image.controller.request; import jakarta.validation.constraints.NotEmpty; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/model/dto/image/ImageUploadRequestDTO.java b/src/main/java/umc/th/juinjang/api/image/controller/request/ImageUploadRequestDTO.java similarity index 88% rename from src/main/java/umc/th/juinjang/model/dto/image/ImageUploadRequestDTO.java rename to src/main/java/umc/th/juinjang/api/image/controller/request/ImageUploadRequestDTO.java index 28da1cd6..425cf973 100644 --- a/src/main/java/umc/th/juinjang/model/dto/image/ImageUploadRequestDTO.java +++ b/src/main/java/umc/th/juinjang/api/image/controller/request/ImageUploadRequestDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.image; +package umc.th.juinjang.api.image.controller.request; import java.util.List; import lombok.AllArgsConstructor; diff --git a/src/main/java/umc/th/juinjang/service/image/ImageCommandService.java b/src/main/java/umc/th/juinjang/api/image/service/ImageCommandService.java similarity index 50% rename from src/main/java/umc/th/juinjang/service/image/ImageCommandService.java rename to src/main/java/umc/th/juinjang/api/image/service/ImageCommandService.java index f0e6f894..0c0c7156 100644 --- a/src/main/java/umc/th/juinjang/service/image/ImageCommandService.java +++ b/src/main/java/umc/th/juinjang/api/image/service/ImageCommandService.java @@ -1,11 +1,11 @@ -package umc.th.juinjang.service.image; +package umc.th.juinjang.api.image.service; import java.util.List; import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.model.dto.image.ImageDeleteRequestDTO; +import umc.th.juinjang.api.image.controller.request.ImageDeleteRequestDTO; public interface ImageCommandService { - void uploadImages(Long limjangId, List images); + void createImages(long limjangId, List images); void deleteImages(ImageDeleteRequestDTO.DeleteDto ids); diff --git a/src/main/java/umc/th/juinjang/api/image/service/ImageCommandServiceImpl.java b/src/main/java/umc/th/juinjang/api/image/service/ImageCommandServiceImpl.java new file mode 100644 index 00000000..fb87f9bc --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/image/service/ImageCommandServiceImpl.java @@ -0,0 +1,49 @@ +package umc.th.juinjang.api.image.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.api.image.controller.request.ImageDeleteRequestDTO; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.image.repository.ImageRepository; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.external.s3.S3Service; + +@Service +@RequiredArgsConstructor +public class ImageCommandServiceImpl implements ImageCommandService { + + private final ImageRepository imageRepository; + private final LimjangRepository limjangRepository; + private final S3Service s3Service; + private final String DIR_NAME = "image"; + + @Override + @Transactional + public void createImages(final long limjangId, final List files) { + Limjang limjang = getLimjangById(limjangId); + for (MultipartFile file : files) { + String imageUrl = s3Service.upload(file, DIR_NAME); + imageRepository.save(Image.create(imageUrl, limjang)); + } + } + + private Limjang getLimjangById(final long limjangId) { + return limjangRepository.findByLimjangIdAndDeletedIsFalse(limjangId).orElseThrow(()-> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + } + + @Override + @Transactional + public void deleteImages(final ImageDeleteRequestDTO.DeleteDto ids) { + List images = imageRepository.findAllById(ids.getImageIdList()); + for (Image image : images) { + s3Service.deleteFile(image.getImageUrl()); + imageRepository.deleteById(image.getImageId()); + } + } +} diff --git a/src/main/java/umc/th/juinjang/api/image/service/ImageFinder.java b/src/main/java/umc/th/juinjang/api/image/service/ImageFinder.java new file mode 100644 index 00000000..1056553f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/image/service/ImageFinder.java @@ -0,0 +1,21 @@ +package umc.th.juinjang.api.image.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.image.repository.ImageRepository; +import umc.th.juinjang.domain.limjang.model.Limjang; + +@Component +@RequiredArgsConstructor +public class ImageFinder { + + private final ImageRepository imageRepository; + + public List findAllFirstCreatedImagePerNote(List notes) { + return imageRepository.findAllFirstCreatedImagePerNote(notes); + } +} diff --git a/src/main/java/umc/th/juinjang/api/image/service/ImageQueryService.java b/src/main/java/umc/th/juinjang/api/image/service/ImageQueryService.java new file mode 100644 index 00000000..fdfa0169 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/image/service/ImageQueryService.java @@ -0,0 +1,7 @@ +package umc.th.juinjang.api.image.service; + +import umc.th.juinjang.api.image.service.response.ImagesGetResponse; + +public interface ImageQueryService { + ImagesGetResponse getImageList(long limjangId); +} diff --git a/src/main/java/umc/th/juinjang/api/image/service/ImageQueryServiceImpl.java b/src/main/java/umc/th/juinjang/api/image/service/ImageQueryServiceImpl.java new file mode 100644 index 00000000..874fabe7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/image/service/ImageQueryServiceImpl.java @@ -0,0 +1,35 @@ +package umc.th.juinjang.api.image.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.api.image.service.response.ImagesGetResponse; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.image.repository.ImageRepository; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ImageQueryServiceImpl implements ImageQueryService { + + private final ImageRepository imageRepository; + private final LimjangRepository limjangRepository; + + @Override + @Transactional(readOnly = true) + public ImagesGetResponse getImageList(final long limjangId) { + Limjang limjang = getLimjang(limjangId); + List images = imageRepository.findImagesByLimjangId(limjang); + return ImagesGetResponse.of(images); + } + + private Limjang getLimjang(final long limjangId) { + return limjangRepository.findByLimjangIdAndDeletedIsFalse(limjangId).orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + } +} diff --git a/src/main/java/umc/th/juinjang/model/dto/image/ImageListResponseDTO.java b/src/main/java/umc/th/juinjang/api/image/service/response/ImageListResponseDTO.java similarity index 89% rename from src/main/java/umc/th/juinjang/model/dto/image/ImageListResponseDTO.java rename to src/main/java/umc/th/juinjang/api/image/service/response/ImageListResponseDTO.java index 38040398..63ac8e92 100644 --- a/src/main/java/umc/th/juinjang/model/dto/image/ImageListResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/image/service/response/ImageListResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.image; +package umc.th.juinjang.api.image.service.response; import java.util.List; import lombok.AllArgsConstructor; diff --git a/src/main/java/umc/th/juinjang/model/dto/image/ImageUploadResponseDTO.java b/src/main/java/umc/th/juinjang/api/image/service/response/ImageUploadResponseDTO.java similarity index 86% rename from src/main/java/umc/th/juinjang/model/dto/image/ImageUploadResponseDTO.java rename to src/main/java/umc/th/juinjang/api/image/service/response/ImageUploadResponseDTO.java index b4ba5acf..ac9697f0 100644 --- a/src/main/java/umc/th/juinjang/model/dto/image/ImageUploadResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/image/service/response/ImageUploadResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.image; +package umc.th.juinjang.api.image.service.response; import java.time.LocalDateTime; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/api/image/service/response/ImagesGetResponse.java b/src/main/java/umc/th/juinjang/api/image/service/response/ImagesGetResponse.java new file mode 100644 index 00000000..2b9b45a3 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/image/service/response/ImagesGetResponse.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.image.service.response; + +import java.util.List; +import umc.th.juinjang.domain.image.model.Image; + +public record ImagesGetResponse (List images) { + record ImageResponse(Long imageId, String imageUrl) { + static ImageResponse of(Long imageId, String imageUrl) { + return new ImageResponse(imageId, imageUrl); + } + } + + public static ImagesGetResponse of(List images) { + return new ImagesGetResponse(images.stream().map(it -> ImageResponse.of(it.getImageId(), it.getImageUrl())).toList()); + } +} + + diff --git a/src/main/java/umc/th/juinjang/controller/LimjangController.java b/src/main/java/umc/th/juinjang/api/limjang/controller/LimjangController.java similarity index 80% rename from src/main/java/umc/th/juinjang/controller/LimjangController.java rename to src/main/java/umc/th/juinjang/api/limjang/controller/LimjangController.java index 4cd407df..636f99d1 100644 --- a/src/main/java/umc/th/juinjang/controller/LimjangController.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/LimjangController.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.controller; +package umc.th.juinjang.api.limjang.controller; import io.swagger.v3.oas.annotations.Operation; @@ -16,21 +16,21 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.apiPayload.code.status.SuccessStatus; -import umc.th.juinjang.model.dto.limjang.enums.LimjangSortOptions; -import umc.th.juinjang.model.dto.limjang.request.LimjangPatchRequest; -import umc.th.juinjang.model.dto.limjang.request.LimjangPostRequest; -import umc.th.juinjang.model.dto.limjang.request.LimjangsDeleteRequest; -import umc.th.juinjang.model.dto.limjang.response.LimjangDetailGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangPostResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsGetByKeywordResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetVersion2Response; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.service.limjang.LimjangCommandService; -import umc.th.juinjang.service.limjang.LimjangQueryService; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.common.code.status.SuccessStatus; +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.api.limjang.controller.request.LimjangPatchRequest; +import umc.th.juinjang.api.limjang.controller.request.LimjangPostRequest; +import umc.th.juinjang.api.limjang.controller.request.LimjangsDeleteRequest; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangPostResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsGetByKeywordResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetVersion2Response; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.api.limjang.service.LimjangCommandService; +import umc.th.juinjang.api.limjang.service.LimjangQueryService; @RestController @RequestMapping("/api/limjang") diff --git a/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java b/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java new file mode 100644 index 00000000..8120692a --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/NoteControllerV2.java @@ -0,0 +1,82 @@ +package umc.th.juinjang.api.limjang.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.api.limjang.controller.request.NotePatchRequest; +import umc.th.juinjang.api.limjang.controller.request.NotePostRequest; +import umc.th.juinjang.api.limjang.service.NoteCommandServiceV2; +import umc.th.juinjang.api.limjang.service.NoteQueryServiceV2; +import umc.th.juinjang.api.limjang.service.response.ChecklistConditionResponse; +import umc.th.juinjang.api.limjang.service.response.NotePostResponse; +import umc.th.juinjang.api.limjang.service.response.UserNoteGetResponse; +import umc.th.juinjang.api.limjang.service.response.UserNotesGetResponse; +import umc.th.juinjang.api.limjang.service.response.UserNotesShareableGetResponse; +import umc.th.juinjang.common.code.status.SuccessStatus; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2/users") +@RequiredArgsConstructor +public class NoteControllerV2 { + + private final NoteCommandServiceV2 noteCommandService; + private final NoteQueryServiceV2 noteQueryService; + + @Operation(summary = "임장 생성 API V2") + @PostMapping("/notes") + public ApiResponse createNote(@RequestBody @Valid NotePostRequest request, + @AuthenticationPrincipal Member member) { + return ApiResponse.of(SuccessStatus._CREATED, noteCommandService.createNote(request, member)); + } + + @Operation(summary = "마이 노트 조회 API V2") + @GetMapping("/notes") + public ApiResponse findUsersNotes( + @RequestParam("sort") LimjangSortOptions sortOptions, + @RequestParam(value = "keyword", required = false) String keyword, + @AuthenticationPrincipal Member member) { + return ApiResponse.onSuccess(noteQueryService.findUsersNotes(member, sortOptions, keyword)); + } + + @Operation(summary = "임장 수정 API V2") + @PatchMapping("/notes/{noteId}") + public ApiResponse updateNote(@PathVariable(name = "noteId") Long noteId, + @RequestBody @Valid NotePatchRequest request, + @AuthenticationPrincipal Member member) { + noteCommandService.updateNote(noteId, request); + return ApiResponse.onSuccess(null); + } + + @Operation(summary = "임장 노트 출력 - 공유하기 선택 화면 API") + @GetMapping("/notes/shareable") + public ApiResponse findNotesShareable(@AuthenticationPrincipal Member member) { + return ApiResponse.onSuccess(noteQueryService.findNotesShareable(member)); + } + + @Operation(summary = "임장 노트 상세보기(내 임장) 화면 API") + @GetMapping("/notes/{noteId}") + public ApiResponse findNote(@AuthenticationPrincipal Member member, + @PathVariable("noteId") Long noteId) { + return ApiResponse.onSuccess(noteQueryService.findNote(noteId)); + } + + @GetMapping("/notes/{noteId}/checklist-condition") + public ApiResponse checkChecklistCondition( + @PathVariable Long noteId + ) { + return ApiResponse.onSuccess(noteQueryService.checkLimjangChecklistSatisfaction(noteId)); + } +} diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/enums/LimjangSortOptions.java b/src/main/java/umc/th/juinjang/api/limjang/controller/parameter/LimjangSortOptions.java similarity index 81% rename from src/main/java/umc/th/juinjang/model/dto/limjang/enums/LimjangSortOptions.java rename to src/main/java/umc/th/juinjang/api/limjang/controller/parameter/LimjangSortOptions.java index bc356c38..1dca3dc3 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/enums/LimjangSortOptions.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/parameter/LimjangSortOptions.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.enums; +package umc.th.juinjang.api.limjang.controller.parameter; public enum LimjangSortOptions { UPDATED("UPDATED"), diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangPatchRequest.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangPatchRequest.java similarity index 85% rename from src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangPatchRequest.java rename to src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangPatchRequest.java index 793b0197..d7a4a655 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangPatchRequest.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangPatchRequest.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.request; +package umc.th.juinjang.api.limjang.controller.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangPostRequest.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangPostRequest.java similarity index 73% rename from src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangPostRequest.java rename to src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangPostRequest.java index 8b32369e..fdb439bc 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangPostRequest.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangPostRequest.java @@ -1,13 +1,13 @@ -package umc.th.juinjang.model.dto.limjang.request; +package umc.th.juinjang.api.limjang.controller.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.util.List; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.enums.LimjangPriceType; -import umc.th.juinjang.model.entity.enums.LimjangPropertyType; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; public record LimjangPostRequest( @NotNull diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangUpdateRequestDTO.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangUpdateRequestDTO.java similarity index 71% rename from src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangUpdateRequestDTO.java rename to src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangUpdateRequestDTO.java index d0a0c444..252e8bad 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangUpdateRequestDTO.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangUpdateRequestDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.request; +package umc.th.juinjang.api.limjang.controller.request; import lombok.Getter; diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangsDeleteRequest.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangsDeleteRequest.java similarity index 73% rename from src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangsDeleteRequest.java rename to src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangsDeleteRequest.java index 10122f65..52962440 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/request/LimjangsDeleteRequest.java +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/LimjangsDeleteRequest.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.request; +package umc.th.juinjang.api.limjang.controller.request; import jakarta.validation.constraints.NotEmpty; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequest.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequest.java new file mode 100644 index 00000000..a6e561f5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePatchRequest.java @@ -0,0 +1,44 @@ +package umc.th.juinjang.api.limjang.controller.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.limjang.repository.NotePriceFactory; + +public record NotePatchRequest( + @NotNull + LimjangPriceType priceType, + @NotBlank + @Pattern(regexp = "^[0-9]+$", message = "가격은 숫자만 입력해야 합니다.") + String price, + @Pattern(regexp = "^[0-9]+$", message = "가격은 숫자만 입력해야 합니다.") + String monthlyRent, + @NotBlank + String roadAddress, + String addressDetail, + @NotBlank + String bcode, + @NotBlank + String nickname, + @NotBlank + String floor, + int pyong, + String sido, + String sigungu, + String bname1, + String bname2 +) { + + public LimjangPrice toUpdatedPrice(LimjangPurpose purpose) { + return NotePriceFactory.create(purpose, priceType, price, monthlyRent); + } + + public Address toUpdatedAddress() { + return Address.create(roadAddress, addressDetail, bcode, sido, sigungu, bname1, bname2); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePostRequest.java b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePostRequest.java new file mode 100644 index 00000000..a381e292 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/controller/request/NotePostRequest.java @@ -0,0 +1,51 @@ +package umc.th.juinjang.api.limjang.controller.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.limjang.repository.NotePriceFactory; +import umc.th.juinjang.domain.member.model.Member; + +public record NotePostRequest( + @NotNull + LimjangPurpose purposeType, + @NotNull + LimjangPropertyType propertyType, + @NotNull + LimjangPriceType priceType, + @NotBlank + @Pattern(regexp = "^[0-9]+$", message = "가격은 숫자만 입력해야 합니다.") + String price, + @Pattern(regexp = "^[0-9]+$", message = "가격은 숫자만 입력해야 합니다.") + String monthlyRent, + @NotBlank + String roadAddress, + String addressDetail, + @NotBlank + String bcode, + @NotBlank + String nickname, + @NotBlank + String floor, + int pyong, + String sido, + String sigungu, + String bname1, + String bname2 +) { + public Limjang toEntity(Member member) { + LimjangPrice limjangPrice = NotePriceFactory.create(purposeType, priceType, price, monthlyRent); + Address address = Address.create(roadAddress, addressDetail, bcode, sido, sigungu, bname1, bname2); + + return Limjang.create(member, limjangPrice, purposeType, propertyType, priceType, nickname, address, pyong, + floor); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/LimjangCommandService.java b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangCommandService.java new file mode 100644 index 00000000..14b900c5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangCommandService.java @@ -0,0 +1,16 @@ +package umc.th.juinjang.api.limjang.service; + +import umc.th.juinjang.api.limjang.controller.request.LimjangPatchRequest; +import umc.th.juinjang.api.limjang.controller.request.LimjangPostRequest; +import umc.th.juinjang.api.limjang.controller.request.LimjangsDeleteRequest; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; + +public interface LimjangCommandService { + + Limjang postLimjang(LimjangPostRequest request, Member member); + + void deleteLimjangs(LimjangsDeleteRequest deleteIds, Member member); + + void updateLimjang(Member member, long limjangId, LimjangPatchRequest request); +} diff --git a/src/main/java/umc/th/juinjang/service/limjang/LimjangCommandServiceImpl.java b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangCommandServiceImpl.java similarity index 73% rename from src/main/java/umc/th/juinjang/service/limjang/LimjangCommandServiceImpl.java rename to src/main/java/umc/th/juinjang/api/limjang/service/LimjangCommandServiceImpl.java index 6778b4a2..44402c38 100644 --- a/src/main/java/umc/th/juinjang/service/limjang/LimjangCommandServiceImpl.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangCommandServiceImpl.java @@ -1,24 +1,24 @@ -package umc.th.juinjang.service.limjang; +package umc.th.juinjang.api.limjang.service; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.determineLimjangPrice; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.determineLimjangPrice; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.apiPayload.exception.handler.MemberHandler; -import umc.th.juinjang.model.dto.limjang.request.LimjangPatchRequest; -import umc.th.juinjang.model.dto.limjang.request.LimjangPostRequest; -import umc.th.juinjang.model.dto.limjang.request.LimjangsDeleteRequest; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.LimjangPrice; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.enums.LimjangPriceType; -import umc.th.juinjang.repository.limjang.LimjangRepository; -import umc.th.juinjang.repository.limjang.MemberRepository; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.api.limjang.controller.request.LimjangPatchRequest; +import umc.th.juinjang.api.limjang.controller.request.LimjangPostRequest; +import umc.th.juinjang.api.limjang.controller.request.LimjangsDeleteRequest; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.member.repository.MemberRepository; @Slf4j @Service diff --git a/src/main/java/umc/th/juinjang/service/limjang/LimjangPriceBridge.java b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangPriceBridge.java similarity index 89% rename from src/main/java/umc/th/juinjang/service/limjang/LimjangPriceBridge.java rename to src/main/java/umc/th/juinjang/api/limjang/service/LimjangPriceBridge.java index 69f2c86b..9b5684b5 100644 --- a/src/main/java/umc/th/juinjang/service/limjang/LimjangPriceBridge.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangPriceBridge.java @@ -1,13 +1,13 @@ -package umc.th.juinjang.service.limjang; +package umc.th.juinjang.api.limjang.service; import java.util.ArrayList; import java.util.List; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.LimjangPrice; -import umc.th.juinjang.model.entity.enums.LimjangPriceType; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; public class LimjangPriceBridge { public static LimjangPrice determineLimjangPrice(List priceList, Integer purpose, Integer priceType){ diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/LimjangQueryService.java b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangQueryService.java new file mode 100644 index 00000000..26ab09fd --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangQueryService.java @@ -0,0 +1,23 @@ +package umc.th.juinjang.api.limjang.service; + +import umc.th.juinjang.api.limjang.service.response.LimjangDetailGetResponse; + +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.api.limjang.service.response.LimjangsGetByKeywordResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetVersion2Response; +import umc.th.juinjang.domain.member.model.Member; + +public interface LimjangQueryService { + + LimjangsGetResponse getLimjangTotalList(Member member, LimjangSortOptions sort); + + LimjangsMainGetResponse getLimjangsMain(Member member); + + LimjangsGetByKeywordResponse getLimjangSearchList(Member member, String keyword); + + LimjangDetailGetResponse getDetail(long id, Member member); + + LimjangsMainGetVersion2Response getLimjangsMainVersion2(Member member); +} diff --git a/src/main/java/umc/th/juinjang/service/limjang/LimjangQueryServiceImpl.java b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangQueryServiceImpl.java similarity index 71% rename from src/main/java/umc/th/juinjang/service/limjang/LimjangQueryServiceImpl.java rename to src/main/java/umc/th/juinjang/api/limjang/service/LimjangQueryServiceImpl.java index de4151b0..6f2f4dc6 100644 --- a/src/main/java/umc/th/juinjang/service/limjang/LimjangQueryServiceImpl.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangQueryServiceImpl.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.service.limjang; +package umc.th.juinjang.api.limjang.service; import java.util.HashSet; import java.util.List; @@ -9,20 +9,20 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.apiPayload.exception.handler.MemberHandler; -import umc.th.juinjang.model.dto.limjang.response.LimjangDetailGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsGetByKeywordResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetVersion2Response; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.dto.limjang.enums.LimjangSortOptions; -import umc.th.juinjang.repository.limjang.LimjangRepository; -import umc.th.juinjang.repository.limjang.MemberRepository; -import umc.th.juinjang.repository.limjang.ScrapRepository; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsGetByKeywordResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetVersion2Response; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.domain.scrap.repository.ScrapRepository; @Slf4j @Service @@ -62,7 +62,7 @@ public LimjangDetailGetResponse getDetail(long id, Member member) { } private Limjang getByIdAndMember(Long id, Member member) { - return limjangRepository.findByLimjangIdAndDeletedIsFalse(id, member).orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + return limjangRepository.findByLimjangIdAndMemberAndDeletedIsFalse(id, member).orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); } @Override diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/LimjangSchedulerService.java b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangSchedulerService.java new file mode 100644 index 00000000..5927eb7c --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/LimjangSchedulerService.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.api.limjang.service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import lombok.RequiredArgsConstructor; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; + +@Component +@RequiredArgsConstructor +public class LimjangSchedulerService { + + private final LimjangRepository limjangRepository; + + @Transactional + // @Scheduled(fixedRate = 60000) // 1분 간격으로 실행 - test용 + // @Scheduled(fixedRate = 7 * 24 * 60 * 60 * 1000) // 일주일 간격으로 실행 <- 나중에 출시하면 이걸로 + // @Scheduled(fixedRate = 31557600000L) // 일년 간격으로 실행(임시) + public void cleanUpData() { + // 삭제 필드 true 된지 1달된거 삭제 + LocalDateTime deletionCycle = LocalDateTime.now().minusMonths(1); + + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); + System.out.println("스케줄러 테스트 " + LocalDateTime.now().format(dtf)); + + try { + limjangRepository.hardDelete(deletionCycle); + } catch (Exception e) { + System.out.println("hardDelete 중 에러발생함.."); + } + } + +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java b/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java new file mode 100644 index 00000000..20fda0ca --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/NoteCommandServiceV2.java @@ -0,0 +1,63 @@ +package umc.th.juinjang.api.limjang.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.limjang.controller.request.NotePatchRequest; +import umc.th.juinjang.api.limjang.controller.request.NotePostRequest; +import umc.th.juinjang.api.address.service.AddressUpdater; +import umc.th.juinjang.api.limjang.service.response.NotePostResponse; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.member.model.Member; + +@Service +@RequiredArgsConstructor +public class NoteCommandServiceV2 { + + private final AddressUpdater addressUpdater; + private final NoteUpdater noteUpdater; + private final NotePriceUpdater notePriceUpdater; + private final NoteFinder noteFinder; + + @Transactional + public NotePostResponse createNote(NotePostRequest request, Member member) { + Limjang note = request.toEntity(member); + + validatePriceType(request.purposeType(), request.priceType()); + + notePriceUpdater.save(note.getLimjangPrice()); + addressUpdater.save(note.getAddressEntity()); + Limjang savedNote = noteUpdater.save(note); + return NotePostResponse.of(savedNote.getLimjangId()); + } + + @Transactional + public void updateNote(Long noteId, NotePatchRequest request) { + Limjang note = noteFinder.getNoteByIdWithAddressAndNotePriceWhereDeletedIsFalse(noteId); + + validatePriceType(note.getPurpose(), request.priceType()); + + LimjangPrice newPrice = request.toUpdatedPrice(note.getPurpose()); + Address newAddress = request.toUpdatedAddress(); + + note.getAddressEntity().update(newAddress); + note.getLimjangPrice().updateLimjangPrice(newPrice); + note.updateNote(request.nickname(), request.priceType(), request.floor(), request.pyong()); + } + + private void validatePriceType(LimjangPurpose purposeType, LimjangPriceType priceType) { + if ( + (purposeType == LimjangPurpose.RESIDENTIAL_PURPOSE && priceType == LimjangPriceType.MARKET_PRICE) || + (purposeType == LimjangPurpose.INVESTMENT && priceType != LimjangPriceType.MARKET_PRICE) + ) { + throw new LimjangHandler(ErrorStatus.LIMJANG_POST_TYPE_ERROR); + } + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/NoteFinder.java b/src/main/java/umc/th/juinjang/api/limjang/service/NoteFinder.java new file mode 100644 index 00000000..8a9b0c03 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/NoteFinder.java @@ -0,0 +1,47 @@ +package umc.th.juinjang.api.limjang.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.member.model.Member; + +@Component +@RequiredArgsConstructor +public class NoteFinder { + + private final LimjangRepository limjangRepository; + + protected List findAllByMemberOrderByOptions(Member member, LimjangSortOptions sortOptions, + String keyword) { + return limjangRepository.findAllByMemberAndDeletedIsFalseOrderByParamV2(member, sortOptions, keyword); + } + + public Limjang getNoteByIdWhereDeletedIsFalse(long id) { + return limjangRepository.findByLimjangIdAndDeletedIsFalse(id) + .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + } + + public Limjang getNoteByIdWithAddressAndNotePriceWhereDeletedIsFalse(long id) { + return limjangRepository.findByIdWithAddressAndNotePriceWhereDeletedIsFalse(id) + .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); + } + + protected List getAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalse( + Member member) { + return limjangRepository.findAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalse( + member); + } + + protected List getAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalseAndAddressBcodeIsNotNull( + Member member) { + return limjangRepository.findAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalseAndAddressBcodeIsNotNull( + member); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/NotePriceUpdater.java b/src/main/java/umc/th/juinjang/api/limjang/service/NotePriceUpdater.java new file mode 100644 index 00000000..8c5ebe4f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/NotePriceUpdater.java @@ -0,0 +1,17 @@ +package umc.th.juinjang.api.limjang.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.repository.LimjangPriceRepository; + +@Component +@RequiredArgsConstructor +public class NotePriceUpdater { + private final LimjangPriceRepository limjangPriceRepository; + + protected void save(LimjangPrice limjangPrice) { + limjangPriceRepository.save(limjangPrice); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/NoteQueryServiceV2.java b/src/main/java/umc/th/juinjang/api/limjang/service/NoteQueryServiceV2.java new file mode 100644 index 00000000..7e1a0ea9 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/NoteQueryServiceV2.java @@ -0,0 +1,185 @@ +package umc.th.juinjang.api.limjang.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.checklist.service.ChecklistAnswerFinder; +import umc.th.juinjang.api.image.service.ImageFinder; +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.api.limjang.service.response.ChecklistConditionResponse; +import umc.th.juinjang.api.limjang.service.response.UserNoteGetResponse; +import umc.th.juinjang.api.limjang.service.response.UserNotesGetResponse; +import umc.th.juinjang.api.limjang.service.response.UserNotesShareableGetResponse; +import umc.th.juinjang.api.note.shared.service.SharedNoteFinder; +import umc.th.juinjang.api.scrap.service.ScarpFinder; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionCategory; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.member.model.Member; + +@Service +@RequiredArgsConstructor +public class NoteQueryServiceV2 { + + private final NoteFinder noteFinder; + private final ScarpFinder scarpFinder; + private final ImageFinder imageFinder; + private final ChecklistAnswerFinder checklistAnswerFinder; + private final SharedNoteFinder sharedNoteFinder; + + @Transactional(readOnly = true) + public UserNotesGetResponse findUsersNotes(Member member, LimjangSortOptions sortOptions, String keyword) { + List notes = noteFinder.findAllByMemberOrderByOptions(member, sortOptions, keyword); + return UserNotesGetResponse.of(notes, mapToNoteScrapStatus(notes)); + } + + private Map mapToNoteScrapStatus(List notes) { + Set notesIdInScrap = getNotesIdInScraps(notes); + return notes.stream().collect(Collectors.toMap( + Limjang::getLimjangId, + it -> notesIdInScrap.contains(it.getLimjangId()) + )); + } + + private Set getNotesIdInScraps(List notes) { + return new HashSet<>(scarpFinder.findAllByNoteId(notes) + .stream() + .map(it -> it.getLimjangId().getLimjangId()) + .toList()); + } + + @Transactional(readOnly = true) + public UserNotesShareableGetResponse findNotesShareable(Member member) { + List filteredSharedNotes = findUnsharedSharableNotes(member); + List imageList = imageFinder.findAllFirstCreatedImagePerNote(filteredSharedNotes); + + // 예상 리워드 판별 + Map mapToExpectedReward = mapInCalculateReward(filteredSharedNotes); + + return UserNotesShareableGetResponse.of(filteredSharedNotes, mapToNoteIdAndImageId(imageList), + mapToNoteScrapStatus(filteredSharedNotes), mapToExpectedReward); + } + + private Map mapInCalculateReward(List notes) { + Set noteIdsWithPastSharedHistory = + sharedNoteFinder.findLimjangIdsByDeletedAtIsNotNullAndLimjang(notes); + + Map mapToExpectedRewardPencil = new HashMap<>(); + for (Limjang note : notes) { + mapToExpectedRewardPencil.put(note.getLimjangId(), calculateReward(note, noteIdsWithPastSharedHistory)); + } + return mapToExpectedRewardPencil; + } + + private Long calculateReward(Limjang note, Set previouslySharedNoteIds) { + if (previouslySharedNoteIds.contains(note.getLimjangId())) + return null; + return note.getImageList().isEmpty() ? 2L : 7L; + } + + private List findUnsharedSharableNotes(Member member) { + List notes = noteFinder.getAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalseAndAddressBcodeIsNotNull( + member); + // 이미 공유 중인 임장들 필터링 + Set noteIdInSharedNotes = sharedNoteFinder.findLimjangIdsByDeletedAtIsNullAndLimjang(notes); + + return notes.stream() + .filter(note -> !noteIdInSharedNotes.contains(note.getLimjangId())) + .toList(); + } + + private Map mapToNoteIdAndImageId(List imageList) { + return imageList.stream() + .collect(Collectors.toMap( + image -> image.getLimjangId().getLimjangId(), + image -> image.getImageUrl() + )); + } + + @Transactional(readOnly = true) + public UserNoteGetResponse findNote(Long noteId) { + Limjang note = noteFinder.getNoteByIdWithAddressAndNotePriceWhereDeletedIsFalse(noteId); + boolean isShared = sharedNoteFinder.existsByDeletedAtIsNullAndLimjang(note); + return UserNoteGetResponse.of(isShared, note, note.getAddressEntity()); + } + + public ChecklistConditionResponse checkLimjangChecklistSatisfaction(Long limjangId) { + Limjang limjang = noteFinder.getNoteByIdWhereDeletedIsFalse(limjangId); + LimjangPurpose purpose = limjang.getPurpose(); + List answers = checklistAnswerFinder.findEntitiesByLimjangId(limjangId); + + Map answeredCountByCategory = answers.stream() + .collect(Collectors.groupingBy( + a -> a.getQuestionId().getCategory(), + Collectors.counting() + )); + + List results = new ArrayList<>(); + boolean allSatisfied = true; + + for (ChecklistQuestionCategory category : List.of( + ChecklistQuestionCategory.LOCATION_CONDITION, + ChecklistQuestionCategory.PUBLIC_SPACE, + ChecklistQuestionCategory.INDOOR + )) { + int totalCount = getTotalCount(purpose, category); + int requiredCount = getRequiredCount(purpose, category); + int answeredCount = answeredCountByCategory.getOrDefault(category, 0L).intValue(); + + boolean satisfied = answeredCount >= requiredCount; + if (!satisfied) + allSatisfied = false; + + results.add(new ChecklistConditionResponse.CategoryCondition( + category.name(), answeredCount, totalCount, requiredCount, satisfied + )); + } + + return new ChecklistConditionResponse(allSatisfied, results); + } + + private int getTotalCount(LimjangPurpose purpose, ChecklistQuestionCategory category) { + return switch (purpose) { + case INVESTMENT -> switch (category) { + case LOCATION_CONDITION -> 19; + case PUBLIC_SPACE -> 8; + case INDOOR -> 21; + default -> 0; + }; + case RESIDENTIAL_PURPOSE -> switch (category) { + case LOCATION_CONDITION -> 9; + case PUBLIC_SPACE -> 6; + case INDOOR -> 20; + default -> 0; + }; + }; + } + + private int getRequiredCount(LimjangPurpose purpose, ChecklistQuestionCategory category) { + return switch (purpose) { + case INVESTMENT -> switch (category) { + case LOCATION_CONDITION -> 16; + case PUBLIC_SPACE -> 5; + case INDOOR -> 18; + default -> 0; + }; + case RESIDENTIAL_PURPOSE -> switch (category) { + case LOCATION_CONDITION -> 7; + case PUBLIC_SPACE -> 4; + case INDOOR -> 18; + default -> 0; + }; + }; + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/NoteUpdater.java b/src/main/java/umc/th/juinjang/api/limjang/service/NoteUpdater.java new file mode 100644 index 00000000..a35aa2f8 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/NoteUpdater.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.limjang.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; + +@Component +@RequiredArgsConstructor +public class NoteUpdater { + + private final LimjangRepository limjangRepository; + + public Limjang save(Limjang limjang) { + return limjangRepository.save(limjang); + } +} diff --git a/src/main/java/umc/th/juinjang/converter/limjang/LimjangDetailConverter.java b/src/main/java/umc/th/juinjang/api/limjang/service/converter/LimjangDetailConverter.java similarity index 71% rename from src/main/java/umc/th/juinjang/converter/limjang/LimjangDetailConverter.java rename to src/main/java/umc/th/juinjang/api/limjang/service/converter/LimjangDetailConverter.java index 08efd522..f4fed15f 100644 --- a/src/main/java/umc/th/juinjang/converter/limjang/LimjangDetailConverter.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/converter/LimjangDetailConverter.java @@ -1,17 +1,17 @@ -package umc.th.juinjang.converter.limjang; +package umc.th.juinjang.api.limjang.service.converter; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.makePriceListVersion2; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.makePriceListVersion2; import java.util.Comparator; import java.util.List; -import umc.th.juinjang.model.dto.limjang.response.LimjangDetailResponseDTO; -import umc.th.juinjang.model.entity.enums.LimjangCheckListVersion; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.LimjangPrice; -import umc.th.juinjang.model.entity.enums.LimjangPriceType; -import umc.th.juinjang.model.entity.enums.LimjangPropertyType; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailResponseDTO; +import umc.th.juinjang.domain.checklist.model.LimjangCheckListVersion; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; public class LimjangDetailConverter { diff --git a/src/main/java/umc/th/juinjang/converter/limjang/LimjangsMainGetResponseConverter.java b/src/main/java/umc/th/juinjang/api/limjang/service/converter/LimjangsMainGetResponseConverter.java similarity index 62% rename from src/main/java/umc/th/juinjang/converter/limjang/LimjangsMainGetResponseConverter.java rename to src/main/java/umc/th/juinjang/api/limjang/service/converter/LimjangsMainGetResponseConverter.java index 6e76a7b3..b8489696 100644 --- a/src/main/java/umc/th/juinjang/converter/limjang/LimjangsMainGetResponseConverter.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/converter/LimjangsMainGetResponseConverter.java @@ -1,12 +1,12 @@ -package umc.th.juinjang.converter.limjang; +package umc.th.juinjang.api.limjang.service.converter; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.getPriceToString; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.getPriceToString; import java.util.Optional; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetResponse.LimjangMainResponse; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Report; +import umc.th.juinjang.api.limjang.service.response.LimjangsMainGetResponse.LimjangMainResponse; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; public class LimjangsMainGetResponseConverter { diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/response/ChecklistConditionResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/ChecklistConditionResponse.java new file mode 100644 index 00000000..3453b44f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/ChecklistConditionResponse.java @@ -0,0 +1,17 @@ +package umc.th.juinjang.api.limjang.service.response; + +import java.util.List; + +public record ChecklistConditionResponse( + boolean isTotalSatisfied, + List conditions +) { + public record CategoryCondition( + String category, + int answeredCount, + int totalCount, + int requiredCount, + boolean isSatisfied + ) { + } +} diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangDetailGetResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangDetailGetResponse.java similarity index 78% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangDetailGetResponse.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangDetailGetResponse.java index 8a59bdb3..63b8251b 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangDetailGetResponse.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangDetailGetResponse.java @@ -1,13 +1,13 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.makePriceListVersion2; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.makePriceListVersion2; import java.time.LocalDateTime; import java.util.List; import lombok.Builder; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.enums.LimjangCheckListVersion; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.checklist.model.LimjangCheckListVersion; +import umc.th.juinjang.domain.limjang.model.Limjang; @Builder public record LimjangDetailGetResponse( diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangDetailResponseDTO.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangDetailResponseDTO.java similarity index 84% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangDetailResponseDTO.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangDetailResponseDTO.java index 3f8fac01..0d2e1ab5 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangDetailResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangDetailResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; import java.time.LocalDateTime; import java.util.List; @@ -6,7 +6,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.enums.LimjangCheckListVersion; +import umc.th.juinjang.domain.checklist.model.LimjangCheckListVersion; public class LimjangDetailResponseDTO { diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangMemoResponseDTO.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangMemoResponseDTO.java similarity index 89% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangMemoResponseDTO.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangMemoResponseDTO.java index 5ddf3df5..d58d5691 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangMemoResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangMemoResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangPostResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangPostResponse.java similarity index 70% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangPostResponse.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangPostResponse.java index 489edb32..34f10b50 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangPostResponse.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangPostResponse.java @@ -1,7 +1,7 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; import java.time.LocalDateTime; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.domain.limjang.model.Limjang; public record LimjangPostResponse(Long limjangId, LocalDateTime createdAt) { public static LimjangPostResponse of(Limjang limjang) { diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangTotalListResponseDTO.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangTotalListResponseDTO.java similarity index 93% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangTotalListResponseDTO.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangTotalListResponseDTO.java index 4264fc3a..b9cf0ee3 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangTotalListResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangTotalListResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; import java.util.List; import lombok.AllArgsConstructor; diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsGetByKeywordResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsGetByKeywordResponse.java similarity index 84% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsGetByKeywordResponse.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsGetByKeywordResponse.java index 95700608..fb4df30c 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsGetByKeywordResponse.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsGetByKeywordResponse.java @@ -1,11 +1,11 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.makePriceListVersion2; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.makePriceListVersion2; import java.util.List; import java.util.Map; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; public record LimjangsGetByKeywordResponse(List limjangList) { record LimjangByKeywordResponse( diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsGetResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsGetResponse.java similarity index 84% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsGetResponse.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsGetResponse.java index 2e0afcad..4f8b5e64 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsGetResponse.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsGetResponse.java @@ -1,13 +1,13 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.makePriceListVersion2; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.makePriceListVersion2; import java.util.List; import java.util.Map; import lombok.Builder; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.LimjangPrice; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; public record LimjangsGetResponse(List limjangList) { @Builder diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsMainGetResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsMainGetResponse.java similarity index 76% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsMainGetResponse.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsMainGetResponse.java index 86e286ee..493b5084 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsMainGetResponse.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsMainGetResponse.java @@ -1,8 +1,8 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; import java.util.List; -import umc.th.juinjang.converter.limjang.LimjangsMainGetResponseConverter; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.api.limjang.service.converter.LimjangsMainGetResponseConverter; +import umc.th.juinjang.domain.limjang.model.Limjang; public record LimjangsMainGetResponse( List recentUpdatedList diff --git a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsMainGetVersion2Response.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsMainGetVersion2Response.java similarity index 84% rename from src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsMainGetVersion2Response.java rename to src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsMainGetVersion2Response.java index 024bbd6b..afd83ee9 100644 --- a/src/main/java/umc/th/juinjang/model/dto/limjang/response/LimjangsMainGetVersion2Response.java +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/LimjangsMainGetVersion2Response.java @@ -1,10 +1,10 @@ -package umc.th.juinjang.model.dto.limjang.response; +package umc.th.juinjang.api.limjang.service.response; -import static umc.th.juinjang.service.limjang.LimjangPriceBridge.getPriceToString; +import static umc.th.juinjang.api.limjang.service.LimjangPriceBridge.getPriceToString; import java.util.List; import lombok.Builder; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.domain.limjang.model.Limjang; public record LimjangsMainGetVersion2Response(List recentUpdatedList) { @Builder diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/response/NotePostResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/NotePostResponse.java new file mode 100644 index 00000000..11d4e133 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/NotePostResponse.java @@ -0,0 +1,7 @@ +package umc.th.juinjang.api.limjang.service.response; + +public record NotePostResponse(Long noteId) { + public static NotePostResponse of(Long noteId) { + return new NotePostResponse(noteId); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNoteGetResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNoteGetResponse.java new file mode 100644 index 00000000..2496cb60 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNoteGetResponse.java @@ -0,0 +1,55 @@ +package umc.th.juinjang.api.limjang.service.response; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; + +public record UserNoteGetResponse( + boolean isShared, + LimjangPurpose purposeType, + LimjangPropertyType propertyType, + LimjangPriceType priceType, + String buildingName, + List images, + String roadAddress, + String addressDetail, + String price, + String monthlyRent, + String updatedAt, + String floor, + Integer pyong, + String bcode, + String sido, + String sigungu, + String bname1, + String bname2 +) { + public static UserNoteGetResponse of(boolean isShared, Limjang note, Address address) { + return new UserNoteGetResponse( + isShared, + note.getPurpose(), + note.getPropertyType(), + note.getPriceType(), + note.getNickname(), + note.getImageList().stream().map(Image::getImageUrl).limit(3).toList(), + address.getRoadAddress(), + address.getAddressDetail(), + note.getLimjangPrice().getPrice(note.getPriceType(), note.getPurpose()), + note.getPriceType() == LimjangPriceType.MONTHLY_RENT ? note.getLimjangPrice().getMonthlyRent() : null, + note.getUpdatedAt().format(DateTimeFormatter.ofPattern("yy.MM.dd")), + note.getFloor(), + note.getPyong(), + address.getBcode(), + address.getSido(), + address.getSigungo(), + address.getBname1(), + address.getBname2() + ); + } +} diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNotesGetResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNotesGetResponse.java new file mode 100644 index 00000000..51038f39 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNotesGetResponse.java @@ -0,0 +1,52 @@ +package umc.th.juinjang.api.limjang.service.response; + +import java.util.List; +import java.util.Map; + +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; + +public record UserNotesGetResponse( + List notes +) { + record UserNoteResponse( + long noteId, + LimjangPurpose purposeType, + LimjangPropertyType propertyType, + LimjangPriceType priceType, + String name, + List imageUrl, + boolean isScraped, + String rate, + String price, + String monthlyRent, + Integer pyong, + String floor, + String address, + String shortAddress + ) { + static UserNoteResponse of(Limjang limjang, boolean isScraped) { + return new UserNoteResponse( + limjang.getLimjangId(), limjang.getPurpose(), limjang.getPropertyType(), limjang.getPriceType(), + limjang.getNickname(), + limjang.getImageList().stream().map(Image::getImageUrl).limit(3).toList(), + isScraped, + limjang.getReport() == null ? null : limjang.getReport().getTotalRate().toString(), + limjang.getLimjangPrice().getPrice(limjang.getPriceType(), limjang.getPurpose()), + limjang.getPriceType() == LimjangPriceType.MONTHLY_RENT ? limjang.getLimjangPrice().getMonthlyRent() : + null, + limjang.getPyong(), + limjang.getFloor(), + limjang.getAddressEntity().getRoadAddress(), + limjang.getAddressEntity().getShortAddress()); + } + } + + public static UserNotesGetResponse of(List limjangs, Map isScraped) { + return new UserNotesGetResponse( + limjangs.stream().map(it -> UserNoteResponse.of(it, isScraped.get(it.getLimjangId()))).toList()); + } +} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNotesShareableGetResponse.java b/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNotesShareableGetResponse.java new file mode 100644 index 00000000..c6c51821 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/limjang/service/response/UserNotesShareableGetResponse.java @@ -0,0 +1,58 @@ +package umc.th.juinjang.api.limjang.service.response; + +import java.util.List; +import java.util.Map; + +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; + +public record UserNotesShareableGetResponse( + List notes +) { + record UserNoteShareableResponse( + long noteId, + LimjangPurpose purposeType, + LimjangPropertyType propertyType, + LimjangPriceType priceType, + String name, + String imageUrl, + boolean isScraped, + String rate, + String price, + String monthlyRent, + Integer pyong, + String floor, + String shortAddress, + Long rewardPencil + ) { + + static UserNoteShareableResponse of(Limjang limjang, String imageUrl, boolean isScraped, Long rewardPencil) { + return new UserNoteShareableResponse( + limjang.getLimjangId(), limjang.getPurpose(), limjang.getPropertyType(), limjang.getPriceType(), + limjang.getNickname(), + imageUrl, + isScraped, + limjang.getReport() == null ? null : limjang.getReport().getTotalRate().toString(), + limjang.getLimjangPrice().getPrice(limjang.getPriceType(), limjang.getPurpose()), + limjang.getPriceType() == LimjangPriceType.MONTHLY_RENT ? limjang.getLimjangPrice().getMonthlyRent() : + null, + limjang.getPyong(), + limjang.getFloor(), + limjang.getAddressEntity().getShortAddress(), + rewardPencil + ); + } + } + + public static UserNotesShareableGetResponse of(List limjangs, Map imageUrl, + Map isScraped, Map expectedReward) { + return new UserNotesShareableGetResponse( + limjangs.stream() + .map(it -> UserNoteShareableResponse.of(it, imageUrl.get(it.getLimjangId()), + isScraped.get(it.getLimjangId()), expectedReward.get(it.getLimjangId()))) + .toList()); + } +} + diff --git a/src/main/java/umc/th/juinjang/api/member/controller/MemberController.java b/src/main/java/umc/th/juinjang/api/member/controller/MemberController.java new file mode 100644 index 00000000..2e98747c --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/member/controller/MemberController.java @@ -0,0 +1,93 @@ +package umc.th.juinjang.api.member.controller; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import java.util.Map; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.member.controller.request.IntroductionPatchRequest; +import umc.th.juinjang.api.member.controller.request.MemberAgreeVersionPostRequest; +import umc.th.juinjang.api.member.controller.request.MemberRequestDto; +import umc.th.juinjang.api.member.service.MemberService; +import umc.th.juinjang.api.member.service.response.MemberResponseDto; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +@Validated +public class MemberController { + + private final MemberService memberService; + + @CrossOrigin + @Operation(summary = "닉네임 설정") + @PatchMapping("/nickname") + public ApiResponse patchNickname(@AuthenticationPrincipal Member member, + @RequestBody MemberRequestDto memberRequestDto) { + if (!memberRequestDto.getNickname().isEmpty()) { + MemberResponseDto.nicknameDto result = memberService.patchNickname(member, memberRequestDto); + return ApiResponse.onSuccess(result); + } else + throw new ExceptionHandler(NICKNAME_EMPTY); + } + + @CrossOrigin + @Operation(summary = "프로필 조회") + @GetMapping("/profile") + public ApiResponse getProfile(@AuthenticationPrincipal Member member) { + MemberResponseDto.profileDto result = memberService.getProfile(member); + return ApiResponse.onSuccess(result); + } + + @CrossOrigin + @Operation(summary = "프로필 이미지 수정") + @PatchMapping("/profile/image") + public ApiResponse getProfile(@AuthenticationPrincipal Member member, + @RequestPart MultipartFile multipartFile) { + if (multipartFile == null || multipartFile.isEmpty()) + throw new ExceptionHandler(ErrorStatus.IMAGE_EMPTY); + MemberResponseDto.profileDto result = memberService.updateProfileImage(member, multipartFile); + return ApiResponse.onSuccess(result); + } + + @Operation(summary = "한줄 소개 변경") + @PatchMapping("/profile/introduction") + public ApiResponse updateIntroduction(@AuthenticationPrincipal Member member, + @RequestBody IntroductionPatchRequest request) { + memberService.updateIntroduction(member, request.getIntroduction()); + return ApiResponse.onSuccess(null); + } + + @Operation(summary = "약관 동의 버전 전송") + @PatchMapping("/members/terms") + public ApiResponse createMemberAgreeVersion(@AuthenticationPrincipal Member member, + @RequestBody @Valid MemberAgreeVersionPostRequest memberAgreeVersionPostRequest) { + memberService.createMemberAgreeVersion(member, memberAgreeVersionPostRequest); + return ApiResponse.onSuccess(null); + } + + @Operation(summary = "닉네임 중복 여부") + @GetMapping("/members/nickname/exists") + public ApiResponse> isNicknameExists(@RequestParam String nickname) { + return ApiResponse.onSuccess(Map.of("exists", memberService.isNicknameExists(nickname))); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/member/controller/request/IntroductionPatchRequest.java b/src/main/java/umc/th/juinjang/api/member/controller/request/IntroductionPatchRequest.java new file mode 100644 index 00000000..2abb9248 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/member/controller/request/IntroductionPatchRequest.java @@ -0,0 +1,8 @@ +package umc.th.juinjang.api.member.controller.request; + +import lombok.Getter; + +@Getter +public class IntroductionPatchRequest { + private String introduction; +} diff --git a/src/main/java/umc/th/juinjang/model/dto/member/MemberAgreeVersionPostRequest.java b/src/main/java/umc/th/juinjang/api/member/controller/request/MemberAgreeVersionPostRequest.java similarity index 85% rename from src/main/java/umc/th/juinjang/model/dto/member/MemberAgreeVersionPostRequest.java rename to src/main/java/umc/th/juinjang/api/member/controller/request/MemberAgreeVersionPostRequest.java index 599ad67c..fce37983 100644 --- a/src/main/java/umc/th/juinjang/model/dto/member/MemberAgreeVersionPostRequest.java +++ b/src/main/java/umc/th/juinjang/api/member/controller/request/MemberAgreeVersionPostRequest.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.member; +package umc.th.juinjang.api.member.controller.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; diff --git a/src/main/java/umc/th/juinjang/api/member/controller/request/MemberRequestDto.java b/src/main/java/umc/th/juinjang/api/member/controller/request/MemberRequestDto.java new file mode 100644 index 00000000..a6167b7e --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/member/controller/request/MemberRequestDto.java @@ -0,0 +1,17 @@ +package umc.th.juinjang.api.member.controller.request; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberRequestDto { + private String nickname; + + @Builder + public MemberRequestDto(String nickname) { + this.nickname = nickname; + } +} diff --git a/src/main/java/umc/th/juinjang/api/member/service/MemberService.java b/src/main/java/umc/th/juinjang/api/member/service/MemberService.java new file mode 100644 index 00000000..1953455c --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/member/service/MemberService.java @@ -0,0 +1,114 @@ +package umc.th.juinjang.api.member.service; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import java.io.IOException; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.member.controller.request.MemberAgreeVersionPostRequest; +import umc.th.juinjang.api.member.controller.request.MemberRequestDto; +import umc.th.juinjang.api.member.service.response.MemberResponseDto; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.repository.MemberRepository; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class MemberService { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.uploadPath}") + private String defaultUrl; + + private final AmazonS3Client amazonS3Client; + private final MemberRepository memberRepository; + + // 닉네임 수정 + public MemberResponseDto.nicknameDto patchNickname(Member member, MemberRequestDto memberRequestDto) { + // Member 받아오면 해당 member의 nickname 변경 + member.updateNickname(memberRequestDto.getNickname()); + memberRepository.save(member); // 변수 없이 member 그대로 저장 + + return new MemberResponseDto.nicknameDto(member.getNickname()); + } + + public void updateIntroduction(Member member, String introduction) { + Long memberId = member.getMemberId(); + memberRepository.patchIntroduction(memberId, introduction); + } + + // 프로필 조회 + public MemberResponseDto.profileDto getProfile(Member member) { + String provider = member.getProvider().toString(); + return new MemberResponseDto.profileDto(member.getNickname(), member.getEmail(), provider, + member.getImageUrl(), member.getIntroduction()); + } + + // 프로필 이미지 수정 + public MemberResponseDto.profileDto updateProfileImage(Member member, MultipartFile multipartFile) { + String newUrl = null; + String fileUrl = member.getImageUrl(); + + if (fileUrl != null) { + String[] url = fileUrl.split("/"); + amazonS3Client.deleteObject(bucket, url[3]); + } + + try { + String originalFilename = multipartFile.getOriginalFilename(); + String newfileName = UUID.randomUUID() + "_" + originalFilename; + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(multipartFile.getSize()); + metadata.setContentType(multipartFile.getContentType()); + + //S3에 저장 + amazonS3Client.putObject(bucket, "profile/" + newfileName, multipartFile.getInputStream(), metadata); + newUrl = amazonS3Client.getUrl(bucket, "profile/" + newfileName).toString(); + } catch (IOException e) { + e.printStackTrace(); + } catch (AmazonServiceException e) { + e.printStackTrace(); + } + + if (newUrl == null) + throw new ExceptionHandler(ErrorStatus.IMAGE_NOT_SAVE); + + member.updateImage(newUrl); + memberRepository.save(member); + + return new MemberResponseDto.profileDto(member.getNickname(), member.getEmail(), + member.getProvider().toString(), member.getImageUrl(), member.getIntroduction()); + } + + public void createMemberAgreeVersion(final Member member, + final MemberAgreeVersionPostRequest memberAgreeVersionPostRequest) { + getMember(member).updateAgreeVersion(memberAgreeVersionPostRequest.agreeVersion()); + } + + public boolean isNicknameExists(String nickname) { + return memberRepository.existsByNickname(nickname); + } + + private Member getMember(Member member) { + return memberRepository.findById(member.getMemberId()).orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/member/service/response/MemberResponseDto.java b/src/main/java/umc/th/juinjang/api/member/service/response/MemberResponseDto.java new file mode 100644 index 00000000..872273a3 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/member/service/response/MemberResponseDto.java @@ -0,0 +1,30 @@ +package umc.th.juinjang.api.member.service.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class MemberResponseDto { + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class nicknameDto { + private String nickname; + } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class profileDto { + private String nickname; + private String email; + private String provider; + private String image; + private String introduction; + } + +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/controller/LikedNoteController.java b/src/main/java/umc/th/juinjang/api/note/liked/controller/LikedNoteController.java new file mode 100644 index 00000000..3e6ee00c --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/controller/LikedNoteController.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.api.note.liked.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.note.liked.service.LikedNoteCommandService; +import umc.th.juinjang.api.note.liked.service.response.LikedNoteDeleteResponse; +import umc.th.juinjang.api.note.liked.service.response.LikedNotePostResponse; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2/shared-notes") +@RequiredArgsConstructor +public class LikedNoteController { + + private final LikedNoteCommandService likedNoteCommandService; + + @Operation(summary = "공유노트 좋아요 등록 API") + @PostMapping("/{sharedNoteId}/likes") + public ApiResponse createLikedNote(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess(likedNoteCommandService.createLikedNote(member, sharedNoteId)); + } + + @Operation(summary = "공유노트 좋아요 취소 API") + @DeleteMapping("/{sharedNoteId}/likes") + public ApiResponse deleteLikedNote(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess(likedNoteCommandService.deleteLikedNote(member, sharedNoteId)); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteCommandService.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteCommandService.java new file mode 100644 index 00000000..b875eca6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteCommandService.java @@ -0,0 +1,47 @@ +package umc.th.juinjang.api.note.liked.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.note.liked.service.response.LikedNoteDeleteResponse; +import umc.th.juinjang.api.note.liked.service.response.LikedNotePostResponse; +import umc.th.juinjang.api.note.shared.service.SharedNoteFinder; +import umc.th.juinjang.api.note.shared.service.SharedNoteUpdater; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +@Service +@RequiredArgsConstructor +public class LikedNoteCommandService { + + private final LikedNoteUpdater likedNoteUpdater; + private final SharedNoteFinder sharedNoteFinder; + private final SharedNoteUpdater sharedNoteUpdater; + private final LikedNoteFinder likedNoteFinder; + private final LikedNoteDeleter likedNoteDeleter; + + @Transactional + public LikedNotePostResponse createLikedNote(Member member, Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.getByIdWhereDeletedAtIsNull(sharedNoteId); + LikedNote likedNote = LikedNote.create(member, sharedNote); + + likedNoteUpdater.save(likedNote); + sharedNoteUpdater.incrementLikedCountById(sharedNoteId); + + return new LikedNotePostResponse(sharedNoteFinder.getLikedNoteById(sharedNoteId)); + } + + @Transactional + public LikedNoteDeleteResponse deleteLikedNote(Member member, Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.getByIdWhereDeletedAtIsNull(sharedNoteId); + LikedNote likedNote = likedNoteFinder.getByMemberAndSharedNote(member, sharedNote); + + likedNoteDeleter.delete(likedNote); + sharedNoteUpdater.decrementLikedCountById(sharedNoteId); + + return new LikedNoteDeleteResponse(sharedNoteFinder.getLikedNoteById(sharedNoteId)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteDeleter.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteDeleter.java new file mode 100644 index 00000000..5aaf7d50 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteDeleter.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.note.liked.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.liked.model.repository.LikedNoteRepository; + +@Component +@RequiredArgsConstructor +public class LikedNoteDeleter { + + private final LikedNoteRepository likedNoteRepository; + + void delete(LikedNote likedNote) { + likedNoteRepository.delete(likedNote); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java new file mode 100644 index 00000000..b32b1062 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java @@ -0,0 +1,41 @@ +package umc.th.juinjang.api.note.liked.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LikedNoteHandler; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.liked.model.repository.LikedNoteRepository; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +@Component +@RequiredArgsConstructor +public class LikedNoteFinder { + + private final LikedNoteRepository likedNoteRepository; + + public boolean existsByMemberAndSharedNote(Member member, SharedNote sharedNote) { + return likedNoteRepository.existsByMemberAndSharedNote(member, sharedNote); + } + + public LikedNote getByMemberAndSharedNote(Member member, SharedNote sharedNote) { + return likedNoteRepository.findByMemberAndSharedNote(member, sharedNote) + .orElseThrow(() -> new LikedNoteHandler(ErrorStatus.LIKEDNOTE_NOT_FOUND)); + } + + public List findLikedSharedNoteIds(Member member, List sharedNotes) { + return likedNoteRepository.findLikedSharedNoteIds(member, sharedNotes); + } + + public List findAllByMemberAndDynamic(Member user, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword) { + return likedNoteRepository.findAllByMemberAndDynamicWhereDeletedAtIsNull(user, propertyType, priceType, + keyword); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteUpdater.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteUpdater.java new file mode 100644 index 00000000..ea11e6ec --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteUpdater.java @@ -0,0 +1,27 @@ +package umc.th.juinjang.api.note.liked.service; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LikedNoteHandler; +import umc.th.juinjang.common.exception.handler.SharedNoteHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.liked.model.repository.LikedNoteRepository; + +@Component +@RequiredArgsConstructor +public class LikedNoteUpdater { + + private final LikedNoteRepository likedNoteRepository; + + public void save(LikedNote likedNote) { + try { + likedNoteRepository.save(likedNote); + } catch (DataIntegrityViolationException e) { + throw new LikedNoteHandler(ErrorStatus.LIKEDNOTE_CONFLICT); + } + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNoteDeleteResponse.java b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNoteDeleteResponse.java new file mode 100644 index 00000000..2130c384 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNoteDeleteResponse.java @@ -0,0 +1,4 @@ +package umc.th.juinjang.api.note.liked.service.response; + +public record LikedNoteDeleteResponse(Long count) { +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNotePostResponse.java b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNotePostResponse.java new file mode 100644 index 00000000..4192f40f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNotePostResponse.java @@ -0,0 +1,4 @@ +package umc.th.juinjang.api.note.liked.service.response; + +public record LikedNotePostResponse(long count) { +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/controller/SharedNoteController.java b/src/main/java/umc/th/juinjang/api/note/shared/controller/SharedNoteController.java new file mode 100644 index 00000000..2e496c0e --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/controller/SharedNoteController.java @@ -0,0 +1,121 @@ +package umc.th.juinjang.api.note.shared.controller; + +import static umc.th.juinjang.common.code.status.SuccessStatus.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.checklist.service.response.ReportWithLimjangResponseDTO; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.note.shared.controller.request.ExploreSortType; +import umc.th.juinjang.api.note.shared.controller.request.NoteType; +import umc.th.juinjang.api.note.shared.controller.request.SharedNotePostRequest; +import umc.th.juinjang.api.note.shared.service.SharedNoteCommandService; +import umc.th.juinjang.api.note.shared.service.SharedNoteQueryService; +import umc.th.juinjang.api.note.shared.service.response.SharedNoteCheckListAndReviewResponse; +import umc.th.juinjang.api.note.shared.service.response.SharedNoteExploreGetResponse; +import umc.th.juinjang.api.note.shared.service.response.SharedNoteGetResponse; +import umc.th.juinjang.api.note.shared.service.response.SharedNotePostResponse; +import umc.th.juinjang.api.note.shared.service.response.UserSharedNotesGetResponse; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2") +@RequiredArgsConstructor +public class SharedNoteController { + + private final SharedNoteCommandService sharedNoteCommandService; + private final SharedNoteQueryService sharedNoteQueryService; + + @Operation(summary = "노트 구매 API") + @PostMapping("/shared-notes/{sharedNoteId}/purchase") + public ApiResponse createSharedNotePurchase(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + sharedNoteCommandService.createSharedNotePurchase(member, sharedNoteId); + return ApiResponse.onSuccess(null); + } + + @Operation(summary = "공유 노트 상세보기 API") + @GetMapping("/shared-notes/{sharedNoteId}") + public ApiResponse findSharedNote(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess(sharedNoteQueryService.findSharedNote(member, sharedNoteId)); + } + + @Operation(summary = "임장 노트 공유 중단하기 API") + @DeleteMapping("/shared-notes/{sharedNoteId}") + public ApiResponse deleteSharedNote(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + sharedNoteCommandService.deleteSharedNote(member, sharedNoteId, LocalDateTime.now()); + return ApiResponse.of(SHARED_NOTE_DELETE, null); + } + + @Operation(summary = "공유 노트 생성 API") + @PostMapping("/{noteId}") + public ApiResponse uploadSharedNote(@AuthenticationPrincipal Member member, + @PathVariable("noteId") Long noteId, + @RequestBody SharedNotePostRequest request) { + return ApiResponse.onSuccess(sharedNoteCommandService.createSharedNote(member, noteId, request)); + } + + @Operation(summary = "공유 노트 둘러보기 API") + @GetMapping("/explore") + public ApiResponse findSharedNote(@AuthenticationPrincipal Member member, + @RequestParam(value = "code", required = false) List code, + @RequestParam(value = "sort", required = false) ExploreSortType sort, + @RequestParam(value = "propertyType", required = false) LimjangPropertyType propertyType, + @RequestParam(value = "priceType", required = false) LimjangPriceType priceType, + @RequestParam(value = "keyword", required = false) String keyword, + Pageable pageable + ) { + return ApiResponse.onSuccess( + sharedNoteQueryService.findExploreSharedNote(member, code, sort, propertyType, priceType, + keyword, pageable)); + } + + @Operation(summary = "마이 노트 API") + @GetMapping("/users/shared-notes") + public ApiResponse findUsersSharedNotes(@AuthenticationPrincipal Member member, + @RequestParam(value = "noteType") NoteType noteType, + @RequestParam(value = "propertyType", required = false) LimjangPropertyType propertyType, + @RequestParam(value = "priceType", required = false) LimjangPriceType priceType, + @RequestParam(value = "keyword", required = false) String keyword + ) { + return ApiResponse.onSuccess( + sharedNoteQueryService.findUserSharedNotes(member, noteType, propertyType, priceType, keyword)); + } + + @Operation(summary = "체크리스트 및 상세 후기 API") + @GetMapping("/shared-note/{sharedNoteId}/checklist") + public ApiResponse findChecklistAndReview( + @AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess( + sharedNoteQueryService.findChecklistAndReview(member, sharedNoteId)); + } + + @CrossOrigin + @Operation(summary = "둘러보기 리포트 조회") + @GetMapping("/shared-notes/{sharedNoteId}/report") + public ApiResponse getReport( + @PathVariable(name = "sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess(sharedNoteQueryService.getReportBySharedNoteId(sharedNoteId)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/controller/request/ExploreSortType.java b/src/main/java/umc/th/juinjang/api/note/shared/controller/request/ExploreSortType.java new file mode 100644 index 00000000..e9cb4cda --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/controller/request/ExploreSortType.java @@ -0,0 +1,6 @@ +package umc.th.juinjang.api.note.shared.controller.request; + +public enum ExploreSortType { + POPULAR, + LATEST +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/controller/request/NoteType.java b/src/main/java/umc/th/juinjang/api/note/shared/controller/request/NoteType.java new file mode 100644 index 00000000..60e8d4c6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/controller/request/NoteType.java @@ -0,0 +1,5 @@ +package umc.th.juinjang.api.note.shared.controller.request; + +public enum NoteType { + SHARED, OWNED, LIKED +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/controller/request/SharedNotePostRequest.java b/src/main/java/umc/th/juinjang/api/note/shared/controller/request/SharedNotePostRequest.java new file mode 100644 index 00000000..654ef9eb --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/controller/request/SharedNotePostRequest.java @@ -0,0 +1,14 @@ +package umc.th.juinjang.api.note.shared.controller.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record SharedNotePostRequest( + @NotBlank String buildingName, + @NotBlank String review, + @NotNull Boolean isImageShared, + @NotNull Integer year, + @NotNull Integer month, + @NotBlank String period +) { +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteCommandService.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteCommandService.java new file mode 100644 index 00000000..3123534f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteCommandService.java @@ -0,0 +1,207 @@ +package umc.th.juinjang.api.note.shared.service; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.hibernate.exception.LockAcquisitionException; +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.google.cloud.vision.v1.Likelihood; + +import jakarta.persistence.PessimisticLockException; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.limjang.service.NoteFinder; +import umc.th.juinjang.api.limjang.service.NoteUpdater; +import umc.th.juinjang.api.note.shared.controller.request.SharedNotePostRequest; +import umc.th.juinjang.api.note.shared.service.response.SharedNotePostResponse; +import umc.th.juinjang.api.pencil.service.AcquiredPencilUpdater; +import umc.th.juinjang.api.pencil.service.PurchasedPencilUpdater; +import umc.th.juinjang.api.pencil.service.UsedPencilFinder; +import umc.th.juinjang.api.pencil.service.UsedPencilUpdater; +import umc.th.juinjang.api.pencilAccount.service.PencilAccountFinder; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.SharedNoteHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredType; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.pencil.used.model.Usedtype; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.external.safeSearch.SafeSearchClient; + +@Service +@RequiredArgsConstructor +public class SharedNoteCommandService { + + private final SharedNoteFinder sharedNoteFinder; + private final UsedPencilUpdater usedPencilUpdater; + private final UsedPencilFinder usedPencilFinder; + private final AcquiredPencilUpdater acquiredPencilUpdater; + private final PencilAccountFinder pencilAccountFinder; + private final NoteFinder noteFinder; + private final SharedNoteUpdater sharedNoteUpdater; + private final SafeSearchClient safeSearchClient; + private final NoteUpdater noteUpdater; + private final PurchasedPencilUpdater purchasedPencilUpdater; + + @Transactional + public void createSharedNotePurchase(Member buyer, Long sharedNoteId) { + checkAlreadyPurchase(buyer, sharedNoteId); + + SharedNote sharedNote = sharedNoteFinder.getByIdWhereDeletedAtIsNull(sharedNoteId); + Member seller = sharedNote.getMember(); + Long price = sharedNote.getPrice(); + + try { + PencilAccount buyerAccount = pencilAccountFinder.findByMemberWithLock(buyer); + PencilAccount sellerAccount = pencilAccountFinder.findByMemberWithLock(seller); + + executePayment(buyer, buyerAccount, sellerAccount, price); + + usedPencilUpdater.save(createUsedPencil(buyer, sharedNoteId, sharedNote, buyerAccount)); + acquiredPencilUpdater.save(createAcquiredPencil(sharedNoteId, seller, price, AcquiredType.SOLD)); + + } catch (CannotAcquireLockException | PessimisticLockException | LockAcquisitionException e) { + throw new SharedNoteHandler(ErrorStatus.SHAREDNOTE_DEADLOCK); + } + } + + private void checkAlreadyPurchase(Member buyer, Long sharedNoteId) { + if (usedPencilFinder.existsByMemberAndSharedNoteId(buyer, sharedNoteId)) { + throw new SharedNoteHandler(ErrorStatus.SHAREDNOTE_CONFLICT); + } + } + + public void executePayment(Member buyer, PencilAccount buyerAccount, PencilAccount sellerAccount, Long price) { + long acquiredUsed = Math.min(buyerAccount.getAcquiredBalance(), price); + buyerAccount.decreaseAcquiredBalance(acquiredUsed); + + long unpaidPencil = price - acquiredUsed; + if (unpaidPencil > 0) { + if (buyerAccount.getPurchasedBalance() < unpaidPencil) { + throw new SharedNoteHandler(ErrorStatus.SHAREDNOTE_NOT_ENOUGH_PENCIL); + } + consumePurchasedPencils(buyer, unpaidPencil); + buyerAccount.decreasePurchasedBalance(unpaidPencil); + } + + sellerAccount.increaseAcquiredBalance(price); + } + + private AcquiredPencil createAcquiredPencil(Long sharedNoteId, Member seller, Long price, AcquiredType type) { + String content = "노트 공유 완료!"; + return AcquiredPencil.create(seller, content, sharedNoteId, price, false, type); + } + + private void consumePurchasedPencils(Member buyer, long unpaidPencil) { + List purchasedPencils = purchasedPencilUpdater.findByMemberAndDeliverySuccessRemainQuantityGreaterThanOrderByCreatedAtAsc( + buyer, 0L); + + long remainingToConsume = unpaidPencil; + + for (PurchasedPencil purchasedPencil : purchasedPencils) { + if (remainingToConsume == 0) + break; + + long available = purchasedPencil.getRemainQuantity(); + long toConsume = Math.min(available, remainingToConsume); + + purchasedPencil.decreaseUsedQuantity(toConsume); // remainQuantity -= toConsume + remainingToConsume -= toConsume; + } + + if (remainingToConsume > 0) { + throw new SharedNoteHandler(ErrorStatus.SHAREDNOTE_NOT_ENOUGH_PENCIL); + } + } + + private UsedPencil createUsedPencil(Member member, Long sharedNoteId, SharedNote sharedNote, + PencilAccount buyerAccount) { + return UsedPencil.create(member, sharedNoteId, sharedNote.getPrice(), Usedtype.OWNED, + sharedNote.getBuildingName(), buyerAccount.getTotalBalance()); + } + + @Transactional + public void deleteSharedNote(Member member, Long sharedNoteId, LocalDateTime deletedAt) { + SharedNote sharedNote = sharedNoteFinder.getBySharedNoteIdAndMemberAndDeletedAtIsNull(sharedNoteId, member); + sharedNote.updateDeletedAt(Timestamp.valueOf(deletedAt)); + } + + @Transactional + public SharedNotePostResponse createSharedNote(Member member, Long noteId, SharedNotePostRequest request) { + + Limjang limjang = noteFinder.getNoteByIdWhereDeletedIsFalse(noteId); + Optional latestSharedNote = sharedNoteFinder.findLatestByLimjangId(noteId); + + // 이미 삭제되지 않은 공유글이 있으면 차단 + if (latestSharedNote.isPresent() && latestSharedNote.get().getDeletedAt() == null) { + throw new SharedNoteHandler(ErrorStatus.SHAREDNOTE_ALREADY_EXISTS); + } + + // 최초 공유라면 보상 지급 + boolean isFirstTimeShared = latestSharedNote.isEmpty(); + int reward = calculateReward(limjang, request); + Integer rewardPencilCount = isFirstTimeShared ? reward : 0; + Long price = calculatePrice(reward); + + // 공유 저장 + SharedNote sharedNote = SharedNote.toSharedNote(member, limjang, request, price); + sharedNote = sharedNoteUpdater.save(sharedNote); + + // 보상 처리 + if (rewardPencilCount > 0) { + applyReward(member, limjang, sharedNote.getSharedNoteId(), rewardPencilCount); + } + return new SharedNotePostResponse(sharedNote.getSharedNoteId()); + } + + private int calculateReward(Limjang limjang, SharedNotePostRequest request) { + if (request.isImageShared() == Boolean.TRUE && !limjang.getImageList().isEmpty()) { + validateImagesAreSafe(limjang); + return 7; + } else + return 2; + } + + private Long calculatePrice(int reward) { + if (reward == 7) + return 10L; + else + return 5L; + } + + private void validateImagesAreSafe(Limjang limjang) { + for (var image : limjang.getImageList()) { + boolean safe = safeSearchClient.isSafeImage( + image.getImageUrl(), + Likelihood.POSSIBLE, // adult + Likelihood.LIKELY, // spoof + Likelihood.LIKELY, // medical + Likelihood.POSSIBLE, // violence + Likelihood.LIKELY // racy + ); + if (!safe) { + throw new SharedNoteHandler(ErrorStatus.SHARED_NOT_ALLOWED); + } + } + } + + private void applyReward(Member member, Limjang limjang, Long sharedNoteId, int rewardPencilCount) { + limjang.updateRewardPencil(rewardPencilCount); + noteUpdater.save(limjang); + + PencilAccount pencilAccount = pencilAccountFinder.findByMemberWithLock(member); + pencilAccount.increaseAcquiredBalance(rewardPencilCount); + + acquiredPencilUpdater.save( + createAcquiredPencil(sharedNoteId, member, (long)rewardPencilCount, AcquiredType.NOTE)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java new file mode 100644 index 00000000..bec8ec17 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java @@ -0,0 +1,92 @@ +package umc.th.juinjang.api.note.shared.service; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.note.shared.controller.request.ExploreSortType; +import umc.th.juinjang.api.note.shared.controller.request.NoteType; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.SharedNoteHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.domain.note.shared.repository.SharedNoteRepository; + +@Component +@RequiredArgsConstructor +public class SharedNoteFinder { + + private final SharedNoteRepository sharedNoteRepository; + + public SharedNote getByIdWhereDeletedAtIsNull(Long id) { + return sharedNoteRepository.findBySharedNoteIdAndDeletedAtIsNull(id) + .orElseThrow(() -> new SharedNoteHandler(ErrorStatus.SHAREDNOTE_NOT_FOUND)); + } + + public SharedNote getBySharedNoteIdAndMemberAndDeletedAtIsNull(Long sharedNoteId, Member member) { + return sharedNoteRepository.getBySharedNoteIdAndMemberAndDeletedAtIsNull(sharedNoteId, member).orElseThrow( + () -> new SharedNoteHandler(ErrorStatus.SHAREDNOTE_NOT_FOUND)); + } + + SharedNote findByIdWithNoteAndAddress(Long id) { + return sharedNoteRepository.findByIdWithNoteAndAddress(id) + .orElseThrow(() -> new SharedNoteHandler(ErrorStatus.SHAREDNOTE_NOT_FOUND)); + } + + Optional findById(Long id) { + return sharedNoteRepository.findById(id); + } + + public Long getLikedNoteById(Long id) { + return sharedNoteRepository.getLikeCountById(id); + } + + public Optional findLatestByLimjangId(Long noteId) { + return sharedNoteRepository.findTop1ByLimjang_LimjangIdOrderByCreatedAtDesc(noteId); + } + + Page findSharedNoteInExployer(List code, ExploreSortType sort, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword, Pageable pageable) { + return sharedNoteRepository.findSharedNoteInExployer(code, sort, propertyType, priceType, + keyword, pageable); + } + + public List findUserSharedNotes(Member member, NoteType noteType, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword, List filterIds) { + return sharedNoteRepository.findUserSharedNotes(member, noteType, propertyType, priceType, keyword, filterIds); + } + + public Map findAllIdAndViewCountById(List ids) { + return sharedNoteRepository.findAllViewCountById(ids).stream() + .collect(Collectors.toMap( + row -> (Long)row[0], + row -> (Long)row[1] + )); + } + + public Long findViewCountById(Long id) { + return sharedNoteRepository.findViewCountById(id); + } + + public boolean existsByDeletedAtIsNullAndLimjang(Limjang limjang) { + return sharedNoteRepository.existsByDeletedAtIsNullAndLimjang(limjang); + } + + public Set findLimjangIdsByDeletedAtIsNullAndLimjang(List notes) { + return sharedNoteRepository.findLimjangIdsByDeletedAtIsNullAndLimjang(notes); + } + + public Set findLimjangIdsByDeletedAtIsNotNullAndLimjang(List notes) { + return sharedNoteRepository.findLimjangIdsByDeletedAtIsNotNullAndLimjang(notes); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java new file mode 100644 index 00000000..55a34169 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteQueryService.java @@ -0,0 +1,209 @@ +package umc.th.juinjang.api.note.shared.service; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.checklist.service.ChecklistAnswerFinder; +import umc.th.juinjang.api.checklist.service.ReportFinder; +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; +import umc.th.juinjang.api.checklist.service.response.ReportGetResponse; +import umc.th.juinjang.api.checklist.service.response.ReportWithLimjangResponseDTO; +import umc.th.juinjang.api.limjang.service.response.LimjangDetailGetResponse; +import umc.th.juinjang.api.note.liked.service.LikedNoteFinder; +import umc.th.juinjang.api.note.shared.controller.request.ExploreSortType; +import umc.th.juinjang.api.note.shared.controller.request.NoteType; +import umc.th.juinjang.api.note.shared.service.response.SharedNoteCheckListAndReviewResponse; +import umc.th.juinjang.api.note.shared.service.response.SharedNoteExploreGetResponse; +import umc.th.juinjang.api.note.shared.service.response.SharedNoteGetResponse; +import umc.th.juinjang.api.note.shared.service.response.UserSharedNotesGetResponse; +import umc.th.juinjang.api.pencil.service.UsedPencilFinder; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.SharedNoteHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.report.model.Report; + +@Service +@Slf4j +@RequiredArgsConstructor +public class SharedNoteQueryService { + + private final UsedPencilFinder usedPencilFinder; + private final SharedNoteFinder sharedNoteFinder; + private final LikedNoteFinder likedNoteFinder; + private final ChecklistAnswerFinder checklistAnswerFinder; + private final ViewCountService viewCountService; + private final ReportFinder reportFinder; + + @Transactional + public SharedNoteGetResponse findSharedNote(Member member, Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.findByIdWithNoteAndAddress(sharedNoteId); + Limjang limjang = sharedNote.getLimjang(); + + boolean isBuyerOrOwner = getIsBuyerOrOwner(member, sharedNote); + + long viewCount = viewCountService.getViewCount(member, sharedNote); + + Integer countBuyer = makeBuyerCount(usedPencilFinder.countBySharedNoteId(sharedNoteId)); + boolean isLiked = likedNoteFinder.existsByMemberAndSharedNote(member, sharedNote); + + if (isBuyerOrOwner) { + return SharedNoteGetResponse.ofPurchased(isBuyerOrOwner, limjang, limjang.getAddressEntity(), sharedNote, + sharedNote.getMember(), countBuyer, isLiked, viewCount); + } else { + return SharedNoteGetResponse.ofNotPurchased(isBuyerOrOwner, limjang, limjang.getAddressEntity(), sharedNote, + sharedNote.getMember(), countBuyer, isLiked, viewCount); + } + } + + private boolean getIsBuyerOrOwner(Member requestMember, SharedNote sharedNote) { + return usedPencilFinder.existsByMemberAndSharedNoteId(requestMember, sharedNote.getSharedNoteId()) || + sharedNote.getMember().getMemberId().equals(requestMember.getMemberId()); + } + + private Integer makeBuyerCount(int count) { + if (count >= 100) { + return 100; + } else if (count >= 50) { + return 50; + } else if (count >= 30) { + return 30; + } else if (count >= 10) { + return 10; + } + return null; + } + + @Transactional(readOnly = true) + public SharedNoteExploreGetResponse findExploreSharedNote(Member member, List code, + ExploreSortType sort, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword, + Pageable pageable) { + + Page pages = sharedNoteFinder.findSharedNoteInExployer(code, sort, propertyType, priceType, keyword, + pageable); + List sharedNotes = pages.getContent(); + List ids = sharedNotes.stream().map(SharedNote::getSharedNoteId).toList(); + + Set likedNoteIds = new HashSet<>(likedNoteFinder.findLikedSharedNoteIds(member, sharedNotes)); + Set purchasedIds = new HashSet<>(usedPencilFinder.findByMemberInSharedNoteIdsAndTypeIsOwned(member, ids)); + + Map viewcountMap = mapIdsAndViewcount(sharedNotes); + return SharedNoteExploreGetResponse.of(pages.getTotalElements(), sharedNotes, purchasedIds, likedNoteIds, + viewcountMap, member.getMemberId()); + } + + private Map mapIdsAndViewcount(List sharedNotes) { + return sharedNotes.stream().collect(Collectors.toMap( + SharedNote::getSharedNoteId, + SharedNote::getViewCount + )); + } + + public UserSharedNotesGetResponse findUserSharedNotes(Member member, NoteType noteType, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword) { + + switch (noteType) { + case LIKED -> { + return getUserLikedSharedNotes(member, propertyType, priceType, keyword); + } + case SHARED -> { + return getUserSharedNotes(member, noteType, propertyType, priceType, keyword); + } + case OWNED -> { + return getUserOwnedSharedNotes(member, noteType, propertyType, priceType, keyword); + } + default -> throw new SharedNoteHandler(ErrorStatus.SHAREDNOTE_TYPE_ERROR); + } + } + + private UserSharedNotesGetResponse getUserOwnedSharedNotes(Member member, NoteType noteType, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword) { + + List usedPencils = usedPencilFinder.findAllByMemberAndTypeIsOwnedOrderByCreatedAtDesc(member); + List sharedNoteIds = usedPencils.stream().map(UsedPencil::getSharedNoteId).toList(); + + List sharedNotes = sharedNoteFinder.findUserSharedNotes(member, noteType, propertyType, priceType, + keyword, sharedNoteIds); + List sortedSharedNotes = sortByUsedPencilCreatedAt(sharedNoteIds, sharedNotes); + + Map viewcountMap = mapIdsAndViewcount(sharedNotes); + Set likedNoteIds = new HashSet<>(likedNoteFinder.findLikedSharedNoteIds(member, sharedNotes)); + + return UserSharedNotesGetResponse.ofOwned(sortedSharedNotes, likedNoteIds, viewcountMap); + } + + private List sortByUsedPencilCreatedAt(List sharedNoteIds, List sharedNotes) { + Map orderMap = IntStream.range(0, sharedNoteIds.size()) + .boxed() + .collect(Collectors.toMap(sharedNoteIds::get, i -> i)); + + return sharedNotes.stream() + .sorted(Comparator.comparingInt(note -> orderMap.get(note.getSharedNoteId()))) + .toList(); + } + + private UserSharedNotesGetResponse getUserSharedNotes(Member member, NoteType noteType, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword) { + List sharedNotes = sharedNoteFinder.findUserSharedNotes(member, noteType, propertyType, priceType, + keyword, List.of()); + Map viewcountMap = mapIdsAndViewcount(sharedNotes); + + Set likedNoteIds = new HashSet<>(likedNoteFinder.findLikedSharedNoteIds(member, sharedNotes)); + return UserSharedNotesGetResponse.ofShared(member, sharedNotes, likedNoteIds, viewcountMap); + } + + private UserSharedNotesGetResponse getUserLikedSharedNotes(Member member, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword) { + + List userLikedNotes = likedNoteFinder.findAllByMemberAndDynamic(member, propertyType, + priceType, keyword); + List sharedNotes = userLikedNotes.stream().map(LikedNote::getSharedNote).toList(); + + Set purchasedIds = new HashSet<>(usedPencilFinder.findByMemberInSharedNoteIdsAndTypeIsOwned(member, + sharedNotes.stream().map(SharedNote::getSharedNoteId).toList())); + Map viewcountMap = mapIdsAndViewcount(sharedNotes); + + return UserSharedNotesGetResponse.ofLiked(sharedNotes, purchasedIds, viewcountMap, member.getMemberId()); + } + + @Transactional(readOnly = true) + public SharedNoteCheckListAndReviewResponse findChecklistAndReview(Member member, Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.findByIdWithNoteAndAddress(sharedNoteId); + Limjang note = sharedNote.getLimjang(); + List answers = checklistAnswerFinder.findByLimjangId( + note.getLimjangId()); + + Report report = reportFinder.findReportByNote(note); + + boolean isOwned = usedPencilFinder.existsByMemberAndSharedNoteId(member, sharedNoteId); + // 구매했다면 review 포함, 아니면 null + String review = isOwned ? sharedNote.getReview() : null; + Float totalRate = isOwned ? report.getTotalRate() : null; + return new SharedNoteCheckListAndReviewResponse(review, totalRate, answers); + } + + public ReportWithLimjangResponseDTO getReportBySharedNoteId(Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.findByIdWithNoteAndAddress(sharedNoteId); + Limjang note = sharedNote.getLimjang(); + Report report = reportFinder.findReportByNote(note); + return new ReportWithLimjangResponseDTO(ReportGetResponse.of(report), LimjangDetailGetResponse.of(note)); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java new file mode 100644 index 00000000..461fe797 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java @@ -0,0 +1,30 @@ +package umc.th.juinjang.api.note.shared.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.domain.note.shared.repository.SharedNoteRepository; + +@Component +@RequiredArgsConstructor +public class SharedNoteUpdater { + + private final SharedNoteRepository sharedNoteRepository; + + public void updateViewCount(long sharedNoteId) { + sharedNoteRepository.incrementViewCount(sharedNoteId); + } + + public void incrementLikedCountById(Long sharedNoteId) { + sharedNoteRepository.incrementLikedCountById(sharedNoteId); + } + + public void decrementLikedCountById(Long sharedNoteId) { + sharedNoteRepository.decrementLikedCountById(sharedNoteId); + } + + public SharedNote save(SharedNote sharedNote) { + return sharedNoteRepository.save(sharedNote); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java b/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java new file mode 100644 index 00000000..1bd744a6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/ViewCountService.java @@ -0,0 +1,59 @@ +package umc.th.juinjang.api.note.shared.service; + +import java.time.Duration; + +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.common.redis.RedisKeyFactory; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.event.publisher.ApplicationRewardViewCountPublisherAdapter; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ViewCountService { + + private final RedisTemplate redisTemplate; + private final SharedNoteFinder sharedNoteFinder; + private final SharedNoteUpdater sharedNoteUpdater; + private final ApplicationRewardViewCountPublisherAdapter applicationRewardViewCountPublisherAdapter; + + public void recordViewerHistory(long memberId, long sharedNoteId) { + try { + redisTemplate.opsForValue() + .setIfAbsent(RedisKeyFactory.viewHistoryKey(sharedNoteId, memberId), "1", Duration.ofHours(3)); + } catch (RedisConnectionFailureException | RedisSystemException e) { + log.error("Redis 연결 실패 - 중복 조회 기록 불가, sharedNoteId={}, memberId={}", sharedNoteId, memberId, e); + } + } + + public boolean isDuplicate(long memberId, long sharedNoteId) { + try { + return Boolean.TRUE.equals(redisTemplate.hasKey(RedisKeyFactory.viewHistoryKey(sharedNoteId, memberId))); + } catch (RedisConnectionFailureException | RedisSystemException e) { + log.error("Redis 장애로 조회 기록 확인 실패. sharedNoteId={}, memberId={}", sharedNoteId, memberId, e); + return false; + } + } + + public long getViewCount(Member member, SharedNote sharedNote) { + long sharedNoteId = sharedNote.getSharedNoteId(); + long viewCount = sharedNoteFinder.findViewCountById(sharedNoteId); + + if (!isDuplicate(member.getMemberId(), sharedNoteId) && sharedNote.getDeletedAt() == null) { + sharedNoteUpdater.updateViewCount(sharedNoteId); + viewCount++; + recordViewerHistory(member.getMemberId(), sharedNoteId); + + applicationRewardViewCountPublisherAdapter.checkViewCountRewardPolicy(sharedNote.getMember(), + sharedNote.getSharedNoteId(), viewCount); + } + return viewCount; + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteCheckListAndReviewResponse.java b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteCheckListAndReviewResponse.java new file mode 100644 index 00000000..e83a17ad --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteCheckListAndReviewResponse.java @@ -0,0 +1,12 @@ +package umc.th.juinjang.api.note.shared.service.response; + +import java.util.List; + +import umc.th.juinjang.api.checklist.service.response.ChecklistAnswerResponseDTO; + +public record SharedNoteCheckListAndReviewResponse( + String review, + Float totalRate, + List checklistAnswers +) { +} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteExploreGetResponse.java b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteExploreGetResponse.java new file mode 100644 index 00000000..70c3452a --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteExploreGetResponse.java @@ -0,0 +1,77 @@ +package umc.th.juinjang.api.note.shared.service.response; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import umc.th.juinjang.api.note.shared.service.util.SharedNotesTimeAgoFormatter; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +public record SharedNoteExploreGetResponse( + long totalResults, + List notes +) { + + public static SharedNoteExploreGetResponse of(long totalResults, List sharedNotes, + Set isPurchaseMap, Set likedNotes, Map viewCountMap, long requestMemberId + ) { + return new SharedNoteExploreGetResponse(totalResults, + sharedNotes.stream() + .map(it -> SharedNoteExploreResponse.of( + it, + it.getLimjang(), + isPurchaseMap.contains(it.getSharedNoteId()) || it.getMember().getMemberId() == requestMemberId, + likedNotes.contains(it.getSharedNoteId()), + viewCountMap.get(it.getSharedNoteId()), + it.getMember())) + .toList()); + } +} + +record SharedNoteExploreResponse( + Long sharedNoteId, + LimjangPropertyType propertyType, + LimjangPriceType priceType, + String buildingName, + String imageUrl, + Boolean isPurchase, + Boolean isLiked, + String rate, + String price, + String monthlyRent, + Integer pyong, + String floor, + String address, + String ownerImageUrl, + String ownerNickname, + String timeAge, + Long viewCount +) { + + public static SharedNoteExploreResponse of(SharedNote sharedNote, Limjang note, boolean isPurchase, boolean isLiked, + Long viewCount, Member member) { + return new SharedNoteExploreResponse( + sharedNote.getSharedNoteId(), + note.getPropertyType(), + note.getPriceType(), + sharedNote.getBuildingName(), + sharedNote.isImageShared() ? note.getDefaultImage() : null, + isPurchase, + isLiked, + note.getReport() == null ? null : note.getReport().getTotalRate().toString(), + note.getLimjangPrice().getPrice(note.getPriceType(), note.getPurpose()), + note.getPriceType() == LimjangPriceType.MONTHLY_RENT ? note.getLimjangPrice().getMonthlyRent() : null, + note.getPyong(), + note.getFloor(), + note.getAddressEntity().getShortAddress(), + member.getImageUrl(), + member.getNickname(), + SharedNotesTimeAgoFormatter.getTimeAge(sharedNote.getCreatedAt()), + viewCount + ); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java new file mode 100644 index 00000000..44079de5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java @@ -0,0 +1,131 @@ +package umc.th.juinjang.api.note.shared.service.response; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Address; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +public record SharedNoteGetResponse( + boolean isBuyer, + boolean isImageShared, + Long requiredPencils, + Integer imageCount, + Integer checkedCount, + Integer reviewLength, + String buildingName, + LimjangPurpose limjangPurpose, + LimjangPropertyType propertyType, + LimjangPriceType priceType, + Integer buyerCount, + List images, + String address, + String addressShort, + String price, + String monthlyRent, + boolean isLiked, + Long likedCount, + String period, + String updatedAt, + Long viewCount, + String floor, + int pyong, + String ownerProfileUrl, + String ownerNickname, + String ownerProfileBio +) { + public static SharedNoteGetResponse ofNotPurchased( + boolean isBuyer, + Limjang limjang, + Address address, + SharedNote sharedNote, + Member member, + Integer buyerCount, + boolean isLiked, + Long viewCount) { + return new SharedNoteGetResponse( + isBuyer, + sharedNote.isImageShared(), + sharedNote.getPrice(), + limjang.getImageList().size(), + limjang.getAnswerList().size(), + sharedNote.getReview().length(), + sharedNote.getBuildingName(), + limjang.getPurpose(), + limjang.getPropertyType(), + limjang.getPriceType(), + buyerCount, + findImagesUrlBySharingStatus(sharedNote.isImageShared(), limjang, 2), + address.getRoadAddress(), + address.getShortAddress(), + limjang.getLimjangPrice().getPrice(limjang.getPriceType(), limjang.getPurpose()), + limjang.getPriceType() == LimjangPriceType.MONTHLY_RENT ? limjang.getLimjangPrice().getMonthlyRent() : + null, + isLiked, + sharedNote.getLikeCount(), + sharedNote.getPullPeriod(), + null, + viewCount, + limjang.getFloor(), + limjang.getPyong(), + member.getImageUrl(), + member.getNickname(), + member.getIntroduction() + + ); + } + + public static SharedNoteGetResponse ofPurchased( + boolean isBuyer, + Limjang limjang, + Address address, + SharedNote sharedNote, + Member member, + Integer buyerCount, + boolean isLiked, + Long viewCount) { + return new SharedNoteGetResponse( + isBuyer, + sharedNote.isImageShared(), + null, + null, + null, + null, + sharedNote.getBuildingName(), + limjang.getPurpose(), + limjang.getPropertyType(), + limjang.getPriceType(), + buyerCount, + findImagesUrlBySharingStatus(sharedNote.isImageShared(), limjang, 3), + address.getRoadAddress(), + address.getShortAddress(), + limjang.getLimjangPrice().getPrice(limjang.getPriceType(), limjang.getPurpose()), + limjang.getPriceType() == LimjangPriceType.MONTHLY_RENT ? limjang.getLimjangPrice().getMonthlyRent() : + null, + isLiked, + sharedNote.getLikeCount(), + sharedNote.getPullPeriod(), + sharedNote.getUpdatedAt().format(DateTimeFormatter.ofPattern("yy.MM.dd")), + viewCount, + limjang.getFloor(), + limjang.getPyong(), + member.getImageUrl(), + member.getNickname(), + member.getIntroduction() + + ); + } + + private static List findImagesUrlBySharingStatus(boolean isImageShared, Limjang limjang, int maxSize) { + if (isImageShared) { + return limjang.getImageList().stream().map(Image::getImageUrl).limit(maxSize).toList(); + } + return List.of(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNotePostResponse.java b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNotePostResponse.java new file mode 100644 index 00000000..9967be76 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNotePostResponse.java @@ -0,0 +1,6 @@ +package umc.th.juinjang.api.note.shared.service.response; + +public record SharedNotePostResponse( + Long sharedNoteId +) { +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/response/UserSharedNotesGetResponse.java b/src/main/java/umc/th/juinjang/api/note/shared/service/response/UserSharedNotesGetResponse.java new file mode 100644 index 00000000..133a90d4 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/response/UserSharedNotesGetResponse.java @@ -0,0 +1,97 @@ +package umc.th.juinjang.api.note.shared.service.response; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import umc.th.juinjang.api.note.shared.service.util.SharedNotesTimeAgoFormatter; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +public record UserSharedNotesGetResponse( + List notes + +) { + + public static UserSharedNotesGetResponse ofLiked(List sharedNotes, Set isPurchaseMap, + Map viewCountMap, long requestMemberId) { + return new UserSharedNotesGetResponse(sharedNotes.stream().map(it -> UsersSharedNoteResponse.of( + it, + it.getLimjang(), + isPurchaseMap.contains(it.getSharedNoteId()) || it.getMember().getMemberId() == requestMemberId, + true, + viewCountMap.get(it.getSharedNoteId()), + it.getMember() + )).toList()); + } + + public static UserSharedNotesGetResponse ofShared(Member member, List sharedNotes, + Set likedNotes, Map viewCountMap) { + return new UserSharedNotesGetResponse(sharedNotes.stream().map(it -> UsersSharedNoteResponse.of( + it, + it.getLimjang(), + true, + likedNotes.contains(it.getSharedNoteId()), + viewCountMap.get(it.getSharedNoteId()), + member + )).toList()); + } + + public static UserSharedNotesGetResponse ofOwned(List sharedNotes, + Set likedNotes, Map viewCountMap) { + return new UserSharedNotesGetResponse(sharedNotes.stream().map(it -> UsersSharedNoteResponse.of( + it, + it.getLimjang(), + true, + likedNotes.contains(it.getSharedNoteId()), + viewCountMap.get(it.getSharedNoteId()), + it.getMember() + )).toList()); + } +} + +record UsersSharedNoteResponse( + Long sharedNoteId, + LimjangPropertyType propertyType, + LimjangPriceType priceType, + String buildingName, + String imageUrl, + Boolean isPurchase, + Boolean isLiked, + String rate, + String price, + String monthlyRent, + Integer pyong, + String floor, + String address, + String ownerImageUrl, + String ownerNickname, + String timeAge, + Long viewCount) { + + static UsersSharedNoteResponse of(SharedNote sharedNote, Limjang note, boolean isPurchase, boolean isLiked, + Long viewCount, Member member) { + return new UsersSharedNoteResponse( + sharedNote.getSharedNoteId(), + note.getPropertyType(), + note.getPriceType(), + sharedNote.getBuildingName(), + sharedNote.isImageShared() ? note.getDefaultImage() : null, + isPurchase, + isLiked, + note.getReport() == null ? null : note.getReport().getTotalRate().toString(), + note.getLimjangPrice().getPrice(note.getPriceType(), note.getPurpose()), + note.getPriceType() == LimjangPriceType.MONTHLY_RENT ? note.getLimjangPrice().getMonthlyRent() : null, + note.getPyong(), + note.getFloor(), + note.getAddressEntity().getShortAddress(), + member.getImageUrl(), + member.getNickname(), + SharedNotesTimeAgoFormatter.getTimeAge(sharedNote.getCreatedAt()), + viewCount + ); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/util/SharedNotesTimeAgoFormatter.java b/src/main/java/umc/th/juinjang/api/note/shared/service/util/SharedNotesTimeAgoFormatter.java new file mode 100644 index 00000000..bb1ea10a --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/util/SharedNotesTimeAgoFormatter.java @@ -0,0 +1,36 @@ +package umc.th.juinjang.api.note.shared.service.util; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Period; + +public class SharedNotesTimeAgoFormatter { + public static String getTimeAge(LocalDateTime createdAt) { + LocalDateTime now = LocalDateTime.now(); + Duration duration = Duration.between(createdAt, now); + long seconds = duration.getSeconds(); + + if (seconds < 600) { + return "방금 전"; + } else if (seconds < 3600) { + long minutes = seconds / 60; + return minutes + "분 전"; + } else if (seconds < 86400) { + long hours = seconds / 3600; + return hours + "시간 전"; + } + + LocalDate createdDate = createdAt.toLocalDate(); + LocalDate currentDate = now.toLocalDate(); + Period period = Period.between(createdDate, currentDate); + + if (period.getYears() >= 1) { + return period.getYears() + "년 전"; + } else if (period.getMonths() >= 1) { + return period.getMonths() + "개월 전"; + } else { + return period.getDays() + "일 전"; + } + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java b/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java new file mode 100644 index 00000000..103d049c --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/controller/PencilController.java @@ -0,0 +1,85 @@ +package umc.th.juinjang.api.pencil.controller; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest; +import umc.th.juinjang.api.pencil.service.PencilCommandService; +import umc.th.juinjang.api.pencil.service.PencilQueryService; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadResponse; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilReadStatusResponse; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse; +import umc.th.juinjang.api.pencil.service.response.AppleIAPPurchaseResponse; +import umc.th.juinjang.api.pencil.service.response.PurchasedPencilResponse; +import umc.th.juinjang.api.pencil.service.response.UsedPencilResponse; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2/pencil") +@RequiredArgsConstructor +public class PencilController { + + private final PencilQueryService pencilQueryService; + private final PencilCommandService pencilCommandService; + + @Operation(summary = "얻은 연필 목록을 불러온다.") + @GetMapping("/acquired") + public ApiResponse> getAcquiredPencilHistory(@AuthenticationPrincipal Member member) { + return ApiResponse.onSuccess(pencilQueryService.getAcquiredPencils(member)); + } + + @Operation(summary = "얻은 연필 목록에서 읽음 처리를 진행한다.") + @PatchMapping("/acquired/{acquiredPencilId}/read") + public ApiResponse markAcquiredPencilAsRead( + @PathVariable Long acquiredPencilId, + @AuthenticationPrincipal Member member) { + return ApiResponse.onSuccess( + AcquiredPencilReadResponse.of(pencilCommandService.markAcquiredPencilAsRead(acquiredPencilId), + pencilQueryService.isAcquiredPencilReadStatus(member))); + } + + @Operation(summary = "얻은 연필 목록에서 읽지 않은 항목이 존재하는 여부를 확인한다.") + @GetMapping("/acquired/is-total-read") + public ApiResponse isAcquiredPencilReadStatus( + @AuthenticationPrincipal Member member + ) { + return ApiResponse.onSuccess( + AcquiredPencilReadStatusResponse.of(pencilQueryService.isAcquiredPencilReadStatus(member))); + } + + @Operation(summary = "구매한 연필 목록을 불러온다") + @GetMapping("/purchased") + public ApiResponse> getPurchasedPencilHistory( + @AuthenticationPrincipal Member member) { + return ApiResponse.onSuccess(pencilQueryService.getPurchasedPencils(member)); + } + + @Operation(summary = "사용한 연필 목록을 불러온다") + @GetMapping("/used") + public ApiResponse> getUsedPencilHistory( + @AuthenticationPrincipal Member member) { + return ApiResponse.onSuccess(pencilQueryService.getUsedPencils(member)); + } + + @Operation(summary = "애플 인앱 결제를 통해 연필을 구매한다") + @PostMapping("/purchase/apple") + public ApiResponse purchasePencil( + @AuthenticationPrincipal Member member, + @RequestBody AppleIAPPurchaseRequest request + ) { + return ApiResponse.onSuccess( + pencilCommandService.processAppleIAPPurchase(request, member, LocalDateTime.now())); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/controller/request/AppleIAPPurchaseRequest.java b/src/main/java/umc/th/juinjang/api/pencil/controller/request/AppleIAPPurchaseRequest.java new file mode 100644 index 00000000..ec2a99c4 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/controller/request/AppleIAPPurchaseRequest.java @@ -0,0 +1,51 @@ +package umc.th.juinjang.api.pencil.controller.request; + +import java.util.UUID; + +import lombok.Builder; +import lombok.Getter; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; + +@Getter +public class AppleIAPPurchaseRequest { + private String transactionId; + private UUID appAccountToken; + private Long pencilQuantity; + private Long price; + private String productId; + private Integer playTime; + + @Builder + private AppleIAPPurchaseRequest(String transactionId, UUID appAccountToken, Long pencilQuantity, Long price, + String productId , Integer playTime) { + this.transactionId = transactionId; + this.appAccountToken = appAccountToken; + this.pencilQuantity = pencilQuantity; + this.price = price; + this.productId = productId; + this.playTime = playTime; + } + + public static AppleIAPPurchaseRequest of(String transactionId, UUID appAccountToken, Long pencilQuantity, + Long price, String productId, Integer playTime) { + return AppleIAPPurchaseRequest.builder() + .transactionId(transactionId) + .appAccountToken(appAccountToken) + .pencilQuantity(pencilQuantity) + .price(price) + .productId(productId) + .playTime(playTime) + .build(); + } + + public static AppleIAPPurchaseRequest ofRetry(PurchasedPencil pencil) { + return AppleIAPPurchaseRequest.builder() + .transactionId(pencil.getTransactionId()) + .pencilQuantity(pencil.getPurchaseQuantity()) + .price(pencil.getPrice()) + .playTime(pencil.getPlayTime()) + .appAccountToken(pencil.getAppAccountToken()) + .build(); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java b/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java new file mode 100644 index 00000000..f168d938 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilFinder.java @@ -0,0 +1,34 @@ +package umc.th.juinjang.api.pencil.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; +import umc.th.juinjang.domain.pencil.acquired.repository.AcquiredPencilRepository; + +@Component +@RequiredArgsConstructor +public class AcquiredPencilFinder { + + private final AcquiredPencilRepository acquiredPencilRepository; + + public List findAllByMemberOrderByCreatedAtDesc(Member member) { + return acquiredPencilRepository.findAllByMemberWithBuildingNameOrderByCreatedAtDesc(member); + } + + public boolean existsByMemberAndIsReadFalse(Member member) { + return acquiredPencilRepository.existsByMemberAndIsReadFalse(member); + } + + public AcquiredPencil findById(Long id) { + return acquiredPencilRepository.findById(id).orElse(null); + } + + public boolean existsByMember(Member member) { + return acquiredPencilRepository.existsByMember(member); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilUpdater.java b/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilUpdater.java new file mode 100644 index 00000000..58860a56 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/AcquiredPencilUpdater.java @@ -0,0 +1,19 @@ +package umc.th.juinjang.api.pencil.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; +import umc.th.juinjang.domain.pencil.acquired.repository.AcquiredPencilRepository; + +@Component +@RequiredArgsConstructor +public class AcquiredPencilUpdater { + + private final AcquiredPencilRepository acquiredPencilRepository; + + public void save(AcquiredPencil acquiredPencil) { + acquiredPencilRepository.save(acquiredPencil); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java b/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java new file mode 100644 index 00000000..d45d7b23 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/PencilCommandService.java @@ -0,0 +1,189 @@ +package umc.th.juinjang.api.pencil.service; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.apple.service.AppleService; +import umc.th.juinjang.api.apple.service.command.AppleTransactionVerifyCommand; +import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest; +import umc.th.juinjang.api.pencil.service.response.AppleIAPPurchaseResponse; +import umc.th.juinjang.api.pencil.service.response.VerificationResult; +import umc.th.juinjang.api.pencilAccount.service.PencilAccountFinder; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.event.publisher.PaymentEventPublisher; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PencilCommandService { + + private final AppleService appleService; + + private final PurchasedPencilUpdater purchasedPencilUpdater; + private final PurchasedPencilFinder purchasedPencilFinder; + private final AcquiredPencilFinder acquiredPencilFinder; + private final PencilAccountFinder pencilAccountFinder; + private final PaymentEventPublisher paymentEventPublisher; + + @Transactional + public Boolean markAcquiredPencilAsRead(Long acquiredPencilId) { + AcquiredPencil acquiredPencil = acquiredPencilFinder.findById(acquiredPencilId); + + if (acquiredPencil == null) { + throw new EntityNotFoundException("AcquiredPencil not found with id: " + acquiredPencilId); + } + + acquiredPencil.updateIsReadAsTrue(); + return true; + } + + @Transactional + public AppleIAPPurchaseResponse processAppleIAPPurchase(AppleIAPPurchaseRequest request, Member member, + LocalDateTime now) { + String transactionId = request.getTransactionId(); + Long purchaseQuantity = request.getPencilQuantity(); + Optional existing = purchasedPencilFinder.findByTransactionIdAndMember(transactionId, member); + + if (existing.isEmpty()) { + // 기존 트랜잭션이 없는 경우 + log.info("기존 트랜잭션이 없습니다. {}", transactionId); + return validateAndCommitApplePurchase(request, member, now); + } + + PurchasedPencil pencil = existing.get(); + TransactionStatus status = pencil.getTransactionStatus(); + PencilAccount buyer = pencilAccountFinder.findByMember(member); + + if (status == TransactionStatus.SUCCESS) { + // 트랜잭션이 정상적으로 성공된 기록이 있는 경우 + return AppleIAPPurchaseResponse.ofSuccess(transactionId, purchaseQuantity, buyer.getTotalBalance()); + } + + PurchasedPencil newPencil = retryPurchasedPencil(request, pencil, member); // 실패 재시도 처리 + return AppleIAPPurchaseResponse.of(transactionId, newPencil.getTransactionStatus(), purchaseQuantity, + buyer.getTotalBalance()); + } + + @Transactional + public AppleIAPPurchaseResponse validateAndCommitApplePurchase(AppleIAPPurchaseRequest request, Member member, + LocalDateTime now) { + String transactionId = request.getTransactionId(); + + VerificationResult verificationResult = appleService.verifyAppleTransaction( + AppleTransactionVerifyCommand.fromRequest(request)); + + if (VerificationResult.isSuccess(verificationResult)) { + // 성공 시, DB에 저장 + handleSuccessfulApplePurchase(request, member, now); + + paymentEventPublisher.publishPaymentEvent(member, request.getPrice(), request.getPencilQuantity(), + TransactionStatus.SUCCESS); + PencilAccount buyer = pencilAccountFinder.findByMember(member); + return AppleIAPPurchaseResponse.ofSuccess(transactionId, request.getPencilQuantity(), + buyer.getTotalBalance()); + } else { + // 실패 시, DB에 저장 + handleFailureApplePurchase(request, member, now); + + paymentEventPublisher.publishPaymentEvent(member, request.getPrice(), request.getPencilQuantity(), + TransactionStatus.VALIDATION_FAILED); + return AppleIAPPurchaseResponse.ofValidationFailure(transactionId); + } + } + + @Transactional + public void handleSuccessfulApplePurchase(AppleIAPPurchaseRequest request, Member member, LocalDateTime now) { + String transactionId = request.getTransactionId(); + Long pencilAmount = request.getPencilQuantity(); + + String title = createTitle(pencilAmount); + PencilAccount buyer = pencilAccountFinder.findByMemberWithLock(member); + buyer.increasePurchasedBalance(pencilAmount); + + purchasedPencilUpdater.save( + PurchasedPencil.successOf(member, title, pencilAmount, buyer.getTotalBalance(), request.getPrice(), + request.getPlayTime(), transactionId, request.getAppAccountToken(), now)); + + } + + @Transactional + public void handleFailureApplePurchase(AppleIAPPurchaseRequest request, Member member, LocalDateTime now) { + String transactionId = request.getTransactionId(); + Long pencilAmount = request.getPencilQuantity(); + + String title = createTitle(pencilAmount); + purchasedPencilUpdater.save( + PurchasedPencil.failedDueToValidation(member, title, pencilAmount, request.getPrice(), + request.getPlayTime(), transactionId, request.getAppAccountToken(), now)); + } + + @Transactional + public PurchasedPencil retryPurchasedPencil(AppleIAPPurchaseRequest request, PurchasedPencil pencil, + Member member) { + if (pencil.getRetryCount() >= 3) { // 재시도 횟수가 3회 이상일 경우 실패로 처리 + return pencil; + } + + VerificationResult verificationResult = appleService.verifyAppleTransaction( + AppleTransactionVerifyCommand.fromRequest(request) + ); + + if (VerificationResult.isSuccess(verificationResult)) { + pencil.markAsSuccess(); + pencil.updateRetryCount(pencil.getRetryCount() + 1); + + pencilAccountFinder.findByMemberWithLock(member) + .increasePurchasedBalance(pencil.getPurchaseQuantity()); + } + + return pencil; + } + + private String createTitle(Long pencilAmount) { + return String.format("연필 %d개 구매", pencilAmount); + } + + @Transactional + public void handleRefundPurchase(String transactionId) { + PurchasedPencil pencil = purchasedPencilFinder.findByTransactionId(transactionId) + .orElseThrow( + () -> new EntityNotFoundException("PurchasedPencil not found with transactionId: " + transactionId)); + + log.info("Refund processed for transactionId: {}", transactionId); + + pencil.markAsRefund(); + + PencilAccount buyerAccount = pencilAccountFinder.findByMemberWithLock(pencil.getMember()); + executeRefund(buyerAccount, pencil.getPurchaseQuantity(), pencil.getPrice()); + + paymentEventPublisher.publishPaymentEvent(pencil.getMember(), pencil.getPrice(), pencil.getPurchaseQuantity(), + TransactionStatus.REFUNDED); + } + + public void executeRefund(PencilAccount buyerAccount, long pencilQuantity, long price) { + long purchasedToUse = Math.min(buyerAccount.getPurchasedBalance(), pencilQuantity); + buyerAccount.decreasePurchasedBalance(purchasedToUse); + + long remaining = pencilQuantity - purchasedToUse; + long acquiredToUse = Math.min(buyerAccount.getAcquiredBalance(), remaining); + buyerAccount.decreaseAcquiredBalance(acquiredToUse); + + buyerAccount.increaseTotalRefundAmount(price); + // 남은 수량이 0이 아닐 경우 로그 기록 + if (remaining - acquiredToUse > 0) { + log.warn("Not enough balance to fully refund {} pencils. Refunded only {}.", pencilQuantity, + (purchasedToUse + acquiredToUse)); + } + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java b/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java new file mode 100644 index 00000000..e469dbee --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/PencilQueryService.java @@ -0,0 +1,226 @@ +package umc.th.juinjang.api.pencil.service; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.apple.itunes.storekit.model.AccountTenure; +import com.apple.itunes.storekit.model.ConsumptionRequest; +import com.apple.itunes.storekit.model.ConsumptionStatus; +import com.apple.itunes.storekit.model.DeliveryStatus; +import com.apple.itunes.storekit.model.LifetimeDollarsPurchased; +import com.apple.itunes.storekit.model.LifetimeDollarsRefunded; +import com.apple.itunes.storekit.model.Platform; +import com.apple.itunes.storekit.model.PlayTime; +import com.apple.itunes.storekit.model.RefundPreference; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse; +import umc.th.juinjang.api.pencil.service.response.PurchasedPencilResponse; +import umc.th.juinjang.api.pencil.service.response.UsedPencilResponse; +import umc.th.juinjang.api.pencilAccount.service.PencilAccountFinder; +import umc.th.juinjang.common.exception.handler.PencilAccountHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PencilQueryService { + + private static final double DOLLAR_EXCHANGE_RATE = 1374.0; + private final AcquiredPencilFinder acquiredPencilFinder; + private final PurchasedPencilFinder purchasedPencilFinder; + private final UsedPencilFinder usedPencilFinder; + private final PencilAccountFinder pencilAccountFinder; + + public List getAcquiredPencils(Member member) { + return acquiredPencilFinder.findAllByMemberOrderByCreatedAtDesc(member); + } + + public List getPurchasedPencils(Member member) { + List purchasedPencils = purchasedPencilFinder.findAllByMemberWhereDeliverySuccessOrderByCreatedAtDesc( + member); + return purchasedPencils.stream() + .map(PurchasedPencilResponse::from) + .toList(); + } + + public List getUsedPencils(Member member) { + List usedPencils = usedPencilFinder.findAllByMemberOrderByCreatedAtDesc(member); + return usedPencils.stream() + .map(UsedPencilResponse::from) + .toList(); + } + + public boolean isAcquiredPencilReadStatus(Member member) { + // false 인 것이 존재하면 안됨. + return !acquiredPencilFinder.existsByMemberAndIsReadFalse(member); + } + + public ConsumptionRequest getConsumptionRequest(String transactionId) { + return converterToConsumptionRequest(purchasedPencilFinder.findByTransactionId(transactionId)); + } + + private ConsumptionRequest converterToConsumptionRequest(Optional purchasedPencil) { + if (purchasedPencil.isPresent()) { + PurchasedPencil purchase = purchasedPencil.get(); + Member member = purchase.getMember(); + + ConsumptionRequest request = new ConsumptionRequest(); + request.setCustomerConsented(true); + request.setPlayTime(calculatePlayTime(purchase.getPlayTime())); + request.setAppAccountToken(purchase.getAppAccountToken()); + request.setDeliveryStatus(DeliveryStatus.fromValue(purchase.getDeliveryStatus().getAppleCode())); + request.setConsumptionStatus( + converterToConsumptionStatus(purchase.getPurchaseQuantity(), purchase.getRemainQuantity())); + request.setAccountTenure(calculateAccountTenure(member)); + request.setLifetimeDollarsPurchased(calculateLifeDollarPurchased(member)); + request.setLifetimeDollarsRefunded(calculateLifeDollarRefunded(member)); + request.setPlatform(Platform.APPLE); + request.setSampleContentProvided(getSampleContentProvided(member)); + // request.setUserStatusgetUserStatus(member)); + request.setRefundPreference(RefundPreference.PREFER_GRANT); + log.info("getConsumptionRequest : {}", request); + + return request; + } + return null; + } + + private LifetimeDollarsPurchased calculateLifeDollarPurchased(Member member) { + try { + PencilAccount buyerAccount = pencilAccountFinder.findByMember(member); + long totalPrice = buyerAccount.getTotalPurchaseAmount() - buyerAccount.getTotalRefundAmount(); + + if (totalPrice == 0L) { + return LifetimeDollarsPurchased.ZERO_DOLLARS; + } + + double usdAmount = totalPrice / DOLLAR_EXCHANGE_RATE; + + if (usdAmount <= 0.0) { + return LifetimeDollarsPurchased.ZERO_DOLLARS; + } else if (usdAmount < 50) { + return LifetimeDollarsPurchased.ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 100) { + return LifetimeDollarsPurchased.FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 500) { + return LifetimeDollarsPurchased.ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 1000) { + return LifetimeDollarsPurchased.FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 2000) { + return LifetimeDollarsPurchased.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else { + return LifetimeDollarsPurchased.TWO_THOUSAND_DOLLARS_OR_GREATER; + } + } catch (PencilAccountHandler exception) { + return LifetimeDollarsPurchased.UNDECLARED; + } + } + + private LifetimeDollarsRefunded calculateLifeDollarRefunded(Member member) { + try { + PencilAccount buyerAccount = pencilAccountFinder.findByMember(member); + long totalRefundWon = buyerAccount.getTotalRefundAmount(); + + if (totalRefundWon == 0L) { + return LifetimeDollarsRefunded.ZERO_DOLLARS; + } + + double usdAmount = totalRefundWon / DOLLAR_EXCHANGE_RATE; + + if (usdAmount <= 0.0) { + return LifetimeDollarsRefunded.ZERO_DOLLARS; + } else if (usdAmount < 50) { + return LifetimeDollarsRefunded.ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 100) { + return LifetimeDollarsRefunded.FIFTY_DOLLARS_TO_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 500) { + return LifetimeDollarsRefunded.ONE_HUNDRED_DOLLARS_TO_FOUR_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 1000) { + return LifetimeDollarsRefunded.FIVE_HUNDRED_DOLLARS_TO_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else if (usdAmount < 2000) { + return LifetimeDollarsRefunded.ONE_THOUSAND_DOLLARS_TO_ONE_THOUSAND_NINE_HUNDRED_NINETY_NINE_DOLLARS_AND_NINETY_NINE_CENTS; + } else { + return LifetimeDollarsRefunded.TWO_THOUSAND_DOLLARS_OR_GREATER; + } + } catch (PencilAccountHandler exception) { + return LifetimeDollarsRefunded.UNDECLARED; + } + + } + + private boolean getSampleContentProvided(Member member) { + return acquiredPencilFinder.existsByMember(member); + } + + private ConsumptionStatus converterToConsumptionStatus(Long purchaseQuantity, Long remainQuantity) { + if (remainQuantity == null || purchaseQuantity == null) { + return ConsumptionStatus.UNDECLARED; + } + + if (remainQuantity.equals(purchaseQuantity)) { + return ConsumptionStatus.NOT_CONSUMED; + } else if (remainQuantity == 0) { + return ConsumptionStatus.FULLY_CONSUMED; + } else if (remainQuantity > 0) { + return ConsumptionStatus.PARTIALLY_CONSUMED; + } + + return ConsumptionStatus.UNDECLARED; + } + + private AccountTenure calculateAccountTenure(Member member) { + // 회원 가입일로부터 현재까지의 기간을 계산 + LocalDateTime memberCreatedAt = member.getCreatedAt(); + LocalDateTime now = LocalDateTime.now(); + long daysBetween = ChronoUnit.DAYS.between(memberCreatedAt, now); + + // 기간에 따라 AccountTenure 반환 + if (daysBetween <= 3) { + return AccountTenure.ZERO_TO_THREE_DAYS; + } else if (daysBetween <= 10) { + return AccountTenure.THREE_DAYS_TO_TEN_DAYS; + } else if (daysBetween <= 30) { + return AccountTenure.TEN_DAYS_TO_THIRTY_DAYS; + } else if (daysBetween <= 90) { + return AccountTenure.THIRTY_DAYS_TO_NINETY_DAYS; + } else if (daysBetween <= 180) { + return AccountTenure.NINETY_DAYS_TO_ONE_HUNDRED_EIGHTY_DAYS; + } else if (daysBetween <= 365) { + return AccountTenure.ONE_HUNDRED_EIGHTY_DAYS_TO_THREE_HUNDRED_SIXTY_FIVE_DAYS; + } else { + return AccountTenure.GREATER_THAN_THREE_HUNDRED_SIXTY_FIVE_DAYS; + } + } + + private PlayTime calculatePlayTime(Integer playTime) { + if (playTime == null || playTime < 0) { + return PlayTime.UNDECLARED; + } + + if (playTime <= 5) { + return PlayTime.ZERO_TO_FIVE_MINUTES; + } else if (playTime <= 60) { + return PlayTime.FIVE_TO_SIXTY_MINUTES; + } else if (playTime <= 360) { // 6시간 + return PlayTime.ONE_TO_SIX_HOURS; + } else if (playTime <= 1440) { // 24시간 + return PlayTime.SIX_HOURS_TO_TWENTY_FOUR_HOURS; + } else if (playTime <= 5760) { // 4일 + return PlayTime.ONE_DAY_TO_FOUR_DAYS; + } else if (playTime <= 23040) { // 16일 + return PlayTime.FOUR_DAYS_TO_SIXTEEN_DAYS; + } else { + return PlayTime.OVER_SIXTEEN_DAYS; + } + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/PurchasedPencilFinder.java b/src/main/java/umc/th/juinjang/api/pencil/service/PurchasedPencilFinder.java new file mode 100644 index 00000000..bf01670f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/PurchasedPencilFinder.java @@ -0,0 +1,37 @@ +package umc.th.juinjang.api.pencil.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +import umc.th.juinjang.domain.pencil.purchased.repository.PurchasedPencilRepository; + +@Component +@RequiredArgsConstructor +public class PurchasedPencilFinder { + private final PurchasedPencilRepository purchasedPencilRepository; + + public List findAllByMemberWhereDeliverySuccessOrderByCreatedAtDesc(Member member) { + return purchasedPencilRepository.findAllByMemberWhereDeliverySuccessOrderByCreatedAtDesc(member); + } + + public Optional findByTransactionIdAndMember(String transactionId, Member member) { + return purchasedPencilRepository.findByTransactionIdAndMember(transactionId, member); + } + + public Optional findByTransactionId(String transactionId) { + return purchasedPencilRepository.findByTransactionId(transactionId); + } + + public Long getSumPriceWhereMemberAndSuccess(Member member) { + return purchasedPencilRepository.getSumPriceWhereMemberAndSuccess(member).orElse(0L); + } + + public Long getSumPriceWhereMemberAndRefund(Member member) { + return purchasedPencilRepository.getSumPriceWhereMemberAndRefund(member).orElse(0L); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/PurchasedPencilUpdater.java b/src/main/java/umc/th/juinjang/api/pencil/service/PurchasedPencilUpdater.java new file mode 100644 index 00000000..b238c3f6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/PurchasedPencilUpdater.java @@ -0,0 +1,29 @@ +package umc.th.juinjang.api.pencil.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +import umc.th.juinjang.domain.pencil.purchased.repository.PurchasedPencilRepository; + +@Component +@RequiredArgsConstructor +public class PurchasedPencilUpdater { + + private final PurchasedPencilRepository purchasedPencilRepository; + + public List findByMemberAndDeliverySuccessRemainQuantityGreaterThanOrderByCreatedAtAsc( + Member buyer, + Long remainQuantity) { + return purchasedPencilRepository.findByMemberAndDeliverySuccessAndRemainQuantityGreaterThanOrderByCreatedAtAsc( + buyer, + remainQuantity); + } + + public void save(PurchasedPencil successPurchase) { + purchasedPencilRepository.save(successPurchase); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/UsedPencilFinder.java b/src/main/java/umc/th/juinjang/api/pencil/service/UsedPencilFinder.java new file mode 100644 index 00000000..1f44ae89 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/UsedPencilFinder.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.api.pencil.service; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.pencil.used.repository.UsedPencilRepository; + +@Component +@RequiredArgsConstructor +public class UsedPencilFinder { + + private final UsedPencilRepository usedPencilRepository; + + public boolean existsByMemberAndSharedNoteId(Member member, long sharedNoteId) { + return usedPencilRepository.existsByMemberAndSharedNoteId(member, sharedNoteId); + } + + public int countBySharedNoteId(long sharedNoteId) { + return usedPencilRepository.countBySharedNoteId(sharedNoteId); + } + + public List findAllByMemberOrderByCreatedAtDesc(Member member) { + return usedPencilRepository.findAllByMemberOrderByCreatedAtDesc(member); + } + + public List findByMemberInSharedNoteIdsAndTypeIsOwned(Member member, List sharedNoteIds) { + return usedPencilRepository.findByMemberInSharedNoteIdsAndTypeIsOwned(member, sharedNoteIds); + } + + public List findAllByMemberAndTypeIsOwnedOrderByCreatedAtDesc(Member member) { + return usedPencilRepository.findAllByMemberAndTypeIsOwnedOrderByCreatedAtDesc(member); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/UsedPencilUpdater.java b/src/main/java/umc/th/juinjang/api/pencil/service/UsedPencilUpdater.java new file mode 100644 index 00000000..4bb15f63 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/UsedPencilUpdater.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.pencil.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.pencil.used.repository.UsedPencilRepository; + +@Component +@RequiredArgsConstructor +public class UsedPencilUpdater { + + private final UsedPencilRepository usedPencilRepository; + + public void save(UsedPencil usedPencil) { + usedPencilRepository.save(usedPencil); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadResponse.java new file mode 100644 index 00000000..6c182bba --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadResponse.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.api.pencil.service.response; + +public record AcquiredPencilReadResponse( + boolean isMarked, + boolean isTotalRead +) { + public static AcquiredPencilReadResponse of(boolean isMarked, boolean isTotalRead) { + return new AcquiredPencilReadResponse(isMarked, isTotalRead); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadStatusResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadStatusResponse.java new file mode 100644 index 00000000..10f21000 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilReadStatusResponse.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.api.pencil.service.response; + +public record AcquiredPencilReadStatusResponse( + boolean isTotalRead +) { + public static AcquiredPencilReadStatusResponse of(boolean isTotalRead) { + return new AcquiredPencilReadStatusResponse(isTotalRead); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilResponse.java new file mode 100644 index 00000000..c651b02b --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AcquiredPencilResponse.java @@ -0,0 +1,46 @@ +package umc.th.juinjang.api.pencil.service.response; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredType; + +@Getter +public class AcquiredPencilResponse { + + private final Long acquiredPencilId; + private final String content; + private final Long sharedNoteId; + private final Long acquiredQuantity; + private final String buildingName; + private final boolean isRead; + private final String type; + private final LocalDateTime createdAt; + + @Builder + public AcquiredPencilResponse(Long acquiredPencilId, String content, Long sharedNoteId, Long acquiredQuantity, + String buildingName, boolean isRead, String type, LocalDateTime createdAt) { + this.acquiredPencilId = acquiredPencilId; + this.content = content; + this.sharedNoteId = sharedNoteId; + this.acquiredQuantity = acquiredQuantity; + this.buildingName = buildingName; + this.isRead = isRead; + this.type = type; + this.createdAt = createdAt; + } + + public AcquiredPencilResponse(Long acquiredPencilId, String content, Long sharedNoteId, + Long acquiredQuantity, boolean isRead, AcquiredType type, + LocalDateTime createdAt, String buildingName) { + this.acquiredPencilId = acquiredPencilId; + this.content = content; + this.sharedNoteId = sharedNoteId; + this.acquiredQuantity = acquiredQuantity; + this.isRead = isRead; + this.type = type.name(); + this.createdAt = createdAt; + this.buildingName = buildingName; + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java new file mode 100644 index 00000000..ff4ea2ff --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/AppleIAPPurchaseResponse.java @@ -0,0 +1,67 @@ +package umc.th.juinjang.api.pencil.service.response; + +import lombok.Builder; +import lombok.Getter; +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; + +@Getter +public class AppleIAPPurchaseResponse { + + private final TransactionStatus status; + private final String transactionId; + private final Long purchaseQuantity; + /** + * 구매한 후에 남아 있는 연필 개수 + */ + private final Long remainQuantity; + + @Builder + private AppleIAPPurchaseResponse( + TransactionStatus status, + String transactionId, + Long purchaseQuantity, + Long remainQuantity + ) { + this.status = status; + this.transactionId = transactionId; + this.purchaseQuantity = purchaseQuantity; + this.remainQuantity = remainQuantity; + } + + /** + * 일반적인 정적 팩토리: 모든 필드 수동 지정 + */ + public static AppleIAPPurchaseResponse of(String transactionId, TransactionStatus status, Long purchaseQuantity, + Long remainQuantity) { + return AppleIAPPurchaseResponse.builder() + .transactionId(transactionId) + .status(status) + .purchaseQuantity(purchaseQuantity) + .remainQuantity(remainQuantity) + .build(); + } + + /** + * 성공 응답용 팩토리 + */ + public static AppleIAPPurchaseResponse ofSuccess(String transactionId, Long purchaseQuantity, Long remainQuantity) { + return AppleIAPPurchaseResponse.builder() + .status(TransactionStatus.SUCCESS) + .transactionId(transactionId) + .purchaseQuantity(purchaseQuantity) + .remainQuantity(remainQuantity) + .build(); + } + + /** + * 검증 실패 응답용 팩토리 + */ + public static AppleIAPPurchaseResponse ofValidationFailure(String transactionId) { + return AppleIAPPurchaseResponse.builder() + .status(TransactionStatus.VALIDATION_FAILED) + .transactionId(transactionId) + .purchaseQuantity(0L) + .remainQuantity(null) + .build(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/PurchasedPencilResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/PurchasedPencilResponse.java new file mode 100644 index 00000000..6343a248 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/PurchasedPencilResponse.java @@ -0,0 +1,39 @@ +package umc.th.juinjang.api.pencil.service.response; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; + +@Getter +public class PurchasedPencilResponse { + private final Long purchasePencilId; + private final Long purchaseQuantity; + private final Long remainQuantity; + private final String title; + private final Long price; + private final LocalDateTime purchasedAt; + + @Builder + public PurchasedPencilResponse(Long purchasePencilId, Long purchaseQuantity, Long remainQuantity, + String title, Long price, LocalDateTime purchasedAt) { + this.purchasePencilId = purchasePencilId; + this.purchaseQuantity = purchaseQuantity; + this.remainQuantity = remainQuantity; + this.title = title; + this.price = price; + this.purchasedAt = purchasedAt; + } + + public static PurchasedPencilResponse from(PurchasedPencil purchasedPencil) { + return PurchasedPencilResponse.builder() + .purchasePencilId(purchasedPencil.getId()) + .purchaseQuantity(purchasedPencil.getPurchaseQuantity()) + .remainQuantity(purchasedPencil.getRemainQuantity()) + .title(purchasedPencil.getTitle()) + .price(purchasedPencil.getPrice()) + .purchasedAt(purchasedPencil.getPurchasedAt()) + .build(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/UsedPencilResponse.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/UsedPencilResponse.java new file mode 100644 index 00000000..cfcd30b1 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/UsedPencilResponse.java @@ -0,0 +1,45 @@ +package umc.th.juinjang.api.pencil.service.response; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.pencil.used.model.Usedtype; + +@Getter +public class UsedPencilResponse { + + private Long usedPencilId; + private Long useQuantity; + private Usedtype type; + private Long remainQuantity; + private String buildingName; + private Long sharedNoteId; + private LocalDateTime createdAt; + + @Builder + public UsedPencilResponse(Long usedPencilId, Long useQuantity, Usedtype type, + Long remainQuantity, String buildingName, + Long sharedNoteId, LocalDateTime createdAt) { + this.usedPencilId = usedPencilId; + this.useQuantity = useQuantity; + this.type = type; + this.remainQuantity = remainQuantity; + this.buildingName = buildingName; + this.sharedNoteId = sharedNoteId; + this.createdAt = createdAt; + } + + public static UsedPencilResponse from(UsedPencil usedPencil) { + return UsedPencilResponse.builder() + .usedPencilId(usedPencil.getUsedPencilId()) + .useQuantity(usedPencil.getUsedQuantity()) + .type(usedPencil.getType()) + .remainQuantity(usedPencil.getRemainQuantity()) + .buildingName(usedPencil.getBuildingName()) + .sharedNoteId(usedPencil.getSharedNoteId()) + .createdAt(usedPencil.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencil/service/response/VerificationResult.java b/src/main/java/umc/th/juinjang/api/pencil/service/response/VerificationResult.java new file mode 100644 index 00000000..ed7b878f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencil/service/response/VerificationResult.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.api.pencil.service.response; + +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class VerificationResult { + + + public enum Status { SUCCESS, INVALID, IO_ERROR, SERVER_ERROR, VERIFICATION_ERROR } + + private final Status status; + private final JWSTransactionDecodedPayload payload; + + @Builder + private VerificationResult(Status status, JWSTransactionDecodedPayload payload) { + this.status = status; + this.payload = payload; + } + + public static VerificationResult ofSuccess(JWSTransactionDecodedPayload payload) { + return VerificationResult.builder().status(Status.SUCCESS).payload(payload).build(); + } + + public static VerificationResult ofServerError() { + return VerificationResult.builder().status(Status.SERVER_ERROR).build(); + } + + public static VerificationResult ofVerificationError() { + return VerificationResult.builder().status(Status.VERIFICATION_ERROR).build(); + } + + public static boolean isSuccess(VerificationResult verificationResult) { + return verificationResult.getStatus() == Status.SUCCESS; + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencilAccount/controller/PencilAccountController.java b/src/main/java/umc/th/juinjang/api/pencilAccount/controller/PencilAccountController.java new file mode 100644 index 00000000..a29213c0 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencilAccount/controller/PencilAccountController.java @@ -0,0 +1,29 @@ +package umc.th.juinjang.api.pencilAccount.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.pencilAccount.service.PencilAccountService; +import umc.th.juinjang.api.pencilAccount.service.response.PencilQuantityGetResponse; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2/pencil-account") +@RequiredArgsConstructor +public class PencilAccountController { + + private final PencilAccountService pencilAccountService; + + @GetMapping("/balance") + public ApiResponse getTotalPencilAmountByMember( + @AuthenticationPrincipal Member member + ) { + return ApiResponse.onSuccess( + PencilQuantityGetResponse.of(pencilAccountService.getTotalPencilAmountByMember(member))); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java new file mode 100644 index 00000000..35b44ded --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountFinder.java @@ -0,0 +1,35 @@ +package umc.th.juinjang.api.pencilAccount.service; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.common.exception.handler.PencilAccountHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.domain.pencilaccount.repository.PencilAccountRepository; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PencilAccountFinder { + + private final PencilAccountRepository pencilAccountRepository; + + public PencilAccount findByMember(Member member) { + return pencilAccountRepository.findByMember(member).orElseThrow( + () -> { + log.error("[PENCIL_ACCOUNT]"); + return new PencilAccountHandler(PENCIL_ACCOUNT_NOT_FOUND); + } + ); + } + + public PencilAccount findByMemberWithLock(Member member) { + return pencilAccountRepository.findByMemberWithLock(member) + .orElseThrow(() -> new PencilAccountHandler(PENCIL_ACCOUNT_NOT_FOUND)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountService.java b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountService.java new file mode 100644 index 00000000..d8e89c30 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountService.java @@ -0,0 +1,19 @@ +package umc.th.juinjang.api.pencilAccount.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; + +@Service +@RequiredArgsConstructor +public class PencilAccountService { + + private final PencilAccountFinder pencilAccountFinder; + + public Long getTotalPencilAmountByMember(Member member) { + PencilAccount pencilAccount = pencilAccountFinder.findByMember(member); + return pencilAccount.getTotalBalance(); + } +} diff --git a/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountUpdater.java b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountUpdater.java new file mode 100644 index 00000000..1974ae5a --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountUpdater.java @@ -0,0 +1,21 @@ +package umc.th.juinjang.api.pencilAccount.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.domain.pencilaccount.repository.PencilAccountRepository; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PencilAccountUpdater { + + private final PencilAccountRepository pencilAccountRepository; + + public void save(PencilAccount pencilAccount) { + pencilAccountRepository.save(pencilAccount); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/pencilAccount/service/response/PencilQuantityGetResponse.java b/src/main/java/umc/th/juinjang/api/pencilAccount/service/response/PencilQuantityGetResponse.java new file mode 100644 index 00000000..e39afad7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/pencilAccount/service/response/PencilQuantityGetResponse.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.pencilAccount.service.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class PencilQuantityGetResponse { + private Long totalBalance; + + @Builder + private PencilQuantityGetResponse(Long totalBalance) { + this.totalBalance = totalBalance; + } + + public static PencilQuantityGetResponse of(Long totalBalance) { + return PencilQuantityGetResponse.builder().totalBalance(totalBalance).build(); + } +} diff --git a/src/main/java/umc/th/juinjang/controller/RecordController.java b/src/main/java/umc/th/juinjang/api/record/controller/RecordController.java similarity index 89% rename from src/main/java/umc/th/juinjang/controller/RecordController.java rename to src/main/java/umc/th/juinjang/api/record/controller/RecordController.java index 0ddfb8fa..b9392a99 100644 --- a/src/main/java/umc/th/juinjang/controller/RecordController.java +++ b/src/main/java/umc/th/juinjang/api/record/controller/RecordController.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.controller; +package umc.th.juinjang.api.record.controller; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -7,13 +7,13 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangMemoResponseDTO; -import umc.th.juinjang.model.dto.limjang.request.LimjangUpdateRequestDTO; -import umc.th.juinjang.model.dto.record.RecordRequestDTO; -import umc.th.juinjang.model.dto.record.RecordResponseDTO; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.service.record.RecordService; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.limjang.service.response.LimjangMemoResponseDTO; +import umc.th.juinjang.api.limjang.controller.request.LimjangUpdateRequestDTO; +import umc.th.juinjang.api.record.controller.request.RecordRequestDTO; +import umc.th.juinjang.api.record.service.response.RecordResponseDTO; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.api.record.service.RecordService; import java.io.IOException; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/model/dto/record/RecordRequestDTO.java b/src/main/java/umc/th/juinjang/api/record/controller/request/RecordRequestDTO.java similarity index 92% rename from src/main/java/umc/th/juinjang/model/dto/record/RecordRequestDTO.java rename to src/main/java/umc/th/juinjang/api/record/controller/request/RecordRequestDTO.java index 65b38be1..751e9835 100644 --- a/src/main/java/umc/th/juinjang/model/dto/record/RecordRequestDTO.java +++ b/src/main/java/umc/th/juinjang/api/record/controller/request/RecordRequestDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.record; +package umc.th.juinjang.api.record.controller.request; import lombok.*; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/umc/th/juinjang/service/record/RecordService.java b/src/main/java/umc/th/juinjang/api/record/service/RecordService.java similarity index 88% rename from src/main/java/umc/th/juinjang/service/record/RecordService.java rename to src/main/java/umc/th/juinjang/api/record/service/RecordService.java index 61b93e35..6810c4f7 100644 --- a/src/main/java/umc/th/juinjang/service/record/RecordService.java +++ b/src/main/java/umc/th/juinjang/api/record/service/RecordService.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.service.record; +package umc.th.juinjang.api.record.service; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; @@ -8,19 +8,19 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.converter.record.LimjangMemoConverter; -import umc.th.juinjang.converter.record.RecordConverter; -import umc.th.juinjang.model.dto.limjang.response.LimjangMemoResponseDTO; -import umc.th.juinjang.model.dto.record.RecordRequestDTO; -import umc.th.juinjang.model.dto.record.RecordResponseDTO; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.Record; -import umc.th.juinjang.repository.limjang.LimjangRepository; -import umc.th.juinjang.repository.record.RecordRepository; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.api.record.service.converter.LimjangMemoConverter; +import umc.th.juinjang.api.record.service.converter.RecordConverter; +import umc.th.juinjang.api.limjang.service.response.LimjangMemoResponseDTO; +import umc.th.juinjang.api.record.controller.request.RecordRequestDTO; +import umc.th.juinjang.api.record.service.response.RecordResponseDTO; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.record.model.Record; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.record.repository.RecordRepository; import java.io.FileNotFoundException; import java.io.IOException; diff --git a/src/main/java/umc/th/juinjang/converter/record/LimjangMemoConverter.java b/src/main/java/umc/th/juinjang/api/record/service/converter/LimjangMemoConverter.java similarity index 68% rename from src/main/java/umc/th/juinjang/converter/record/LimjangMemoConverter.java rename to src/main/java/umc/th/juinjang/api/record/service/converter/LimjangMemoConverter.java index e529b502..56990ae6 100644 --- a/src/main/java/umc/th/juinjang/converter/record/LimjangMemoConverter.java +++ b/src/main/java/umc/th/juinjang/api/record/service/converter/LimjangMemoConverter.java @@ -1,7 +1,7 @@ -package umc.th.juinjang.converter.record; +package umc.th.juinjang.api.record.service.converter; -import umc.th.juinjang.model.dto.limjang.response.LimjangMemoResponseDTO; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.api.limjang.service.response.LimjangMemoResponseDTO; +import umc.th.juinjang.domain.limjang.model.Limjang; public class LimjangMemoConverter { diff --git a/src/main/java/umc/th/juinjang/converter/record/RecordConverter.java b/src/main/java/umc/th/juinjang/api/record/service/converter/RecordConverter.java similarity index 85% rename from src/main/java/umc/th/juinjang/converter/record/RecordConverter.java rename to src/main/java/umc/th/juinjang/api/record/service/converter/RecordConverter.java index 9542a1d8..5131feec 100644 --- a/src/main/java/umc/th/juinjang/converter/record/RecordConverter.java +++ b/src/main/java/umc/th/juinjang/api/record/service/converter/RecordConverter.java @@ -1,9 +1,9 @@ -package umc.th.juinjang.converter.record; +package umc.th.juinjang.api.record.service.converter; -import umc.th.juinjang.model.dto.record.RecordRequestDTO; -import umc.th.juinjang.model.dto.record.RecordResponseDTO; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Record; +import umc.th.juinjang.api.record.controller.request.RecordRequestDTO; +import umc.th.juinjang.api.record.service.response.RecordResponseDTO; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.record.model.Record; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/umc/th/juinjang/model/dto/record/RecordResponseDTO.java b/src/main/java/umc/th/juinjang/api/record/service/response/RecordResponseDTO.java similarity index 94% rename from src/main/java/umc/th/juinjang/model/dto/record/RecordResponseDTO.java rename to src/main/java/umc/th/juinjang/api/record/service/response/RecordResponseDTO.java index ac31d6b1..8ba7d010 100644 --- a/src/main/java/umc/th/juinjang/model/dto/record/RecordResponseDTO.java +++ b/src/main/java/umc/th/juinjang/api/record/service/response/RecordResponseDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.record; +package umc.th.juinjang.api.record.service.response; import lombok.*; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/umc/th/juinjang/api/reward/service/RewardFinder.java b/src/main/java/umc/th/juinjang/api/reward/service/RewardFinder.java new file mode 100644 index 00000000..d8ed6e63 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/reward/service/RewardFinder.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.reward.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.reward.model.RewardType; +import umc.th.juinjang.domain.reward.repository.RewardRepository; + +@Component +@RequiredArgsConstructor +public class RewardFinder { + + private final RewardRepository rewardRepository; + + boolean existsByTypeAndMilestoneAndSharedNoteId(RewardType type, Long milestone, Long sharedNoteId) { + return rewardRepository.existsByTypeAndMilestoneAndSharedNoteId(type, milestone, sharedNoteId); + } +} diff --git a/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java b/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java new file mode 100644 index 00000000..a11c8f3e --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/reward/service/RewardService.java @@ -0,0 +1,60 @@ +package umc.th.juinjang.api.reward.service; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.pencil.service.AcquiredPencilUpdater; +import umc.th.juinjang.api.pencilAccount.service.PencilAccountFinder; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.ViewCountPolicy; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredType; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.domain.reward.model.Reward; +import umc.th.juinjang.domain.reward.model.RewardType; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RewardService { + + private final AcquiredPencilUpdater acquiredPencilUpdater; + private final PencilAccountFinder pencilAccountFinder; + private final ViewCountPolicy viewCountPolicy; + private final RewardFinder rewardFinder; + private final RewardUpdater rewardUpdater; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void giveViewCountReward(Member member, Long sharedNoteId, Long milestone, Long rewardPencil) { + + if (alreadyViewCountRewardEarned(RewardType.VIEWCOUNT, sharedNoteId, milestone)) { + return; + } + + PencilAccount account = pencilAccountFinder.findByMemberWithLock(member); + account.increaseAcquiredBalance(rewardPencil); + + acquiredPencilUpdater.save( + createAcquiredPencil(member, sharedNoteId, milestone, rewardPencil)); + rewardUpdater.save(createReward(member, sharedNoteId, milestone, rewardPencil)); + + log.info("유저에게 조회수 리워드 지급 완료: memberId={}, sharedNoteId={}, milestone={}, rewardPencil={}", + member.getMemberId(), sharedNoteId, milestone, rewardPencil); + } + + private AcquiredPencil createAcquiredPencil(Member member, Long sharedNoteId, Long milestone, Long rewardPencil) { + return AcquiredPencil.create(member, viewCountPolicy.getMessageForMilestone(milestone), sharedNoteId, + rewardPencil, false, AcquiredType.VIEWCOUNT); + } + + private Reward createReward(Member member, Long sharedNoteId, Long milestone, Long rewardPencil) { + return Reward.create(member, RewardType.VIEWCOUNT, milestone, sharedNoteId, rewardPencil); + } + + private boolean alreadyViewCountRewardEarned(RewardType type, Long sharedNoteId, Long milestone) { + return rewardFinder.existsByTypeAndMilestoneAndSharedNoteId(type, milestone, sharedNoteId); + } +} diff --git a/src/main/java/umc/th/juinjang/api/reward/service/RewardUpdater.java b/src/main/java/umc/th/juinjang/api/reward/service/RewardUpdater.java new file mode 100644 index 00000000..d1e7415e --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/reward/service/RewardUpdater.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.reward.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.reward.model.Reward; +import umc.th.juinjang.domain.reward.repository.RewardRepository; + +@Component +@RequiredArgsConstructor +public class RewardUpdater { + + private final RewardRepository rewardRepository; + + void save(Reward reward) { + rewardRepository.save(reward); + } +} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/controller/ScrapController.java b/src/main/java/umc/th/juinjang/api/scrap/controller/ScrapController.java similarity index 83% rename from src/main/java/umc/th/juinjang/controller/ScrapController.java rename to src/main/java/umc/th/juinjang/api/scrap/controller/ScrapController.java index a4586ecf..50e6aa42 100644 --- a/src/main/java/umc/th/juinjang/controller/ScrapController.java +++ b/src/main/java/umc/th/juinjang/api/scrap/controller/ScrapController.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.controller; +package umc.th.juinjang.api.scrap.controller; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -8,10 +8,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.apiPayload.code.status.SuccessStatus; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.service.scrap.ScrapService; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.common.code.status.SuccessStatus; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.api.scrap.service.ScrapService; @RestController @RequestMapping("/api/limjangs/scraps") diff --git a/src/main/java/umc/th/juinjang/api/scrap/service/ScarpFinder.java b/src/main/java/umc/th/juinjang/api/scrap/service/ScarpFinder.java new file mode 100644 index 00000000..96a464d9 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/scrap/service/ScarpFinder.java @@ -0,0 +1,21 @@ +package umc.th.juinjang.api.scrap.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.scrap.model.Scrap; +import umc.th.juinjang.domain.scrap.repository.ScrapRepository; + +@Service +@RequiredArgsConstructor +public class ScarpFinder { + + private final ScrapRepository scrapRepository; + + public List findAllByNoteId(List notes) { + return scrapRepository.findAllByLimjangIdIn(notes); + } +} diff --git a/src/main/java/umc/th/juinjang/service/scrap/ScrapService.java b/src/main/java/umc/th/juinjang/api/scrap/service/ScrapService.java similarity index 59% rename from src/main/java/umc/th/juinjang/service/scrap/ScrapService.java rename to src/main/java/umc/th/juinjang/api/scrap/service/ScrapService.java index 6601a059..66b87716 100644 --- a/src/main/java/umc/th/juinjang/service/scrap/ScrapService.java +++ b/src/main/java/umc/th/juinjang/api/scrap/service/ScrapService.java @@ -1,6 +1,6 @@ -package umc.th.juinjang.service.scrap; +package umc.th.juinjang.api.scrap.service; -import umc.th.juinjang.model.entity.Member; +import umc.th.juinjang.domain.member.model.Member; public interface ScrapService { diff --git a/src/main/java/umc/th/juinjang/service/scrap/ScrapServiceImpl.java b/src/main/java/umc/th/juinjang/api/scrap/service/ScrapServiceImpl.java similarity index 77% rename from src/main/java/umc/th/juinjang/service/scrap/ScrapServiceImpl.java rename to src/main/java/umc/th/juinjang/api/scrap/service/ScrapServiceImpl.java index e3a682f8..c44679b6 100644 --- a/src/main/java/umc/th/juinjang/service/scrap/ScrapServiceImpl.java +++ b/src/main/java/umc/th/juinjang/api/scrap/service/ScrapServiceImpl.java @@ -1,18 +1,18 @@ -package umc.th.juinjang.service.scrap; +package umc.th.juinjang.api.scrap.service; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.apiPayload.exception.handler.ScrapHandler; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.Scrap; -import umc.th.juinjang.repository.limjang.LimjangRepository; -import umc.th.juinjang.repository.limjang.ScrapRepository; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.common.exception.handler.ScrapHandler; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.scrap.model.Scrap; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.scrap.repository.ScrapRepository; @Slf4j @Service diff --git a/src/main/java/umc/th/juinjang/api/termsAgreement/controller/TermsAgreementController.java b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/TermsAgreementController.java new file mode 100644 index 00000000..abefc5d1 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/TermsAgreementController.java @@ -0,0 +1,51 @@ +package umc.th.juinjang.api.termsAgreement.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.termsAgreement.controller.request.AgreeRequest; +import umc.th.juinjang.api.termsAgreement.controller.response.AgreeResponse; +import umc.th.juinjang.api.termsAgreement.controller.response.StatusResponse; +import umc.th.juinjang.api.termsAgreement.service.TermsAgreementService; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.termsAgreement.repository.TermsType; + +@RestController +@RequestMapping("/api/v2/terms-agreement") +@RequiredArgsConstructor +public class TermsAgreementController { + + private final TermsAgreementService termsAgreementService; + + @Operation(summary = "특정 약관 동의 여부 확인", + description = "로그인한 멤버가 특정 약관에 동의했는지 확인합니다.") + @GetMapping("/{termsType}") + public ApiResponse checkStatus( + @AuthenticationPrincipal Member member, + @Parameter(description = "확인할 약관 타입") + @PathVariable TermsType termsType) { + boolean isAgreed = termsAgreementService.checkStatus(member.getMemberId(), termsType); + return ApiResponse.onSuccess(StatusResponse.of(isAgreed)); + } + + @Operation(summary = "특정 약관에 동의 하기", + description = "해당 멤버가 특정 약관에 동의합니다.") + @PostMapping + public ApiResponse agreeToSpecificTerms( + @AuthenticationPrincipal Member member, + @RequestBody AgreeRequest request + ) { + termsAgreementService.agreeToSpecificTerms(member.getMemberId(), request.termsType()); + return ApiResponse.onSuccess(AgreeResponse.from(request.termsType(), true)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/termsAgreement/controller/request/AgreeRequest.java b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/request/AgreeRequest.java new file mode 100644 index 00000000..07401c80 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/request/AgreeRequest.java @@ -0,0 +1,8 @@ +package umc.th.juinjang.api.termsAgreement.controller.request; + +import umc.th.juinjang.domain.termsAgreement.repository.TermsType; + +public record AgreeRequest( + TermsType termsType +) { +} diff --git a/src/main/java/umc/th/juinjang/api/termsAgreement/controller/response/AgreeResponse.java b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/response/AgreeResponse.java new file mode 100644 index 00000000..bf1bcd66 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/response/AgreeResponse.java @@ -0,0 +1,15 @@ +package umc.th.juinjang.api.termsAgreement.controller.response; + +import umc.th.juinjang.domain.termsAgreement.repository.TermsType; + +public record AgreeResponse( + TermsType termsType, + boolean isAgreed +) { + public static AgreeResponse from(TermsType termsType, boolean isAgreed) { + return new AgreeResponse( + termsType, + isAgreed + ); + } +} diff --git a/src/main/java/umc/th/juinjang/api/termsAgreement/controller/response/StatusResponse.java b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/response/StatusResponse.java new file mode 100644 index 00000000..b259a486 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/termsAgreement/controller/response/StatusResponse.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.api.termsAgreement.controller.response; + +public record StatusResponse( + boolean status +) { + public static StatusResponse of(boolean isAgreed) { + return new StatusResponse(isAgreed); + } +} diff --git a/src/main/java/umc/th/juinjang/api/termsAgreement/service/TermsAgreementService.java b/src/main/java/umc/th/juinjang/api/termsAgreement/service/TermsAgreementService.java new file mode 100644 index 00000000..642b40e9 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/termsAgreement/service/TermsAgreementService.java @@ -0,0 +1,37 @@ +package umc.th.juinjang.api.termsAgreement.service; + +import static umc.th.juinjang.common.code.status.ErrorStatus.*; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.common.exception.handler.MemberHandler; +import umc.th.juinjang.domain.termsAgreement.repository.TermsAgreement; +import umc.th.juinjang.domain.termsAgreement.repository.TermsAgreementRepository; +import umc.th.juinjang.domain.termsAgreement.repository.TermsType; + +@Service +@RequiredArgsConstructor +public class TermsAgreementService { + + private final TermsAgreementRepository termsAgreementRepository; + + public boolean checkStatus(Long memberId, TermsType termsType) { + return termsAgreementRepository.findByMemberIdAndTermsType(memberId, termsType) + .isPresent(); + } + + public void agreeToSpecificTerms(Long memberId, TermsType termsType) { + Optional existingAgreement = + termsAgreementRepository.findByMemberIdAndTermsType(memberId, termsType); + + if (existingAgreement.isPresent()) { + throw new MemberHandler(TERMS_AGREEMENT_DUPLICATED); + } + + TermsAgreement agreement = TermsAgreement.create(memberId, termsType); + termsAgreementRepository.save(agreement); + } +} diff --git a/src/main/java/umc/th/juinjang/apiPayload/ExceptionHandler.java b/src/main/java/umc/th/juinjang/apiPayload/ExceptionHandler.java deleted file mode 100644 index 3ab25bc5..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/ExceptionHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package umc.th.juinjang.apiPayload; - -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.exception.GeneralException; - -public class ExceptionHandler extends GeneralException { - public ExceptionHandler(BaseErrorCode code) { - super(code); - } -} diff --git a/src/main/java/umc/th/juinjang/apiPayload/code/status/ErrorStatus.java b/src/main/java/umc/th/juinjang/apiPayload/code/status/ErrorStatus.java deleted file mode 100644 index 0a1d2c92..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/code/status/ErrorStatus.java +++ /dev/null @@ -1,126 +0,0 @@ -package umc.th.juinjang.apiPayload.code.status; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.code.ErrorReasonDTO; - -@Getter -@AllArgsConstructor -public enum ErrorStatus implements BaseErrorCode { - // 가장 일반적인 응답 - _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), - _BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON400","잘못된 요청입니다."), - _UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON401","인증이 필요합니다."), - _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), - // For test - TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"), - - // Member Error - MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "해당하는 사용자를 찾을 수 없습니다."), - MEMBER_EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4002", "전달받은 사용자의 이메일이 없습니다."), - MEMBER_NOT_FOUND_IN_KAKAO(HttpStatus.BAD_REQUEST, "MEMBER4003", "APPLE로 회원가입한 회원입니다."), - ALREADY_LOGOUT(HttpStatus.BAD_REQUEST, "MEMBER4004", "이미 로그아웃 되었습니다."), - UNCORRECTED_INFO(HttpStatus.BAD_REQUEST, "MEMBER4005", "올바르지 않은 정보입니다."), - MEMBER_NOT_FOUND_IN_APPLE(HttpStatus.BAD_REQUEST, "MEMBER4006", "KAKAO로 회원가입한 회원입니다."), - ALREADY_MEMBER(HttpStatus.BAD_REQUEST, "MEMBER4007", "이미 가입된 회원입니다."), - // 카카오 target id 에러 - EMPTY_TARGET_ID(HttpStatus.BAD_REQUEST, "MEMBER4008", "카카오 target id가 비어있습니다."), - UNCORRECTED_TARGET_ID(HttpStatus.BAD_REQUEST, "MEMBER4009", "target id와 회원 정보가 일치하지 않습니다. 올바르지 않은 카카오 target id 입니다."), - NOT_UNLINK_KAKAO(HttpStatus.BAD_REQUEST, "MEMBER4010", "카카오 연결 끊기에 실패하였습니다."), - FAILED_TO_LOGIN(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER4011", "잘못된 정보입니다. 서버 관리자에게 문의 바랍니다."), - FAILED_TO_SIGNUP(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER4012", "회원가입에 실패했습니다. 서버 관리자에게 문의 바랍니다."), - - //로그아웃 에러 - FAILED_TO_LOAD_PRIVATE_KEY(HttpStatus.BAD_REQUEST, "REVOKE4002", "private key 실패"), - LOGOUT_FAILED(HttpStatus.BAD_REQUEST, "REVOKE4002", "private key 실패"), - // nickname 에러 - NICKNAME_EMPTY(HttpStatus.BAD_REQUEST, "NICKNAME4001", "닉네임이 존재하지 않습니다. 닉네임을 입력해주세요."), - ALREADY_NICKNAME(HttpStatus.BAD_REQUEST, "NICKNAME4002", "이미 존재하는 닉네임입니다."), - - - // Limjang Error - LIMJANG_POST_REQUEST_NULL(HttpStatus.BAD_REQUEST, "LIMJANG4001", "입력 값이 모두 넘어오지 않았습니다. 누락된 값이 있는지 다시 확인해주세요."), - LIMJANG_POST_TYPE_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4002", "거래목적, 매물유형, 가격유형 입력값 중 하나가 정해지지 않은 값입니다. 다시 확인해주세요."), - LIMJANG_POST_PRICE_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4003", "전달된 가격이 잘못되었습니다. 입력값을 확인해주세요."), - LIMJANG_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG404", "해당 임장이 존재하지 않습니다."), - - LIMJANG_DELETE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LIMJANG4005", "전달된 ID의 값이 DB에 존재하지 않습니다. 전달 값을 다시 확인해주세요."), - LIMJANG_DELETE_NOT_COMPLETE(HttpStatus.BAD_REQUEST, "LIMJANG4006", "요청한 임장 게시글이 모두 삭제되지 않아 삭제가 취소되었습니다. 다시 시도하거나 백엔드 팀에 문의바랍니다."), - LIMJANG_UPDATE_PRICETYPE_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4007", "가격유형 입력값이 잘못되었습니다. 다시 확인해주세요."), - LIMJANG_REQUEST_SORT_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4008", "요청한 정렬 방식이 지정되지 않은 값입니다. 다시 확인해주세요."), - - - // LimjangPrice Error - LIMJANGPRICE_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "LIMJANGPRICE4000", "해당 임장가격 레코드가 존재하지 않습니다."), - LIMJANGPRICE_NULL_ERROR(HttpStatus.BAD_REQUEST, "LIMJANGPRICE4000", "임장 가격 저장에 실패했습니다. 임장 가격이 null입니다."), - - // scrap - _SCRAP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4000", "해당 게시글이 DB에 존재하지 않습니다. 관리자에게 문의 바랍니다."), - _SCRAP_SCRAP_FAILD(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4001", "스크랩 등록 실패. 재시도하거나 관리자에게 문의 바랍니다."), - _SCRAP_UNSCRAP_FAILD(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4002", "스크랩 취소 실패. 재시도하거나 관리자에게 문의 바랍니다."), - _SCRAP_ALREADY_SCRAPED(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4003", "이미 스크랩된 게시글입니다."), - _SCRAP_ALREADY_UNSCRAPED(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4004", "이미 스크랩 취소된 게시글입니다."), - - CHECKLIST_TYPE_ERROR(HttpStatus.BAD_REQUEST, "CHECKLIST400", "정해지지 않은 요청값입니다. 다시 확인해주세요."), - CHECKLIST_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "CHECKLIST404", "해당 체크리스트 질문 또는 답변이 존재하지 않습니다."), - - REPORT_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "REPORT404", "해당 리포트가 존재하지 않습니다."), - - - //JWT 토큰 에러 - TOKEN_UNAUTHORIZED(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE, "TOKEN400", "유효하지 않거나 만료된 토큰입니다."), - TOKEN_EMPTY(HttpStatus.BAD_REQUEST, "TOKEN401", "토큰값이 존재하지 않습니다."), - REFRESH_TOKEN_UNAUTHORIZED(HttpStatus.I_AM_A_TEAPOT, "TOKEN402", "유효하지 않은 Refresh Token입니다. 다시 로그인하세요."), - ACCESS_TOKEN_AUTHORIZED(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE, "TOKEN403", "유효하지 않은 Access Token입니다."), - REFRESH_TOKEN_AUTHORIZED(HttpStatus.UNAUTHORIZED, "TOKEN404", "유효한 Refresh Token입니다."), - APPLE_ID_TOKEN_EMPTY(HttpStatus.BAD_REQUEST,"TOKEN405", "ID TOKEN값이 존재하지 않습니다."), - INVALID_APPLE_ID_TOKEN(HttpStatus.UNAUTHORIZED,"TOKEN406", "Apple OAuth Identity Token 값이 올바르지 않습니다."), - PUBLICKEY_ERROR_IN_APPLE_LOGIN(HttpStatus.UNAUTHORIZED,"TOKEN407", "Apple OAuth 로그인 중 public key 생성에 문제가 발생했습니다."), - INVALID_APPLE_ID_TOKEN_INFO(HttpStatus.UNAUTHORIZED,"TOKEN407", "Apple id_token 값의 alg, kid 정보가 올바르지 않습니다."), - - // Image 에러 - IMAGE_DELETE_NOT_FOUND(HttpStatus.BAD_REQUEST, "IMAGE4000", "전달된 ID의 값이 DB에 존재하지 않습니다. 전달 값을 다시 확인해주세요."), - IMAGE_DELETE_NOT_COMPLETE(HttpStatus.BAD_REQUEST, "IMAGE4001", "요청한 임장 게시글이 모두 삭제되지 않아 삭제가 취소되었습니다." ), - IMAGE_EMPTY(HttpStatus.BAD_REQUEST, "IMAGE4002", "이미지가 첨부되지 않았습니다." ), - IMAGE_NOT_SAVE(HttpStatus.BAD_REQUEST, "IMAGE4003", "이미지 저장에 실패했습니다." ), - - //S3 에러 - //FILE_BAD_REQUEST(HttpStatus.BAD_REQUEST, "FILE400", ""), - S3_NOT_FOUND(HttpStatus.NOT_FOUND, "S34000", "해당 file이 s3에 존재하지 않습니다. 백엔드 팀에 문의바랍니다."), - S3_DELTE_FAILED(HttpStatus.NOT_FOUND, "S34000", "해당 file이 s3에 존재하지 않습니다. 백엔드 팀에 문의바랍니다."), - - //record 에러 - RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "RECORD400", "record가 존재하지 않습니다"), - - //withdraw 에러 - WITHDRAW_REASON_NOT_FOUND(HttpStatus.NOT_FOUND, "WITHDRAW400", "해당 탈퇴 사유 enum 값이 존재하지 않습니다"), - - // discord alert - DISCORD_ALERT_ERROR(HttpStatus.NOT_FOUND, "DISCORD400", "discord 알림 수신 중 오류가 발생했습니다."); - - private final HttpStatus httpStatus; - private final String code; - private final String message; - - @Override - public ErrorReasonDTO getReason() { - return ErrorReasonDTO.builder() - .message(message) - .code(code) - .isSuccess(false) - .build(); - } - - @Override - public ErrorReasonDTO getReasonHttpStatus() { - return ErrorReasonDTO.builder() - .message(message) - .code(code) - .isSuccess(false) - .httpStatus(httpStatus) - .build() - ; - } -} diff --git a/src/main/java/umc/th/juinjang/apiPayload/code/status/SuccessStatus.java b/src/main/java/umc/th/juinjang/apiPayload/code/status/SuccessStatus.java deleted file mode 100644 index ddd8b8ec..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/code/status/SuccessStatus.java +++ /dev/null @@ -1,61 +0,0 @@ -package umc.th.juinjang.apiPayload.code.status; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; -import umc.th.juinjang.apiPayload.code.BaseCode; -import umc.th.juinjang.apiPayload.code.ReasonDTO; - -@Getter -@AllArgsConstructor -public enum SuccessStatus implements BaseCode { - - // 일반적인 응답 - _OK(HttpStatus.OK, "COMMON200", "성공입니다."), - - // 멤버 관련 응답 - - // 임장 관련 응답 - LIMJANG_DELETE(HttpStatus.OK, "LIMJANG2000", "임장 게시글 삭제 성공하였습니다."), - LIMJANG_UPDATE(HttpStatus.OK, "LIMJANG2001", "임장 게시글 수정 성공하였습니다."), - - - // 스크랩 관련 응답 - _SCRAP_ACTION_SCRAP(HttpStatus.OK, "SCRAP2000", "스크랩 추가 성공하였습니다."), - _SCRAP_ACTION_UNSCRAP(HttpStatus.OK, "SCRAP2001", "스크랩 취소 성공하였습니다."), - - // 이미지 관련 응답 - IMAGE_UPDATE(HttpStatus.OK, "IMAGE2000", "이미지 업로드 성공하였습니다."), - IMAGE_DELETE(HttpStatus.OK, "IMAGE2001", "이미지 삭제 성공하였습니다."), - - // 탈퇴 관련 응답 - MEMBER_DELETE(HttpStatus.OK, "MEMBER2000", "회원 탈퇴를 성공하였습니다."), - - // discord alert - DISCORD_ALERT_SIGN_IN(HttpStatus.OK, "DISCORD200", "주인장에 신규 유저가 생겼어요!"); - - private final HttpStatus httpStatus; - private final String code; - private final String message; - - @Override - public ReasonDTO getReason() { - return ReasonDTO.builder() - .message(message) - .code(code) - .isSuccess(true) - .build(); - } - - @Override - public ReasonDTO getReasonHttpStatus() { - return ReasonDTO.builder() - .message(message) - .code(code) - .isSuccess(true) - .httpStatus(httpStatus) - .build() - ; - } -} - diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/ExceptionAdvice.java b/src/main/java/umc/th/juinjang/apiPayload/exception/ExceptionAdvice.java deleted file mode 100644 index 1ebd5ffc..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/ExceptionAdvice.java +++ /dev/null @@ -1,127 +0,0 @@ -package umc.th.juinjang.apiPayload.exception; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.context.request.ServletWebRequest; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.apiPayload.code.ErrorReasonDTO; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.GeneralException; -import jakarta.validation.ConstraintViolationException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; - -@Slf4j -@RestControllerAdvice(annotations = {RestController.class}) -public class ExceptionAdvice extends ResponseEntityExceptionHandler { - - - @org.springframework.web.bind.annotation.ExceptionHandler - public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { - String errorMessage = e.getConstraintViolations().stream() - .map(constraintViolation -> constraintViolation.getMessage()) - .findFirst() - .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); - - return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY,request); - } - - - - - @org.springframework.web.bind.annotation.ExceptionHandler - public ResponseEntity exception(Exception e, WebRequest request) { - e.printStackTrace(); - - return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(),request, e.getMessage()); - } - - @ExceptionHandler(value = GeneralException.class) - public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { - ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); - return handleExceptionInternal(generalException,errorReasonHttpStatus,null,request); - } - - private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, - HttpHeaders headers, HttpServletRequest request) { - - ApiResponse body = ApiResponse.onFailure(reason.getCode(),reason.getMessage(),null); -// e.printStackTrace(); - - WebRequest webRequest = new ServletWebRequest(request); - return super.handleExceptionInternal( - e, - body, - headers, - reason.getHttpStatus(), - webRequest - ); - } - - private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, - HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { - ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorPoint); - return super.handleExceptionInternal( - e, - body, - headers, - status, - request - ); - } - - private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus, - WebRequest request, Map errorArgs) { - ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(),errorCommonStatus.getMessage(),errorArgs); - return super.handleExceptionInternal( - e, - body, - headers, - errorCommonStatus.getHttpStatus(), - request - ); - } - - private ResponseEntity handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, - HttpHeaders headers, WebRequest request) { - ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null); - return super.handleExceptionInternal( - e, - body, - headers, - errorCommonStatus.getHttpStatus(), - request - ); - } - - @Override - protected ResponseEntity handleMethodArgumentNotValid( - MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) { - - Map errors = new LinkedHashMap<>(); - - e.getBindingResult().getFieldErrors().stream() - .forEach(fieldError -> { - String fieldName = fieldError.getField(); - String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); - errors.merge(fieldName, errorMessage, (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); - }); - - return handleExceptionInternalArgs(e,HttpHeaders.EMPTY,ErrorStatus.valueOf("_BAD_REQUEST"),request,errors); - } - -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/ChecklistHandler.java b/src/main/java/umc/th/juinjang/apiPayload/exception/handler/ChecklistHandler.java deleted file mode 100644 index b23dbe04..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/ChecklistHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package umc.th.juinjang.apiPayload.exception.handler; - -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.exception.GeneralException; - -public class ChecklistHandler extends GeneralException { - public ChecklistHandler(BaseErrorCode code) { - super(code); - } -} diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/LimjangHandler.java b/src/main/java/umc/th/juinjang/apiPayload/exception/handler/LimjangHandler.java deleted file mode 100644 index 76ace9d2..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/LimjangHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package umc.th.juinjang.apiPayload.exception.handler; - -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.exception.GeneralException; - -public class LimjangHandler extends GeneralException { - public LimjangHandler(BaseErrorCode code) { - super(code); - } -} diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/MemberHandler.java b/src/main/java/umc/th/juinjang/apiPayload/exception/handler/MemberHandler.java deleted file mode 100644 index c0028963..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/MemberHandler.java +++ /dev/null @@ -1,11 +0,0 @@ -package umc.th.juinjang.apiPayload.exception.handler; - -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.exception.GeneralException; -import umc.th.juinjang.repository.limjang.MemberRepository; - -public class MemberHandler extends GeneralException { - public MemberHandler(BaseErrorCode code) { - super(code); - } -} diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/S3Handler.java b/src/main/java/umc/th/juinjang/apiPayload/exception/handler/S3Handler.java deleted file mode 100644 index c1aab06b..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/S3Handler.java +++ /dev/null @@ -1,10 +0,0 @@ -package umc.th.juinjang.apiPayload.exception.handler; - -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.exception.GeneralException; - -public class S3Handler extends GeneralException { - public S3Handler(BaseErrorCode code) { - super(code); - } -} diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/ScrapHandler.java b/src/main/java/umc/th/juinjang/apiPayload/exception/handler/ScrapHandler.java deleted file mode 100644 index 1321798a..00000000 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/handler/ScrapHandler.java +++ /dev/null @@ -1,10 +0,0 @@ -package umc.th.juinjang.apiPayload.exception.handler; - -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.exception.GeneralException; - -public class ScrapHandler extends GeneralException { - public ScrapHandler(BaseErrorCode code) { - super(code); - } -} diff --git a/src/main/java/umc/th/juinjang/auth/config/SecurityConfig.java b/src/main/java/umc/th/juinjang/auth/config/SecurityConfig.java new file mode 100644 index 00000000..03424875 --- /dev/null +++ b/src/main/java/umc/th/juinjang/auth/config/SecurityConfig.java @@ -0,0 +1,120 @@ +package umc.th.juinjang.auth.config; + +import java.util.Arrays; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.auth.jwt.JwtAuthenticationFilter; +import umc.th.juinjang.auth.jwt.JwtExceptionFilter; +import umc.th.juinjang.auth.jwt.JwtService; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + // 공통적으로 허용되는 URL 패턴 + private static final String[] COMMON_WHITELIST_URLS = { + "/h2-console/**", + "/api/auth/regenerate-token", + "/api/auth/kakao/**", + "/api/auth/apple/**", + "/actuator/prometheus", + "/api/auth/v2/apple/**", + "/api/auth/v2/kakao/**", + "/api/members/nickname/exists", + "/api/app/version/ios", + "/api/apple/notifications/v2" + }; + // 개발 환경에서만 추가로 허용되는 URL 패턴 + private static final String[] DEV_WHITELIST_URLS = { + "/swagger-ui/**", + "/swagger/**", + "/swagger-resources/**", + "/swagger-ui.html", + "/test", + "/configuration/ui", + "/v3/api-docs/**" + }; + private final AuthenticationConfiguration authenticationConfiguration; + private final JwtService jwtService; + private final JwtExceptionFilter jwtExceptionFilter; + private final Environment environment; + + @Bean + @Order(0) + public WebSecurityCustomizer webSecurityCustomizer() { + String[] activeProfiles = environment.getActiveProfiles(); + boolean isProd = Arrays.asList(activeProfiles).contains("prod"); + + //prod아닐때 + if (!isProd) { + return web -> web.ignoring() + .requestMatchers(COMMON_WHITELIST_URLS) + .requestMatchers(DEV_WHITELIST_URLS); + } else { + return web -> web.ignoring() + .requestMatchers(COMMON_WHITELIST_URLS); + } + + } + + //선언 방식이 3.x에서 바뀜 + @Bean + AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception { + return authConfiguration.getAuthenticationManager(); + } + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement((sessionManagement) -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + // 세션을 사용하지 않는다고 설정함 + ) + .addFilter(new JwtAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtService)) + // JwtAuthenticationFilter를 필터에 넣음 + .authorizeHttpRequests((authorizeRequests) -> + authorizeRequests + .requestMatchers( + AntPathRequestMatcher.antMatcher("/api/members/nickname/exists"), + AntPathRequestMatcher.antMatcher("/api/app/version/ios"), + AntPathRequestMatcher.antMatcher("/api/apple/notifications/v2"), + AntPathRequestMatcher.antMatcher("/h2-console/**") + ).permitAll() + .requestMatchers( + AntPathRequestMatcher.antMatcher("/api/auth/**") + ).authenticated() + .anyRequest().authenticated() + + ) + .headers( + headersConfigurer -> + headersConfigurer + .frameOptions( + HeadersConfigurer.FrameOptionsConfig::sameOrigin + ) + ) + .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class); + + return http.build(); + } + +} + diff --git a/src/main/java/umc/th/juinjang/jwt/JwtAuthenticationFilter.java b/src/main/java/umc/th/juinjang/auth/jwt/JwtAuthenticationFilter.java similarity index 96% rename from src/main/java/umc/th/juinjang/jwt/JwtAuthenticationFilter.java rename to src/main/java/umc/th/juinjang/auth/jwt/JwtAuthenticationFilter.java index eec3e720..7772075f 100644 --- a/src/main/java/umc/th/juinjang/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/JwtAuthenticationFilter.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.jwt; +package umc.th.juinjang.auth.jwt; import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; @@ -11,7 +11,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import umc.th.juinjang.service.auth.JwtService; import java.io.IOException; diff --git a/src/main/java/umc/th/juinjang/jwt/JwtExceptionFilter.java b/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java similarity index 92% rename from src/main/java/umc/th/juinjang/jwt/JwtExceptionFilter.java rename to src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java index 63635af2..7ecc8ce3 100644 --- a/src/main/java/umc/th/juinjang/jwt/JwtExceptionFilter.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/JwtExceptionFilter.java @@ -1,10 +1,9 @@ -package umc.th.juinjang.jwt; +package umc.th.juinjang.auth.jwt; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -13,14 +12,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; import java.io.IOException; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; -import java.util.TooManyListenersException; @Slf4j @Component diff --git a/src/main/java/umc/th/juinjang/service/auth/JwtService.java b/src/main/java/umc/th/juinjang/auth/jwt/JwtService.java similarity index 92% rename from src/main/java/umc/th/juinjang/service/auth/JwtService.java rename to src/main/java/umc/th/juinjang/auth/jwt/JwtService.java index 26cb3d16..4b5a9965 100644 --- a/src/main/java/umc/th/juinjang/service/auth/JwtService.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/JwtService.java @@ -1,6 +1,6 @@ -package umc.th.juinjang.service.auth; +package umc.th.juinjang.auth.jwt; -import static umc.th.juinjang.utils.LoggerProvider.registerUserId; +import static umc.th.juinjang.common.LoggerProvider.registerUserId; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -20,22 +20,18 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.jwt.JwtAuthenticationFilter; -import umc.th.juinjang.model.dto.auth.TokenDto; -import umc.th.juinjang.model.dto.auth.apple.AppleClient; -import umc.th.juinjang.model.dto.auth.apple.AppleInfo; -import umc.th.juinjang.repository.limjang.MemberRepository; -import umc.th.juinjang.utils.ApplePublicKeyGenerator; -import umc.th.juinjang.utils.LoggerProvider; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.external.openfeign.apple.AppleClient; +import umc.th.juinjang.api.auth.controller.request.AppleInfo; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.external.openfeign.apple.ApplePublicKeyGenerator; @Service @RequiredArgsConstructor diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/TokenDto.java b/src/main/java/umc/th/juinjang/auth/jwt/TokenDto.java similarity index 81% rename from src/main/java/umc/th/juinjang/model/dto/auth/TokenDto.java rename to src/main/java/umc/th/juinjang/auth/jwt/TokenDto.java index e6fa2b39..108e7a47 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/TokenDto.java +++ b/src/main/java/umc/th/juinjang/auth/jwt/TokenDto.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth; +package umc.th.juinjang.auth.jwt; import lombok.Getter; diff --git a/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java b/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java new file mode 100644 index 00000000..b0d5cafe --- /dev/null +++ b/src/main/java/umc/th/juinjang/auth/jwt/UserDetailServiceImpl.java @@ -0,0 +1,34 @@ +package umc.th.juinjang.auth.jwt; + +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 lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.domain.member.model.MemberStatus; +import umc.th.juinjang.domain.member.repository.MemberRepository; + +@Slf4j +@RequiredArgsConstructor +@Service +public class UserDetailServiceImpl implements UserDetailsService { + private final MemberRepository memberRepository; + + public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { + System.out.println("로그인한 memberId : " + memberId); + UserDetails result = (UserDetails)memberRepository.findByMemberIdAndStatus( + Long.parseLong(memberId), + MemberStatus.ACTIVE + ).orElseThrow(() -> new ExceptionHandler(ErrorStatus.MEMBER_NOT_FOUND)); + log.info("UserDetails: 여기ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ"); + //로그인할 때 result.getUsername() 여기서 에러남 + // log.info("UserDetails: " + result.getUsername()); + log.info("UserDetails: " + result.toString()); + + return result; + } +} diff --git a/src/main/java/umc/th/juinjang/common/ExceptionHandler.java b/src/main/java/umc/th/juinjang/common/ExceptionHandler.java new file mode 100644 index 00000000..2460bcb1 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/ExceptionHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class ExceptionHandler extends GeneralException { + public ExceptionHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/utils/LoggerProvider.java b/src/main/java/umc/th/juinjang/common/LoggerProvider.java similarity index 94% rename from src/main/java/umc/th/juinjang/utils/LoggerProvider.java rename to src/main/java/umc/th/juinjang/common/LoggerProvider.java index af68be00..72b51402 100644 --- a/src/main/java/umc/th/juinjang/utils/LoggerProvider.java +++ b/src/main/java/umc/th/juinjang/common/LoggerProvider.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.utils; +package umc.th.juinjang.common; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/umc/th/juinjang/apiPayload/code/BaseCode.java b/src/main/java/umc/th/juinjang/common/code/BaseCode.java similarity index 72% rename from src/main/java/umc/th/juinjang/apiPayload/code/BaseCode.java rename to src/main/java/umc/th/juinjang/common/code/BaseCode.java index 41ba8ffd..d4efa8b5 100644 --- a/src/main/java/umc/th/juinjang/apiPayload/code/BaseCode.java +++ b/src/main/java/umc/th/juinjang/common/code/BaseCode.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.apiPayload.code; +package umc.th.juinjang.common.code; public interface BaseCode { diff --git a/src/main/java/umc/th/juinjang/apiPayload/code/BaseErrorCode.java b/src/main/java/umc/th/juinjang/common/code/BaseErrorCode.java similarity index 75% rename from src/main/java/umc/th/juinjang/apiPayload/code/BaseErrorCode.java rename to src/main/java/umc/th/juinjang/common/code/BaseErrorCode.java index 6c4ac93e..cdf8d89c 100644 --- a/src/main/java/umc/th/juinjang/apiPayload/code/BaseErrorCode.java +++ b/src/main/java/umc/th/juinjang/common/code/BaseErrorCode.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.apiPayload.code; +package umc.th.juinjang.common.code; public interface BaseErrorCode { diff --git a/src/main/java/umc/th/juinjang/apiPayload/code/ErrorReasonDTO.java b/src/main/java/umc/th/juinjang/common/code/ErrorReasonDTO.java similarity index 89% rename from src/main/java/umc/th/juinjang/apiPayload/code/ErrorReasonDTO.java rename to src/main/java/umc/th/juinjang/common/code/ErrorReasonDTO.java index 01cafee4..8ed9e639 100644 --- a/src/main/java/umc/th/juinjang/apiPayload/code/ErrorReasonDTO.java +++ b/src/main/java/umc/th/juinjang/common/code/ErrorReasonDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.apiPayload.code; +package umc.th.juinjang.common.code; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/umc/th/juinjang/apiPayload/code/ReasonDTO.java b/src/main/java/umc/th/juinjang/common/code/ReasonDTO.java similarity index 88% rename from src/main/java/umc/th/juinjang/apiPayload/code/ReasonDTO.java rename to src/main/java/umc/th/juinjang/common/code/ReasonDTO.java index dccf21a1..37d64d8d 100644 --- a/src/main/java/umc/th/juinjang/apiPayload/code/ReasonDTO.java +++ b/src/main/java/umc/th/juinjang/common/code/ReasonDTO.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.apiPayload.code; +package umc.th.juinjang.common.code; import lombok.Builder; diff --git a/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java b/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java new file mode 100644 index 00000000..bccdc6fa --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java @@ -0,0 +1,150 @@ +package umc.th.juinjang.common.code.status; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.code.ErrorReasonDTO; + +@Getter +@AllArgsConstructor +public enum ErrorStatus implements BaseErrorCode { + // 가장 일반적인 응답 + _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."), + _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."), + _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), + _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + // For test + TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트"), + + // Member Error + MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "해당하는 사용자를 찾을 수 없습니다."), + MEMBER_EMAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4002", "전달받은 사용자의 이메일이 없습니다."), + MEMBER_NOT_FOUND_IN_KAKAO(HttpStatus.BAD_REQUEST, "MEMBER4003", "APPLE로 회원가입한 회원입니다."), + ALREADY_LOGOUT(HttpStatus.BAD_REQUEST, "MEMBER4004", "이미 로그아웃 되었습니다."), + UNCORRECTED_INFO(HttpStatus.BAD_REQUEST, "MEMBER4005", "올바르지 않은 정보입니다."), + MEMBER_NOT_FOUND_IN_APPLE(HttpStatus.BAD_REQUEST, "MEMBER4006", "KAKAO로 회원가입한 회원입니다."), + ALREADY_MEMBER(HttpStatus.BAD_REQUEST, "MEMBER4007", "이미 가입된 회원입니다."), + // 카카오 target id 에러 + EMPTY_TARGET_ID(HttpStatus.BAD_REQUEST, "MEMBER4008", "카카오 target id가 비어있습니다."), + UNCORRECTED_TARGET_ID(HttpStatus.BAD_REQUEST, "MEMBER4009", + "target id와 회원 정보가 일치하지 않습니다. 올바르지 않은 카카오 target id 입니다."), + NOT_UNLINK_KAKAO(HttpStatus.BAD_REQUEST, "MEMBER4010", "카카오 연결 끊기에 실패하였습니다."), + FAILED_TO_LOGIN(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER4011", "잘못된 정보입니다. 서버 관리자에게 문의 바랍니다."), + FAILED_TO_SIGNUP(HttpStatus.INTERNAL_SERVER_ERROR, "MEMBER4012", "회원가입에 실패했습니다. 서버 관리자에게 문의 바랍니다."), + + //로그아웃 에러 + FAILED_TO_LOAD_PRIVATE_KEY(HttpStatus.BAD_REQUEST, "REVOKE4002", "private key 실패"), + LOGOUT_FAILED(HttpStatus.BAD_REQUEST, "REVOKE4002", "private key 실패"), + // nickname 에러 + NICKNAME_EMPTY(HttpStatus.BAD_REQUEST, "NICKNAME4001", "닉네임이 존재하지 않습니다. 닉네임을 입력해주세요."), + ALREADY_NICKNAME(HttpStatus.BAD_REQUEST, "NICKNAME4002", "이미 존재하는 닉네임입니다."), + + // Limjang Error + LIMJANG_POST_REQUEST_NULL(HttpStatus.BAD_REQUEST, "LIMJANG4001", "입력 값이 모두 넘어오지 않았습니다. 누락된 값이 있는지 다시 확인해주세요."), + LIMJANG_POST_TYPE_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4002", + "거래목적, 매물유형, 가격유형 입력값 중 하나가 정해지지 않은 값입니다. 다시 확인해주세요."), + LIMJANG_POST_PRICE_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4003", "전달된 가격이 잘못되었습니다. 입력값을 확인해주세요."), + LIMJANG_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG404", "해당 임장이 존재하지 않습니다."), + + LIMJANG_DELETE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LIMJANG4005", "전달된 ID의 값이 DB에 존재하지 않습니다. 전달 값을 다시 확인해주세요."), + LIMJANG_DELETE_NOT_COMPLETE(HttpStatus.BAD_REQUEST, "LIMJANG4006", + "요청한 임장 게시글이 모두 삭제되지 않아 삭제가 취소되었습니다. 다시 시도하거나 백엔드 팀에 문의바랍니다."), + LIMJANG_UPDATE_PRICETYPE_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4007", "가격유형 입력값이 잘못되었습니다. 다시 확인해주세요."), + LIMJANG_REQUEST_SORT_ERROR(HttpStatus.BAD_REQUEST, "LIMJANG4008", "요청한 정렬 방식이 지정되지 않은 값입니다. 다시 확인해주세요."), + LIMJANG_UPDATE_FAILED(HttpStatus.BAD_REQUEST, "LIMJANG4009", "요청한 임장 업데이트가 정상적으로 진행되지 않았습니다. 백엔드 팀에 문의 바랍니다."), + + // LimjangPrice Error + LIMJANGPRICE_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "LIMJANGPRICE4000", "해당 임장가격 레코드가 존재하지 않습니다."), + LIMJANGPRICE_NULL_ERROR(HttpStatus.BAD_REQUEST, "LIMJANGPRICE4000", "임장 가격 저장에 실패했습니다. 임장 가격이 null입니다."), + + // scrap + _SCRAP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4000", "해당 게시글이 DB에 존재하지 않습니다. 관리자에게 문의 바랍니다."), + _SCRAP_SCRAP_FAILD(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4001", "스크랩 등록 실패. 재시도하거나 관리자에게 문의 바랍니다."), + _SCRAP_UNSCRAP_FAILD(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4002", "스크랩 취소 실패. 재시도하거나 관리자에게 문의 바랍니다."), + _SCRAP_ALREADY_SCRAPED(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4003", "이미 스크랩된 게시글입니다."), + _SCRAP_ALREADY_UNSCRAPED(HttpStatus.INTERNAL_SERVER_ERROR, "SCRAP4004", "이미 스크랩 취소된 게시글입니다."), + + CHECKLIST_TYPE_ERROR(HttpStatus.BAD_REQUEST, "CHECKLIST400", "정해지지 않은 요청값입니다. 다시 확인해주세요."), + CHECKLIST_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "CHECKLIST404", "해당 체크리스트 질문 또는 답변이 존재하지 않습니다."), + + REPORT_NOTFOUND_ERROR(HttpStatus.BAD_REQUEST, "REPORT404", "해당 리포트가 존재하지 않습니다."), + + //JWT 토큰 에러 + TOKEN_UNAUTHORIZED(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE, "TOKEN400", "유효하지 않거나 만료된 토큰입니다."), + TOKEN_EMPTY(HttpStatus.BAD_REQUEST, "TOKEN401", "토큰값이 존재하지 않습니다."), + REFRESH_TOKEN_UNAUTHORIZED(HttpStatus.I_AM_A_TEAPOT, "TOKEN402", "유효하지 않은 Refresh Token입니다. 다시 로그인하세요."), + ACCESS_TOKEN_AUTHORIZED(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE, "TOKEN403", "유효하지 않은 Access Token입니다."), + REFRESH_TOKEN_AUTHORIZED(HttpStatus.UNAUTHORIZED, "TOKEN404", "유효한 Refresh Token입니다."), + APPLE_ID_TOKEN_EMPTY(HttpStatus.BAD_REQUEST, "TOKEN405", "ID TOKEN값이 존재하지 않습니다."), + INVALID_APPLE_ID_TOKEN(HttpStatus.UNAUTHORIZED, "TOKEN406", "Apple OAuth Identity Token 값이 올바르지 않습니다."), + PUBLICKEY_ERROR_IN_APPLE_LOGIN(HttpStatus.UNAUTHORIZED, "TOKEN407", "Apple OAuth 로그인 중 public key 생성에 문제가 발생했습니다."), + INVALID_APPLE_ID_TOKEN_INFO(HttpStatus.UNAUTHORIZED, "TOKEN407", "Apple id_token 값의 alg, kid 정보가 올바르지 않습니다."), + + // Image 에러 + IMAGE_DELETE_NOT_FOUND(HttpStatus.BAD_REQUEST, "IMAGE4000", "전달된 ID의 값이 DB에 존재하지 않습니다. 전달 값을 다시 확인해주세요."), + IMAGE_DELETE_NOT_COMPLETE(HttpStatus.BAD_REQUEST, "IMAGE4001", "요청한 임장 게시글이 모두 삭제되지 않아 삭제가 취소되었습니다."), + IMAGE_EMPTY(HttpStatus.BAD_REQUEST, "IMAGE4002", "이미지가 첨부되지 않았습니다."), + IMAGE_NOT_SAVE(HttpStatus.BAD_REQUEST, "IMAGE4003", "이미지 저장에 실패했습니다."), + + //S3 에러 + //FILE_BAD_REQUEST(HttpStatus.BAD_REQUEST, "FILE400", ""), + S3_NOT_FOUND(HttpStatus.NOT_FOUND, "S34000", "해당 file이 s3에 존재하지 않습니다. 백엔드 팀에 문의바랍니다."), + S3_DELTE_FAILED(HttpStatus.NOT_FOUND, "S34000", "해당 file이 s3에 존재하지 않습니다. 백엔드 팀에 문의바랍니다."), + + //record 에러 + RECORD_NOT_FOUND(HttpStatus.NOT_FOUND, "RECORD400", "record가 존재하지 않습니다"), + + //withdraw 에러 + WITHDRAW_REASON_NOT_FOUND(HttpStatus.NOT_FOUND, "WITHDRAW400", "해당 탈퇴 사유 enum 값이 존재하지 않습니다"), + + // discord alert + DISCORD_ALERT_ERROR(HttpStatus.NOT_FOUND, "DISCORD400", "discord 알림 수신 중 오류가 발생했습니다."), + + // PencilAccount alert + PENCIL_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST, "ACCOUNT4000", "멤버에 해당하는 계좌가 존재하지 않습니다."), + + // Apple ALERT + APPLE_VERIFICATION_ERROR(HttpStatus.BAD_REQUEST, "APPLE4000", "애플 인증 관련 에러가 발생했습니다."), + + SHAREDNOTE_NOT_FOUND(HttpStatus.NOT_FOUND, "SHAREDNOTE4000", "해당하는 공유노트가 존재하지 않습니다."), + SHAREDNOTE_NOT_ENOUGH_PENCIL(HttpStatus.BAD_REQUEST, "SHAREDNOTE4001", "보유한 연필 수가 부족합니다."), + SHAREDNOTE_CONFLICT(HttpStatus.CONFLICT, "SHAREDNOTE4002", "이미 구매한 노트입니다."), + SHAREDNOTE_DEADLOCK(HttpStatus.LOCKED, "SHAREDNOTE4003", "잠시 후 다시 시도해주세요. 현재 다른 요청이 처리 중입니다."), + SHAREDNOTE_TYPE_ERROR(HttpStatus.BAD_REQUEST, "SHAREDNOTE4004", "유효하지 않은 마이 노트 요청 타입입니다."), + SHARED_NOT_ALLOWED(HttpStatus.BAD_REQUEST, "SAHREDNOTE4005", "공유가 금지된 노트입니다."), + SHAREDNOTE_DELETED_RECENTLY(HttpStatus.BAD_REQUEST, "SHAREDNOTE4006", "삭제한지 6개월이 지나지 않아 다시 공유할 수 없습니다."), + SHAREDNOTE_ALREADY_EXISTS(HttpStatus.CONFLICT, "SHAREDNOTE4007", "이미 공유된 노트입니다."), + + // LikedNote + LIKEDNOTE_CONFLICT(HttpStatus.CONFLICT, "LIKEDNOTE4000", "이미 좋아요한 노트입니다"), + LIKEDNOTE_NOT_FOUND(HttpStatus.NOT_FOUND, "LIKEDNOTE4001", "이미 취소했거나 좋아요한 적이 없습니다."), + + // Terms_Agreement + TERMS_AGREEMENT_DUPLICATED(HttpStatus.BAD_REQUEST, "TERMS_AGREEMENT_4000", "이미 약관에 동의하였습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(false) + .httpStatus(httpStatus) + .build() + ; + } +} diff --git a/src/main/java/umc/th/juinjang/common/code/status/SuccessStatus.java b/src/main/java/umc/th/juinjang/common/code/status/SuccessStatus.java new file mode 100644 index 00000000..7f34b0bc --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/code/status/SuccessStatus.java @@ -0,0 +1,66 @@ +package umc.th.juinjang.common.code.status; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import org.springframework.http.HttpStatus; + +import umc.th.juinjang.common.code.BaseCode; +import umc.th.juinjang.common.code.ReasonDTO; + +@Getter +@AllArgsConstructor +public enum SuccessStatus implements BaseCode { + + // 일반적인 응답 + _OK(HttpStatus.OK, "COMMON200", "성공입니다."), + _CREATED(HttpStatus.CREATED, "COMMON201", "리소스가 생성되었습니다."), + + // 멤버 관련 응답 + + // 임장 관련 응답 + LIMJANG_DELETE(HttpStatus.OK, "LIMJANG2000", "임장 게시글 삭제 성공하였습니다."), + LIMJANG_UPDATE(HttpStatus.OK, "LIMJANG2001", "임장 게시글 수정 성공하였습니다."), + + // 스크랩 관련 응답 + _SCRAP_ACTION_SCRAP(HttpStatus.OK, "SCRAP2000", "스크랩 추가 성공하였습니다."), + _SCRAP_ACTION_UNSCRAP(HttpStatus.OK, "SCRAP2001", "스크랩 취소 성공하였습니다."), + + // 이미지 관련 응답 + IMAGE_UPDATE(HttpStatus.OK, "IMAGE2000", "이미지 업로드 성공하였습니다."), + IMAGE_DELETE(HttpStatus.OK, "IMAGE2001", "이미지 삭제 성공하였습니다."), + + // 탈퇴 관련 응답 + MEMBER_DELETE(HttpStatus.OK, "MEMBER2000", "회원 탈퇴를 성공하였습니다."), + + // discord alert + DISCORD_ALERT_SIGN_IN(HttpStatus.OK, "DISCORD200", "주인장에 신규 유저가 생겼어요!"), + + // 공유 노트 응답 + SHARED_NOTE_DELETE(HttpStatus.OK, "SHRARED_NOTE200", "노트 공유가 중지 되었습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ReasonDTO getReason() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .build(); + } + + @Override + public ReasonDTO getReasonHttpStatus() { + return ReasonDTO.builder() + .message(message) + .code(code) + .isSuccess(true) + .httpStatus(httpStatus) + .build() + ; + } +} + diff --git a/src/main/java/umc/th/juinjang/common/exception/ExceptionAdvice.java b/src/main/java/umc/th/juinjang/common/exception/ExceptionAdvice.java new file mode 100644 index 00000000..0139235e --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/ExceptionAdvice.java @@ -0,0 +1,127 @@ +package umc.th.juinjang.common.exception; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.common.code.ErrorReasonDTO; +import umc.th.juinjang.common.code.status.ErrorStatus; + +@Slf4j +@RestControllerAdvice(annotations = {RestController.class}) +public class ExceptionAdvice extends ResponseEntityExceptionHandler { + + @ExceptionHandler + public ResponseEntity validation(ConstraintViolationException e, WebRequest request) { + String errorMessage = e.getConstraintViolations().stream() + .map(constraintViolation -> constraintViolation.getMessage()) + .findFirst() + .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생")); + + return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request); + } + + @ExceptionHandler + public ResponseEntity exception(Exception e, WebRequest request) { + e.printStackTrace(); + + return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, + ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage()); + } + + @ExceptionHandler(value = GeneralException.class) + public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) { + ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus(); + return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request); + } + + private ResponseEntity handleExceptionInternal(Exception e, ErrorReasonDTO reason, + HttpHeaders headers, HttpServletRequest request) { + + ApiResponse body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null); + // e.printStackTrace(); + + WebRequest webRequest = new ServletWebRequest(request); + return super.handleExceptionInternal( + e, + body, + headers, + reason.getHttpStatus(), + webRequest + ); + } + + private ResponseEntity handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), + errorPoint); + return super.handleExceptionInternal( + e, + body, + headers, + status, + request + ); + } + + private ResponseEntity handleExceptionInternalArgs(Exception e, HttpHeaders headers, + ErrorStatus errorCommonStatus, + WebRequest request, Map errorArgs) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), + errorArgs); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } + + private ResponseEntity handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, + HttpHeaders headers, WebRequest request) { + ApiResponse body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), + null); + return super.handleExceptionInternal( + e, + body, + headers, + errorCommonStatus.getHttpStatus(), + request + ); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) { + + Map errors = new LinkedHashMap<>(); + + e.getBindingResult().getFieldErrors().stream() + .forEach(fieldError -> { + String fieldName = fieldError.getField(); + String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse(""); + errors.merge(fieldName, errorMessage, + (existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage); + }); + + return handleExceptionInternalArgs(e, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"), request, errors); + } + +} diff --git a/src/main/java/umc/th/juinjang/apiPayload/exception/GeneralException.java b/src/main/java/umc/th/juinjang/common/exception/GeneralException.java similarity index 70% rename from src/main/java/umc/th/juinjang/apiPayload/exception/GeneralException.java rename to src/main/java/umc/th/juinjang/common/exception/GeneralException.java index 98dd7beb..7c269fe1 100644 --- a/src/main/java/umc/th/juinjang/apiPayload/exception/GeneralException.java +++ b/src/main/java/umc/th/juinjang/common/exception/GeneralException.java @@ -1,9 +1,9 @@ -package umc.th.juinjang.apiPayload.exception; +package umc.th.juinjang.common.exception; import lombok.AllArgsConstructor; import lombok.Getter; -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.code.ErrorReasonDTO; +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.code.ErrorReasonDTO; @Getter @AllArgsConstructor diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/AppleHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/AppleHandler.java new file mode 100644 index 00000000..6a39fdee --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/AppleHandler.java @@ -0,0 +1,11 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class AppleHandler extends GeneralException { + public AppleHandler(BaseErrorCode code) { + super(code); + } + +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/ChecklistHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/ChecklistHandler.java new file mode 100644 index 00000000..6128abb6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/ChecklistHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class ChecklistHandler extends GeneralException { + public ChecklistHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/LikedNoteHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/LikedNoteHandler.java new file mode 100644 index 00000000..838c96ac --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/LikedNoteHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class LikedNoteHandler extends GeneralException { + public LikedNoteHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/LimjangHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/LimjangHandler.java new file mode 100644 index 00000000..c6a22796 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/LimjangHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class LimjangHandler extends GeneralException { + public LimjangHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/MemberHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/MemberHandler.java new file mode 100644 index 00000000..96076f6a --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/MemberHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class MemberHandler extends GeneralException { + public MemberHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/PencilAccountHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/PencilAccountHandler.java new file mode 100644 index 00000000..f3a492e8 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/PencilAccountHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class PencilAccountHandler extends GeneralException { + public PencilAccountHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/S3Handler.java b/src/main/java/umc/th/juinjang/common/exception/handler/S3Handler.java new file mode 100644 index 00000000..09a67d33 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/S3Handler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class S3Handler extends GeneralException { + public S3Handler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/ScrapHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/ScrapHandler.java new file mode 100644 index 00000000..6efb3185 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/ScrapHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class ScrapHandler extends GeneralException { + public ScrapHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/SharedNoteHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/SharedNoteHandler.java new file mode 100644 index 00000000..11c4a7b1 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/SharedNoteHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class SharedNoteHandler extends GeneralException { + public SharedNoteHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/common/redis/RedisKeyFactory.java b/src/main/java/umc/th/juinjang/common/redis/RedisKeyFactory.java new file mode 100644 index 00000000..dc927932 --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/redis/RedisKeyFactory.java @@ -0,0 +1,15 @@ +package umc.th.juinjang.common.redis; + +public class RedisKeyFactory { + + public static final String VIEW_COUNT = "viewcount:sharedNoteId:"; + private static final String VIEW_HISTORY = "viewed:sharedNoteId:"; + + public static String viewCountKey(long sharedNoteId) { + return VIEW_COUNT + sharedNoteId; + } + + public static String viewHistoryKey(long sharedNoteId, long memberId) { + return VIEW_HISTORY + sharedNoteId + ":member:" + memberId; + } +} diff --git a/src/main/java/umc/th/juinjang/validation/annotation/VaildPriceListSize.java b/src/main/java/umc/th/juinjang/common/validation/annotation/VaildPriceListSize.java similarity index 83% rename from src/main/java/umc/th/juinjang/validation/annotation/VaildPriceListSize.java rename to src/main/java/umc/th/juinjang/common/validation/annotation/VaildPriceListSize.java index 3805350a..b1bf15f5 100644 --- a/src/main/java/umc/th/juinjang/validation/annotation/VaildPriceListSize.java +++ b/src/main/java/umc/th/juinjang/common/validation/annotation/VaildPriceListSize.java @@ -1,9 +1,9 @@ -package umc.th.juinjang.validation.annotation; +package umc.th.juinjang.common.validation.annotation; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; -import umc.th.juinjang.validation.validator.PriceListVaildation; +import umc.th.juinjang.common.validation.validator.PriceListVaildation; @Documented @Constraint(validatedBy = PriceListVaildation.class) diff --git a/src/main/java/umc/th/juinjang/validation/validator/PriceListVaildation.java b/src/main/java/umc/th/juinjang/common/validation/validator/PriceListVaildation.java similarity index 86% rename from src/main/java/umc/th/juinjang/validation/validator/PriceListVaildation.java rename to src/main/java/umc/th/juinjang/common/validation/validator/PriceListVaildation.java index 75c6cf40..e1209537 100644 --- a/src/main/java/umc/th/juinjang/validation/validator/PriceListVaildation.java +++ b/src/main/java/umc/th/juinjang/common/validation/validator/PriceListVaildation.java @@ -1,11 +1,11 @@ -package umc.th.juinjang.validation.validator; +package umc.th.juinjang.common.validation.validator; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import umc.th.juinjang.validation.annotation.VaildPriceListSize; +import umc.th.juinjang.common.validation.annotation.VaildPriceListSize; @Component @RequiredArgsConstructor diff --git a/src/main/java/umc/th/juinjang/config/ApiFilterConfig.java b/src/main/java/umc/th/juinjang/config/ApiFilterConfig.java deleted file mode 100644 index e0ba5428..00000000 --- a/src/main/java/umc/th/juinjang/config/ApiFilterConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package umc.th.juinjang.config; - -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; -import umc.th.juinjang.monitoring.ApiLoggerFilter; - -@Configuration -@Slf4j -public class ApiFilterConfig { - - @Value("${logging.api.excluded-paths}") - private List excludedUrls; - - @Bean - public FilterRegistrationBean loggingFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new ApiLoggerFilter(excludedUrls)); - registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 순서 설정 - registrationBean.setUrlPatterns(List.of("/*")); - return registrationBean; - } -} diff --git a/src/main/java/umc/th/juinjang/config/AppVersionProperties.java b/src/main/java/umc/th/juinjang/config/AppVersionProperties.java new file mode 100644 index 00000000..7bad9534 --- /dev/null +++ b/src/main/java/umc/th/juinjang/config/AppVersionProperties.java @@ -0,0 +1,19 @@ +package umc.th.juinjang.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.Setter; + +@Configuration +@ConfigurationProperties(prefix = "app.version") +@Getter +@Setter +public class AppVersionProperties { + + /** + * IOS 의 어플 최신 버전 + */ + private String ios; +} diff --git a/src/main/java/umc/th/juinjang/config/JpaConfig.java b/src/main/java/umc/th/juinjang/config/JpaConfig.java new file mode 100644 index 00000000..f7887790 --- /dev/null +++ b/src/main/java/umc/th/juinjang/config/JpaConfig.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaConfig { +} diff --git a/src/main/java/umc/th/juinjang/config/RedisConfig.java b/src/main/java/umc/th/juinjang/config/RedisConfig.java new file mode 100644 index 00000000..80638375 --- /dev/null +++ b/src/main/java/umc/th/juinjang/config/RedisConfig.java @@ -0,0 +1,19 @@ +package umc.th.juinjang.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + return template; + } +} diff --git a/src/main/java/umc/th/juinjang/config/SecurityConfig.java b/src/main/java/umc/th/juinjang/config/SecurityConfig.java deleted file mode 100644 index 766cb4c7..00000000 --- a/src/main/java/umc/th/juinjang/config/SecurityConfig.java +++ /dev/null @@ -1,100 +0,0 @@ -package umc.th.juinjang.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.core.env.Environment; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.Customizer; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -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.configuration.WebSecurityCustomizer; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import umc.th.juinjang.jwt.JwtAuthenticationFilter; -import umc.th.juinjang.jwt.JwtExceptionFilter; -import umc.th.juinjang.service.auth.JwtService; - -import java.util.Arrays; - -@Configuration -@EnableWebSecurity -@RequiredArgsConstructor -public class SecurityConfig { - private final AuthenticationConfiguration authenticationConfiguration; - - private final JwtService jwtService; - - private final JwtExceptionFilter jwtExceptionFilter; - - private final Environment environment; - @Bean - @Order(0) - public WebSecurityCustomizer webSecurityCustomizer(){ - String[] activeProfiles = environment.getActiveProfiles(); - boolean isProd = Arrays.asList(activeProfiles).contains("prod"); - - //prod아닐때 - if (!isProd) { - return web -> web.ignoring() - .requestMatchers("/swagger-ui/**", "/swagger/**", "/swagger-resources/**", "/swagger-ui.html", "/test", - "/configuration/ui", "/v3/api-docs/**", "/h2-console/**", "/api/auth/regenerate-token", - "/api/auth/kakao/**", "/api/auth/apple/**", "/actuator/prometheus", - "/api/auth/v2/apple/**", "/api/auth/v2/kakao/**"); - } - else { - return web -> web.ignoring() - .requestMatchers("/h2-console/**", "/api/auth/regenerate-token", - "/api/auth/kakao/**", "/api/auth/apple/**", "/actuator/prometheus", - "/api/auth/v2/apple/**", "/api/auth/v2/kakao/**"); - } - - } - - //선언 방식이 3.x에서 바뀜 - @Bean AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception - { return authConfiguration.getAuthenticationManager(); } - - @Bean - protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - - http - .csrf(AbstractHttpConfigurer::disable) - .formLogin(Customizer.withDefaults()) - .sessionManagement((sessionManagement) -> - sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) -// 세션을 사용하지 않는다고 설정함 - ) - .addFilter(new JwtAuthenticationFilter(authenticationManager(authenticationConfiguration),jwtService)) -// JwtAuthenticationFilter를 필터에 넣음 - .authorizeHttpRequests((authorizeRequests) -> - authorizeRequests - .requestMatchers( - AntPathRequestMatcher.antMatcher("/api/auth/**") - ).authenticated() - .requestMatchers( - AntPathRequestMatcher.antMatcher("/h2-console/**") - ).permitAll() - - .anyRequest().authenticated() - - ) - .headers( - headersConfigurer -> - headersConfigurer - .frameOptions( - HeadersConfigurer.FrameOptionsConfig::sameOrigin - ) - ) - .addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class); - - return http.build(); - } - -} - diff --git a/src/main/java/umc/th/juinjang/controller/MemberController.java b/src/main/java/umc/th/juinjang/controller/MemberController.java deleted file mode 100644 index c103563d..00000000 --- a/src/main/java/umc/th/juinjang/controller/MemberController.java +++ /dev/null @@ -1,64 +0,0 @@ -package umc.th.juinjang.controller; - -import io.swagger.v3.oas.annotations.Operation; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.model.dto.member.MemberAgreeVersionPostRequest; -import umc.th.juinjang.model.dto.member.MemberRequestDto; -import umc.th.juinjang.model.dto.member.MemberResponseDto; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.service.member.MemberService; - -import static umc.th.juinjang.apiPayload.code.status.ErrorStatus.NICKNAME_EMPTY; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -@Validated -public class MemberController { - - private final MemberService memberService; - - @CrossOrigin - @Operation(summary = "닉네임 설정") - @PatchMapping("/nickname") - public ApiResponse patchNickname (@AuthenticationPrincipal Member member, @RequestBody MemberRequestDto memberRequestDto) { - if(!memberRequestDto.getNickname().isEmpty()) { - MemberResponseDto.nicknameDto result = memberService.patchNickname(member, memberRequestDto); - return ApiResponse.onSuccess(result); - } else - throw new ExceptionHandler(NICKNAME_EMPTY); - } - - @CrossOrigin - @Operation(summary = "프로필 조회") - @GetMapping("/profile") - public ApiResponse getProfile (@AuthenticationPrincipal Member member) { - MemberResponseDto.profileDto result = memberService.getProfile(member); - return ApiResponse.onSuccess(result); - } - - @CrossOrigin - @Operation(summary = "프로필 이미지 수정") - @PatchMapping("/profile/image") - public ApiResponse getProfile (@AuthenticationPrincipal Member member, @RequestPart MultipartFile multipartFile) { - if (multipartFile == null || multipartFile.isEmpty()) - throw new ExceptionHandler(ErrorStatus.IMAGE_EMPTY); - MemberResponseDto.profileDto result = memberService.updateProfileImage(member, multipartFile); - return ApiResponse.onSuccess(result); - } - - @Operation(summary = "약관 동의 버전 전송") - @PatchMapping ("/members/terms") - public ApiResponse createMemberAgreeVersion(@AuthenticationPrincipal Member member, @RequestBody @Valid MemberAgreeVersionPostRequest memberAgreeVersionPostRequest) { - memberService.createMemberAgreeVersion(member, memberAgreeVersionPostRequest); - return ApiResponse.onSuccess(null); - } -} diff --git a/src/main/java/umc/th/juinjang/controller/OAuthController.java b/src/main/java/umc/th/juinjang/controller/OAuthController.java deleted file mode 100644 index 8085b852..00000000 --- a/src/main/java/umc/th/juinjang/controller/OAuthController.java +++ /dev/null @@ -1,197 +0,0 @@ -package umc.th.juinjang.controller; - -import io.micrometer.common.lang.Nullable; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.util.StringUtils; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import umc.th.juinjang.apiPayload.ApiResponse; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.SuccessStatus; -import umc.th.juinjang.model.dto.auth.LoginResponseDto; -import umc.th.juinjang.model.dto.auth.LoginResponseVersion2Dto; -import umc.th.juinjang.model.dto.auth.WithdrawReasonRequestDto; -import umc.th.juinjang.model.dto.auth.apple.AppleLoginRequestDto; -import umc.th.juinjang.model.dto.auth.apple.AppleSignUpRequestDto; -import umc.th.juinjang.model.dto.auth.apple.AppleSignUpRequestVersion2Dto; -import umc.th.juinjang.model.dto.auth.kakao.KakaoLoginRequestDto; -import umc.th.juinjang.model.dto.auth.kakao.KakaoSignUpRequestDto; -import umc.th.juinjang.model.dto.auth.kakao.KakaoSignUpRequestVersion2Dto; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.service.withdraw.WithdrawService; -import umc.th.juinjang.service.auth.OAuthService; - -import static umc.th.juinjang.apiPayload.code.status.ErrorStatus.*; - -@Slf4j -@RestController -@RequestMapping("/api/auth") -@RequiredArgsConstructor -@Validated -public class OAuthController { - - private final OAuthService oauthService; - private final WithdrawService withdrawService; - - // 카카오 로그인 - // 프론트 측에서 전달해준 사용자 정보로 토큰 발급 - @PostMapping("/kakao/login") - public ApiResponse kakaoLogin(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoLogin(targetId, kakaoReqDto)); - } - - // 카카오 로그인 (회원가입) - @PostMapping("/kakao/signup") - public ApiResponse kakaoSignUp(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoSignUpRequestDto kakaoSignUpReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoSignUp(targetId, kakaoSignUpReqDto)); - } - - //V2 - // 카카오 로그인 - @PostMapping("/v2/kakao/login") - public ApiResponse kakaoLoginVersion2(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoLoginRequestDto kakaoReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoLoginVersion2(targetId, kakaoReqDto)); - } - - // 카카오 로그인 (회원가입) - @PostMapping("/v2/kakao/signup") - public ApiResponse kakaoSignUpVersion2(@RequestHeader("target-id") String kakaoTargetId, @RequestBody @Validated KakaoSignUpRequestVersion2Dto kakaoSignUpReqDto) { - Long targetId; - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } - - targetId = Long.parseLong(kakaoTargetId); - return ApiResponse.onSuccess(oauthService.kakaoSignUpVersion2(targetId, kakaoSignUpReqDto)); - } - - - // refreshToken으로 accessToken 재발급 - // Authorization : Bearer Token에 refreshToken 담기 - @PostMapping("/regenerate-token") - public ApiResponse regenerateAccessToken(HttpServletRequest request) { - String accessToken = request.getHeader("Authorization"); - String refreshToken = request.getHeader("Refresh-Token"); - - if (StringUtils.hasText(accessToken) && accessToken.startsWith("Bearer ") && StringUtils.hasText(refreshToken) && refreshToken.startsWith("Bearer ")) { - LoginResponseDto result = oauthService.regenerateAccessToken(accessToken.substring(7), refreshToken.substring(7)); - return ApiResponse.onSuccess(result); - } else - throw new ExceptionHandler(TOKEN_EMPTY); - } - - // 로그아웃 -> refresh 토큰 만료 - @PostMapping("/logout") - public ApiResponse logout(HttpServletRequest request) { - String token = request.getHeader("Refresh-Token"); - - if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { - String result = oauthService.logout(token.substring(7)); - return ApiResponse.onSuccess(result); - } else - throw new ExceptionHandler(TOKEN_EMPTY); - } - - // 애플 로그인 - // 클라이언트에서 identity token 값 받아오기 - // 사용자가 입력한 정보를 바탕으로 Apple ID servers 에게 Identity Token 발급 요청 (프론트가) -> 이를 우리 서버가 가져오는 것 - // Identity Token 값을 바탕으로 사용자 식별 & refresh, access Token 발급해주고 DB 저장 (로그인하기) - - // 로그인 - @PostMapping("/apple/login") - public ApiResponse appleLogin(@RequestBody @Validated AppleLoginRequestDto appleReqDto) { - if (appleReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleLogin(appleReqDto)); - } - - @PostMapping("/apple/signup") - public ApiResponse appleSignUp(@RequestBody @Validated AppleSignUpRequestDto appleSignUpReqDto) { - if (appleSignUpReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleSignUp(appleSignUpReqDto)); - } - - //V2 - @PostMapping("/v2/apple/login") - public ApiResponse appleLoginVersion2(@RequestBody @Validated AppleLoginRequestDto appleReqDto) { - if (appleReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleLoginVersion2(appleReqDto)); - } - - @PostMapping("/v2/apple/signup") - public ApiResponse appleSignUpVersion2(@RequestBody @Validated AppleSignUpRequestVersion2Dto appleSignUpReqDto) { - if (appleSignUpReqDto.getIdentityToken() == null) - throw new ExceptionHandler(APPLE_ID_TOKEN_EMPTY); - return ApiResponse.onSuccess(oauthService.appleSignUpVersion2(appleSignUpReqDto)); - } - - - // 카카오 탈퇴 - @DeleteMapping("/withdraw/kakao") - public ApiResponse kakaoWithdraw(@AuthenticationPrincipal Member member, @RequestHeader("target-id") String kakaoTargetId, @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto) { - Long targetId; - - if(kakaoTargetId == null) { - throw new ExceptionHandler(EMPTY_TARGET_ID); - } else { - targetId = Long.parseLong(kakaoTargetId); - if(!targetId.equals(member.getKakaoTargetId())) { - throw new ExceptionHandler(UNCORRECTED_TARGET_ID); - } - } - - // 카카오 계정 연결 끊기 - boolean isUnlink = oauthService.kakaoWithdraw(member, targetId); - - // 탈퇴 사유 추가 - if(withdrawReasonReqDto.getWithdrawReason() != null) { - withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); - } - - // 사용자 정보 삭제 (DB) - if (!isUnlink) { - throw new ExceptionHandler(NOT_UNLINK_KAKAO); - } - - return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); - } - - - // 애플 탈퇴 - @DeleteMapping("/withdraw/apple") - public ApiResponse withdraw(@AuthenticationPrincipal Member member, - @Nullable@RequestHeader("X-Apple-Code") final String code, @RequestBody WithdrawReasonRequestDto withdrawReasonReqDto){ - oauthService.appleWithdraw(member, code); - - // 탈퇴 사유 추가 - if(withdrawReasonReqDto.getWithdrawReason() != null) { - withdrawService.addWithdrawReason(withdrawReasonReqDto.getWithdrawReason()); - } - - return ApiResponse.onSuccess(SuccessStatus.MEMBER_DELETE); - } - -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/converter/checklist/ChecklistAnswerAndReportConverter.java b/src/main/java/umc/th/juinjang/converter/checklist/ChecklistAnswerAndReportConverter.java deleted file mode 100644 index 41bdde2a..00000000 --- a/src/main/java/umc/th/juinjang/converter/checklist/ChecklistAnswerAndReportConverter.java +++ /dev/null @@ -1,57 +0,0 @@ -package umc.th.juinjang.converter.checklist; - -import umc.th.juinjang.converter.limjang.LimjangDetailConverter; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerAndReportResponseDTO; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerResponseDTO; -import umc.th.juinjang.model.dto.checklist.ReportResponseDTO; -import umc.th.juinjang.model.dto.limjang.response.LimjangDetailResponseDTO; -import umc.th.juinjang.model.entity.ChecklistAnswer; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Report; - -import java.util.List; -import java.util.stream.Collectors; - -public class ChecklistAnswerAndReportConverter { - - public static ChecklistAnswerAndReportResponseDTO toDto(List answerList, Report report, Limjang limjang) { - List answerDtoList = answerList.stream() - .map(answer -> new ChecklistAnswerResponseDTO.AnswerDto( - answer.getAnswerId(), - answer.getQuestionId().getQuestionId(), - //answer.getQuestionId().getCategory(), - answer.getLimjangId().getLimjangId(), - answer.getAnswer(), - answer.getQuestionId().getAnswerType())) - .collect(Collectors.toList()); - - ReportResponseDTO.ReportDTO reportDto = new ReportResponseDTO.ReportDTO( - report.getReportId(), - report.getIndoorKeyword(), - report.getPublicSpaceKeyword(), - report.getLocationConditionsKeyword(), - report.getIndoorRate(), - report.getPublicSpaceRate(), - report.getLocationConditionsRate(), - report.getTotalRate()); - - LimjangDetailResponseDTO.DetailDto detailDto = LimjangDetailConverter.toDetail(limjang, limjang.getLimjangPrice()); - - return new ChecklistAnswerAndReportResponseDTO(answerDtoList, new ReportResponseDTO(reportDto, detailDto)); - } - public static ReportResponseDTO toReportDto(Report report, Limjang limjang) { - ReportResponseDTO.ReportDTO reportDTO = ReportResponseDTO.ReportDTO.builder() - .reportId(report.getReportId()) - .indoorKeyWord(report.getIndoorKeyword()) - .publicSpaceKeyWord(report.getPublicSpaceKeyword()) - .locationConditionsWord(report.getLocationConditionsKeyword()) - .indoorRate(report.getIndoorRate()) - .publicSpaceRate(report.getPublicSpaceRate()) - .locationConditionsRate(report.getLocationConditionsRate()) - .totalRate(report.getTotalRate()) - .build(); - LimjangDetailResponseDTO.DetailDto detailDto = LimjangDetailConverter.toDetail(limjang, limjang.getLimjangPrice()); - return new ReportResponseDTO(reportDTO, detailDto); - } - -} diff --git a/src/main/java/umc/th/juinjang/converter/image/ImageListConverter.java b/src/main/java/umc/th/juinjang/converter/image/ImageListConverter.java deleted file mode 100644 index 5f02c573..00000000 --- a/src/main/java/umc/th/juinjang/converter/image/ImageListConverter.java +++ /dev/null @@ -1,27 +0,0 @@ -package umc.th.juinjang.converter.image; - -import java.util.List; -import java.util.Optional; -import umc.th.juinjang.model.dto.image.ImageListResponseDTO; -import umc.th.juinjang.model.dto.image.ImageUploadResponseDTO; -import umc.th.juinjang.model.entity.Image; - -public class ImageListConverter { - - public static ImageListResponseDTO.ImagesListDTO toImageListDto(List images){ - List imageDtoList = - images.stream().map(ImageListConverter::toImageDto).toList(); - - return ImageListResponseDTO.ImagesListDTO - .builder() - .images(imageDtoList) - .build(); - } - - public static ImageListResponseDTO.ImageDTO toImageDto(Image image){ - return ImageListResponseDTO.ImageDTO.builder() - .imageId(image.getImageId()) - .imageUrl(image.getImageUrl()) - .build(); - } -} diff --git a/src/main/java/umc/th/juinjang/converter/image/ImageUploadConverter.java b/src/main/java/umc/th/juinjang/converter/image/ImageUploadConverter.java deleted file mode 100644 index 93301f6f..00000000 --- a/src/main/java/umc/th/juinjang/converter/image/ImageUploadConverter.java +++ /dev/null @@ -1,16 +0,0 @@ -package umc.th.juinjang.converter.image; - -import umc.th.juinjang.model.dto.image.ImageUploadRequestDTO; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; - -public class ImageUploadConverter { - - public static Image toImageDto(String fileName, Limjang limjang){ - return Image.builder() - .imageUrl(fileName) - .limjangId(limjang) - .build(); - } - -} diff --git a/src/main/java/umc/th/juinjang/model/entity/ChecklistAnswer.java b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistAnswer.java similarity index 86% rename from src/main/java/umc/th/juinjang/model/entity/ChecklistAnswer.java rename to src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistAnswer.java index 4daecc3d..a3ac0c8e 100644 --- a/src/main/java/umc/th/juinjang/model/entity/ChecklistAnswer.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistAnswer.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.checklist.model; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -13,7 +13,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.common.BaseEntity; @Entity @Getter diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionCategory.java b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionCategory.java similarity index 72% rename from src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionCategory.java rename to src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionCategory.java index b2b97f64..415b3ff7 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionCategory.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionCategory.java @@ -1,9 +1,8 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.checklist.model; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.ChecklistHandler; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; import java.util.Arrays; diff --git a/src/main/java/umc/th/juinjang/model/entity/ChecklistQuestionShort.java b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionShort.java similarity index 74% rename from src/main/java/umc/th/juinjang/model/entity/ChecklistQuestionShort.java rename to src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionShort.java index 3217cba3..9337e32f 100644 --- a/src/main/java/umc/th/juinjang/model/entity/ChecklistQuestionShort.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionShort.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.checklist.model; import jakarta.persistence.*; @@ -9,10 +9,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.common.BaseEntity; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionCategory; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionType; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; +import umc.th.juinjang.domain.common.BaseEntity; @Entity @Getter diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionType.java b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionType.java similarity index 78% rename from src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionType.java rename to src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionType.java index fffd52bb..c4726741 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionType.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionType.java @@ -1,7 +1,7 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.checklist.model; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.ChecklistHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; import java.util.Arrays; diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionVersion.java b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionVersion.java similarity index 78% rename from src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionVersion.java rename to src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionVersion.java index 28b6602d..d9ee43dc 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/ChecklistQuestionVersion.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/model/ChecklistQuestionVersion.java @@ -1,7 +1,7 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.checklist.model; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.ChecklistHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.ChecklistHandler; import java.util.Arrays; diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangCheckListVersion.java b/src/main/java/umc/th/juinjang/domain/checklist/model/LimjangCheckListVersion.java similarity index 68% rename from src/main/java/umc/th/juinjang/model/entity/enums/LimjangCheckListVersion.java rename to src/main/java/umc/th/juinjang/domain/checklist/model/LimjangCheckListVersion.java index 99af9f32..d2eb7810 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangCheckListVersion.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/model/LimjangCheckListVersion.java @@ -1,6 +1,8 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.checklist.model; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; public enum LimjangCheckListVersion { LIMJANG, diff --git a/src/main/java/umc/th/juinjang/repository/checklist/ChecklistAnswerRepository.java b/src/main/java/umc/th/juinjang/domain/checklist/repository/ChecklistAnswerRepository.java similarity index 81% rename from src/main/java/umc/th/juinjang/repository/checklist/ChecklistAnswerRepository.java rename to src/main/java/umc/th/juinjang/domain/checklist/repository/ChecklistAnswerRepository.java index 35c4c1d6..3f8fe06a 100644 --- a/src/main/java/umc/th/juinjang/repository/checklist/ChecklistAnswerRepository.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/repository/ChecklistAnswerRepository.java @@ -1,12 +1,12 @@ -package umc.th.juinjang.repository.checklist; +package umc.th.juinjang.domain.checklist.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.ChecklistAnswer; -import umc.th.juinjang.model.entity.Limjang; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.limjang.model.Limjang; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/repository/checklist/ChecklistQuestionRepository.java b/src/main/java/umc/th/juinjang/domain/checklist/repository/ChecklistQuestionRepository.java similarity index 60% rename from src/main/java/umc/th/juinjang/repository/checklist/ChecklistQuestionRepository.java rename to src/main/java/umc/th/juinjang/domain/checklist/repository/ChecklistQuestionRepository.java index 1bcfa819..484aa466 100644 --- a/src/main/java/umc/th/juinjang/repository/checklist/ChecklistQuestionRepository.java +++ b/src/main/java/umc/th/juinjang/domain/checklist/repository/ChecklistQuestionRepository.java @@ -1,11 +1,7 @@ -package umc.th.juinjang.repository.checklist; +package umc.th.juinjang.domain.checklist.repository; import org.springframework.data.jpa.repository.JpaRepository; -import umc.th.juinjang.model.entity.ChecklistQuestionShort; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionCategory; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; - -import java.util.List; +import umc.th.juinjang.domain.checklist.model.ChecklistQuestionShort; public interface ChecklistQuestionRepository extends JpaRepository { // List findChecklistQuestionsByPurpose(LimjangPurpose purpose); diff --git a/src/main/java/umc/th/juinjang/domain/common/BaseEntity.java b/src/main/java/umc/th/juinjang/domain/common/BaseEntity.java new file mode 100644 index 00000000..6a369e09 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/common/BaseEntity.java @@ -0,0 +1,39 @@ +package umc.th.juinjang.domain.common; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PrePersist; +import lombok.Getter; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + if (this.createdAt == null) { + this.createdAt = LocalDateTime.now(); + } + } + + protected void setCreatedAt(LocalDateTime createdAt) { + if (createdAt != null) { + this.createdAt = createdAt; + } + } + +} diff --git a/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNote.java b/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNote.java new file mode 100644 index 00000000..098e7dc5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNote.java @@ -0,0 +1,52 @@ +package umc.th.juinjang.domain.flag.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class FlagSharedNote extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long sharedNoteFlagId; + + @Enumerated(EnumType.STRING) + private FlagSharedNoteType type; + + @Enumerated(EnumType.STRING) + private FlagSharedNoteStatus status; + + private Long flagged_by_member_id; + + private Long sharedNoteId; + + @Builder + private FlagSharedNote(FlagSharedNoteType type, FlagSharedNoteStatus status, Long flagged_by_member_id, + Long sharedNoteId) { + this.type = type; + this.status = status; + this.flagged_by_member_id = flagged_by_member_id; + this.sharedNoteId = sharedNoteId; + } + + public static FlagSharedNote create(FlagSharedNoteType type, Long flagged_by_member_id, + Long sharedNoteId) { + return FlagSharedNote.builder() + .type(type) + .status(FlagSharedNoteStatus.RECEIVED) + .flagged_by_member_id(flagged_by_member_id) + .sharedNoteId(sharedNoteId) + .build(); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNoteStatus.java b/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNoteStatus.java new file mode 100644 index 00000000..4a2bcc38 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNoteStatus.java @@ -0,0 +1,17 @@ +package umc.th.juinjang.domain.flag.model; + +import lombok.Getter; + +@Getter +public enum FlagSharedNoteStatus { + RECEIVED("접수"), + REVIEWED("관리자가 확인"), + RESOLVED("조치 취함"); + + private final String description; + + FlagSharedNoteStatus(String description) { + this.description = description; + } + +} diff --git a/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNoteType.java b/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNoteType.java new file mode 100644 index 00000000..da41fee5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/flag/model/FlagSharedNoteType.java @@ -0,0 +1,20 @@ +package umc.th.juinjang.domain.flag.model; + +import lombok.Getter; + +@Getter +public enum FlagSharedNoteType { + FALSE_INFORMATION("부정확한 정보 제공"), + ILLEGAL_BROKERING("거래 유도/불법 중개 행위"), + INAPPROPRIATE_CONTENT("부적절한 내용 포함"), + PERSONAL_INFORMATION_LEAK("개인정보 노출"), + INFRINGEMENT("타인의 권리 침해"), + SPAM("반복성 스팸/도배"), + ETC("기타"); + + private final String description; + + FlagSharedNoteType(String description) { + this.description = description; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/flag/repository/FlagSharedNoteRepository.java b/src/main/java/umc/th/juinjang/domain/flag/repository/FlagSharedNoteRepository.java new file mode 100644 index 00000000..8686d356 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/flag/repository/FlagSharedNoteRepository.java @@ -0,0 +1,8 @@ +package umc.th.juinjang.domain.flag.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import umc.th.juinjang.domain.flag.model.FlagSharedNote; + +public interface FlagSharedNoteRepository extends JpaRepository { +} diff --git a/src/main/java/umc/th/juinjang/model/entity/Image.java b/src/main/java/umc/th/juinjang/domain/image/model/Image.java similarity index 73% rename from src/main/java/umc/th/juinjang/model/entity/Image.java rename to src/main/java/umc/th/juinjang/domain/image/model/Image.java index 5972145c..126c5218 100644 --- a/src/main/java/umc/th/juinjang/model/entity/Image.java +++ b/src/main/java/umc/th/juinjang/domain/image/model/Image.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.image.model; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -14,7 +14,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.common.BaseEntity; @Entity @Getter @@ -36,4 +37,10 @@ public class Image extends BaseEntity { @Column(nullable = false) private String imageUrl; + public static Image create(String imageUrl, Limjang limjang) { + return Image.builder() + .imageUrl(imageUrl) + .limjangId(limjang) + .build(); + } } diff --git a/src/main/java/umc/th/juinjang/domain/image/repository/ImageQueryDslRepository.java b/src/main/java/umc/th/juinjang/domain/image/repository/ImageQueryDslRepository.java new file mode 100644 index 00000000..de3e9690 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/image/repository/ImageQueryDslRepository.java @@ -0,0 +1,11 @@ +package umc.th.juinjang.domain.image.repository; + +import java.util.List; + +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; + +public interface ImageQueryDslRepository { + + List findAllFirstCreatedImagePerNote(List limjangs); +} diff --git a/src/main/java/umc/th/juinjang/domain/image/repository/ImageQueryDslRepositoryImpl.java b/src/main/java/umc/th/juinjang/domain/image/repository/ImageQueryDslRepositoryImpl.java new file mode 100644 index 00000000..5791656b --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/image/repository/ImageQueryDslRepositoryImpl.java @@ -0,0 +1,40 @@ +package umc.th.juinjang.domain.image.repository; + +import static umc.th.juinjang.domain.image.model.QImage.*; + +import java.util.List; + +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.JPQLTemplates; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; + +public class ImageQueryDslRepositoryImpl implements ImageQueryDslRepository { + private final JPAQueryFactory queryFactory; + + public ImageQueryDslRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em); + } + + @Override + public List findAllFirstCreatedImagePerNote(List limjangs) { + return queryFactory + .selectFrom(image) + .where(image.imageId.in( + subqueryFirstCreatedImagePerNote(limjangs) + )) + .fetch(); + } + + private JPQLQuery subqueryFirstCreatedImagePerNote(List limjangs) { + return JPAExpressions + .select(image.imageId.min()) + .from(image) + .where(image.limjangId.in(limjangs)) + .groupBy(image.limjangId); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/image/repository/ImageRepository.java b/src/main/java/umc/th/juinjang/domain/image/repository/ImageRepository.java new file mode 100644 index 00000000..b4883b01 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/image/repository/ImageRepository.java @@ -0,0 +1,23 @@ +package umc.th.juinjang.domain.image.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.Limjang; + +public interface ImageRepository extends JpaRepository, ImageQueryDslRepository { + + List findImagesByLimjangId(Limjang limjang); + + @Transactional + @Modifying + @Query(value = "DELETE FROM image i WHERE i.limjang_id = :limjangId", nativeQuery = true) + void deleteByLimjangId(@Param("limjangId") Long limjangId); + +} diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java b/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java new file mode 100644 index 00000000..063f51f8 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/Address.java @@ -0,0 +1,90 @@ +package umc.th.juinjang.domain.limjang.model; + +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.hibernate.annotations.Comment; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Address extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long addressId; + + @Comment("기존 임장 테이블의 address") + private String roadAddress; + + private String addressDetail; + + @Comment("법정동 코드") + private String bcode; + + private String sido; + + private String sigungo; + + @Comment("읍/면") + private String bname1; + + @Comment("동") + private String bname2; + + @Builder + private Address(String roadAddress, String addressDetail, String bcode, String sido, String sigungo, + String bname1, String bname2) { + this.roadAddress = roadAddress; + this.addressDetail = addressDetail; + this.bcode = bcode; + this.sido = sido; + this.sigungo = sigungo; + this.bname1 = bname1; + this.bname2 = bname2; + } + + public static Address create(String roadAddress, String addressDetail, String bcode, String sido, String sigungo, + String bname1, String bname2) { + return Address.builder() + .roadAddress(roadAddress) + .addressDetail(addressDetail) + .bcode(bcode) + .sido(sido) + .sigungo(sigungo) + .bname1(bname1) + .bname2(bname2) + .build(); + } + + public String getShortAddress() { + return Stream.of(sigungo, bname1, bname2) + .filter(s -> s != null && !s.isBlank()) + .collect(Collectors.joining(" ")); + } + + public void update(Address newAddress) { + this.roadAddress = newAddress.roadAddress; + this.addressDetail = newAddress.addressDetail; + this.bcode = newAddress.bcode; + this.sido = newAddress.sido; + this.sigungo = newAddress.sigungo; + this.bname1 = newAddress.bname1; + this.bname2 = newAddress.bname2; + } + + public String getFullAddress() { + return this.roadAddress + " " + this.getAddressDetail(); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java b/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java new file mode 100644 index 00000000..4e218ef2 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/Limjang.java @@ -0,0 +1,189 @@ +package umc.th.juinjang.domain.limjang.model; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.ColumnDefault; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.checklist.model.ChecklistAnswer; +import umc.th.juinjang.domain.common.BaseEntity; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.record.model.Record; +import umc.th.juinjang.domain.report.model.Report; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +//@Where(clause = "deleted = false") +public class Limjang extends BaseEntity { + + @Id + @Column(name = "limjang_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long limjangId; + + // 회원 ID + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member memberId; + + // 가격 ID + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "price_id", referencedColumnName = "price_id") + private LimjangPrice limjangPrice; + + // 거래 목적 + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private LimjangPurpose purpose; + + // 매물 유형 + @Enumerated(EnumType.STRING) + private LimjangPropertyType propertyType; + + // 가격 유형 + @Enumerated(EnumType.STRING) + private LimjangPriceType priceType; + + // 도로명 주소 + // @Column(nullable = false) + private String address; + + private String addressDetail; + + // 보상 연필 + private Integer rewardPencil; + + private Integer pyong; + + private String floor; + + // 집 별명 + @Column(nullable = false) + private String nickname; + + @Column(columnDefinition = "text") + private String memo; + + // 양방향 매핑 + @OneToMany(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) + @BatchSize(size = 100) + private List answerList = new ArrayList<>(); + + @OneToOne(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) + private Report report; + + @OneToMany(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) + private List recordList = new ArrayList<>(); + + @OneToMany(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) + @BatchSize(size = 100) + private List imageList = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "address_id") + private Address addressEntity; + + @Column(name = "record_count") + @ColumnDefault("0") //default 0 + private int recordCount; + + @Column(nullable = false, name = "deleted") + private boolean deleted = Boolean.FALSE; + + @Column(nullable = false) + @ColumnDefault("false") + private boolean isSharable = false; + + public void saveMemberAndPrice(Member member, LimjangPrice limjangPrice) { + this.limjangPrice = limjangPrice; + this.memberId = member; + } + + public void updateLimjang(String address, String addressDetail, String nickname, LimjangPriceType priceType) { + this.address = address; + this.addressDetail = addressDetail; + this.nickname = nickname; + this.priceType = priceType; + } + + public void updateMemo(String memo) { + this.memo = memo; + } + + public void saveImages(Image image) { + this.imageList.add(image); + } + + public String getDefaultImage() { + return this.imageList.isEmpty() ? null : this.imageList.get(0).getImageUrl(); + } + + public void updateRewardPencil(Integer rewardPencil) { + this.rewardPencil = rewardPencil; + } + + public void updateSharable(boolean state) { + this.isSharable = state; + } + + @Builder + private Limjang(Member member, LimjangPrice limjangPrice, LimjangPurpose purpose, + LimjangPropertyType propertyType, LimjangPriceType priceType, + String nickname, Address addressEntity, int pyong, String floor) { + this.memberId = member; + this.limjangPrice = limjangPrice; + this.purpose = purpose; + this.propertyType = propertyType; + this.priceType = priceType; + this.nickname = nickname; + this.addressEntity = addressEntity; + this.pyong = pyong; + this.floor = floor; + } + + public static Limjang create(Member member, LimjangPrice price, LimjangPurpose purpose, + LimjangPropertyType propertyType, LimjangPriceType priceType, String nickname, Address addressEntity, int pyong, + String floor) { + return Limjang.builder() + .memberId(member) + .limjangPrice(price) + .purpose(purpose) + .propertyType(propertyType) + .priceType(priceType) + .nickname(nickname) + .addressEntity(addressEntity) + .pyong(pyong) + .floor(floor) + .build(); + } + + public void updateNote(String nickname, LimjangPriceType limjangPriceType, String floor, int pyong) { + this.nickname = nickname; + this.priceType = limjangPriceType; + this.floor = floor; + this.pyong = pyong; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java new file mode 100644 index 00000000..d6756aa6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPrice.java @@ -0,0 +1,60 @@ +package umc.th.juinjang.domain.limjang.model; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class LimjangPrice extends BaseEntity { + + @Id + @Column(name = "price_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long priceId; + + private String marketPrice; + + private String sellingPrice; + + private String depositPrice; + + private String monthlyRent; + + private String pullRent; + + @OneToOne(mappedBy = "limjangPrice", cascade = CascadeType.ALL, orphanRemoval = true) + private Limjang limjang; + + public void updateLimjangPrice(LimjangPrice newLimjangPrice) { + this.marketPrice = newLimjangPrice.getMarketPrice(); + this.sellingPrice = newLimjangPrice.getSellingPrice(); + this.depositPrice = newLimjangPrice.getDepositPrice(); + this.monthlyRent = newLimjangPrice.getMonthlyRent(); + this.pullRent = newLimjangPrice.getPullRent(); + } + + public String getPrice(LimjangPriceType priceType, LimjangPurpose purpose) { + if (purpose == LimjangPurpose.INVESTMENT) { + return this.getMarketPrice(); + } else if (purpose == LimjangPurpose.RESIDENTIAL_PURPOSE) { + return switch (priceType) { + case SALE -> this.getSellingPrice(); + case PULL_RENT -> this.getPullRent(); + case MONTHLY_RENT -> this.getDepositPrice(); + case MARKET_PRICE -> this.getMarketPrice(); + }; + } + return null; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPriceType.java b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPriceType.java new file mode 100644 index 00000000..e6109b9f --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPriceType.java @@ -0,0 +1,32 @@ +package umc.th.juinjang.domain.limjang.model; + +import java.util.Arrays; + +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; + +public enum LimjangPriceType { + SALE(0), // 매매 + PULL_RENT(1), // 전세 + MONTHLY_RENT(2), //월세 + MARKET_PRICE(3); // 실거래가 + + private final int value; + + LimjangPriceType(int value) { + this.value = value; + } + + // 숫자 리턴 + public int getValue() { + return value; + } + + public static LimjangPriceType find(int inputValue) { + return Arrays.stream(LimjangPriceType.values()) + .filter(it -> it.value == inputValue) + .findAny() + .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_POST_TYPE_ERROR)); + } + +} diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPropertyType.java b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPropertyType.java similarity index 73% rename from src/main/java/umc/th/juinjang/model/entity/enums/LimjangPropertyType.java rename to src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPropertyType.java index 08a133a4..cf1b0bdf 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPropertyType.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPropertyType.java @@ -1,9 +1,8 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.limjang.model; import java.util.Arrays; -import umc.th.juinjang.apiPayload.code.BaseErrorCode; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; // 매물유형 public enum LimjangPropertyType { diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPurpose.java b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPurpose.java similarity index 70% rename from src/main/java/umc/th/juinjang/model/entity/enums/LimjangPurpose.java rename to src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPurpose.java index a013d5c2..432a817a 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPurpose.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/model/LimjangPurpose.java @@ -1,9 +1,8 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.limjang.model; import java.util.Arrays; -import org.hibernate.sql.ComparisonRestriction.Operator; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; public enum LimjangPurpose { diff --git a/src/main/java/umc/th/juinjang/domain/limjang/repository/AddressRepository.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/AddressRepository.java new file mode 100644 index 00000000..f9592740 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/AddressRepository.java @@ -0,0 +1,8 @@ +package umc.th.juinjang.domain.limjang.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import umc.th.juinjang.domain.limjang.model.Address; + +public interface AddressRepository extends JpaRepository { +} diff --git a/src/main/java/umc/th/juinjang/repository/limjang/dto/LimjangMainListDBResponsetDto.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangMainListDBResponsetDto.java similarity index 63% rename from src/main/java/umc/th/juinjang/repository/limjang/dto/LimjangMainListDBResponsetDto.java rename to src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangMainListDBResponsetDto.java index ef80c06f..711a87c0 100644 --- a/src/main/java/umc/th/juinjang/repository/limjang/dto/LimjangMainListDBResponsetDto.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangMainListDBResponsetDto.java @@ -1,8 +1,8 @@ -package umc.th.juinjang.repository.limjang.dto; +package umc.th.juinjang.domain.limjang.repository; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.LimjangPrice; -import umc.th.juinjang.model.entity.Report; +import umc.th.juinjang.domain.image.model.Image; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.report.model.Report; public record LimjangMainListDBResponsetDto( Long limjangId, diff --git a/src/main/java/umc/th/juinjang/repository/limjang/LimjangPriceRepository.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangPriceRepository.java similarity index 58% rename from src/main/java/umc/th/juinjang/repository/limjang/LimjangPriceRepository.java rename to src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangPriceRepository.java index c4ef9b73..8ed71426 100644 --- a/src/main/java/umc/th/juinjang/repository/limjang/LimjangPriceRepository.java +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangPriceRepository.java @@ -1,12 +1,10 @@ -package umc.th.juinjang.repository.limjang; +package umc.th.juinjang.domain.limjang.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; public interface LimjangPriceRepository extends JpaRepository { diff --git a/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangQueryDslRepository.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangQueryDslRepository.java new file mode 100644 index 00000000..c1d1e9f9 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangQueryDslRepository.java @@ -0,0 +1,19 @@ +package umc.th.juinjang.domain.limjang.repository; + +import java.util.List; + +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; + +public interface LimjangQueryDslRepository { + + List searchLimjangsWhereDeletedIsFalse(Member member, String keyword); + + List findAllByMemberAndDeletedIsFalseWithReportAndLimjangPriceOrderByUpdateAtLimit5(Member member); + + List findAllByMemberAndDeletedIsFalseOrderByParam(Member member, LimjangSortOptions sort); + + List findAllByMemberAndDeletedIsFalseOrderByParamV2(Member member, LimjangSortOptions sort, + String keyword); +} diff --git a/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangQueryDslRepositoryImpl.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangQueryDslRepositoryImpl.java new file mode 100644 index 00000000..1943c935 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangQueryDslRepositoryImpl.java @@ -0,0 +1,131 @@ +package umc.th.juinjang.domain.limjang.repository; + +import static com.querydsl.core.types.Order.DESC; +import static umc.th.juinjang.domain.image.model.QImage.image; +import static umc.th.juinjang.domain.limjang.model.QLimjang.limjang; +import static umc.th.juinjang.domain.limjang.model.QLimjangPrice.limjangPrice; +import static umc.th.juinjang.domain.report.model.QReport.report; +import static umc.th.juinjang.domain.limjang.model.QAddress.address; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPQLTemplates; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; + +import java.util.ArrayList; +import java.util.List; + +import umc.th.juinjang.api.limjang.controller.parameter.LimjangSortOptions; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; + +public class LimjangQueryDslRepositoryImpl implements LimjangQueryDslRepository { + private final JPAQueryFactory queryFactory; + + public LimjangQueryDslRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em); + } + + @Override + public List searchLimjangsWhereDeletedIsFalse(Member member, String keyword) { + String rKeyword = removeKeywordBlank(keyword); + return queryFactory + .selectFrom(limjang) + .leftJoin(limjang.report, report).fetchJoin() + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .leftJoin(limjang.imageList, image).fetchJoin() + .where(limjang.deleted.isFalse()) + .where(limjang.memberId.eq(member), + keywordOf( + removeBlank(limjang.nickname).containsIgnoreCase(rKeyword), + removeBlank(limjang.address).containsIgnoreCase(rKeyword), + removeBlank(limjang.addressDetail).containsIgnoreCase(rKeyword) + )) + .fetch(); + } + + private String removeKeywordBlank(String keyword) { + return keyword.replaceAll(" ", ""); + } + + public List findAllByMemberAndDeletedIsFalseOrderByParam(Member member, LimjangSortOptions sort) { + return queryFactory + .selectFrom(limjang) + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .leftJoin(limjang.report, report).fetchJoin() + .leftJoin(limjang.imageList, image).fetchJoin() + .where(limjang.memberId.eq(member)) + .where(limjang.deleted.isFalse()) + .orderBy(getOrderByLimjangSortOptions(sort)) + .fetch(); + } + + @Override + public List findAllByMemberAndDeletedIsFalseOrderByParamV2(Member member, LimjangSortOptions sort, + String keyword) { + return queryFactory + .selectFrom(limjang) + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .join(limjang.addressEntity, address).fetchJoin() + .leftJoin(limjang.report, report).fetchJoin() + .where(limjang.memberId.eq(member)) + .where(limjang.deleted.isFalse()) + .where(keywordConditionForSearch(keyword)) + .orderBy(getOrderByLimjangSortOptions(sort)) + .fetch(); + } + + private BooleanExpression keywordConditionForSearch(String keyword) { + if (keyword == null || keyword.isBlank()) { + return null; + } + return keywordOf( + removeBlank(limjang.nickname).containsIgnoreCase(keyword), + removeBlank(address.roadAddress).containsIgnoreCase(keyword), + removeBlank(address.addressDetail).containsIgnoreCase(keyword) + ); + } + + private OrderSpecifier[] getOrderByLimjangSortOptions(LimjangSortOptions sort) { + List orders = new ArrayList<>(); + switch (sort) { + case UPDATED -> orders.add(new OrderSpecifier(DESC, limjang.updatedAt)); + case STAR -> { + orders.add( + new OrderSpecifier<>(DESC, report.totalRate.coalesce(0f), OrderSpecifier.NullHandling.NullsLast)); + orders.add(new OrderSpecifier<>(DESC, limjang.createdAt)); + } + case CREATED -> orders.add(new OrderSpecifier<>(DESC, limjang.createdAt)); + } + return orders.toArray(new OrderSpecifier[orders.size()]); + } + + private BooleanExpression keywordOf(BooleanExpression... conditions) { + BooleanExpression result = null; + for (BooleanExpression condition : conditions) { + result = result == null ? condition : result.or(condition); + } + return result; + } + + private StringExpression removeBlank(StringExpression origin) { + return Expressions.stringTemplate("function('replace', {0}, ' ', '')", origin); + } + + @Override + public List findAllByMemberAndDeletedIsFalseWithReportAndLimjangPriceOrderByUpdateAtLimit5(Member member) { + return queryFactory + .selectFrom(limjang) + .leftJoin(limjang.report, report).fetchJoin() + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .where(limjang.memberId.eq(member)) + .where(limjang.deleted.isFalse()) + .orderBy(limjang.updatedAt.desc()) + .limit(5) + .fetch(); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangRepository.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangRepository.java new file mode 100644 index 00000000..8341a45c --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/LimjangRepository.java @@ -0,0 +1,70 @@ +package umc.th.juinjang.domain.limjang.repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; + +@Repository +public interface LimjangRepository extends JpaRepository, LimjangQueryDslRepository { + + @Query(value = "SELECT * FROM limjang l WHERE l.member_id = :memberId", nativeQuery = true) + List findLimjangByMemberIdIgnoreDeleted(@Param("memberId") Long memberId); + + List findAllByLimjangIdInAndMemberIdAndDeletedIsFalse(List id, Member member); + + @Modifying + @Query("UPDATE Limjang l SET l.deleted = true WHERE l.limjangId in :ids") + void softDeleteByIds(@Param("ids") List ids); + + @Modifying + @Query(value = "DELETE FROM limjang l WHERE l.deleted = true AND l.updated_at < :dateTime", nativeQuery = true) + void hardDelete(@Param("dateTime") LocalDateTime dateTime); + + Optional findLimjangByLimjangIdAndMemberIdAndDeletedIsFalse(Long limjangId, Member member); + + @Modifying + @Transactional + @Query("UPDATE Limjang l SET l.recordCount = l.recordCount + 1 WHERE l.limjangId = :limjangId") + void incrementRecordCount(@Param("limjangId") Long limjangId); + + @Modifying + @Transactional + @Query("UPDATE Limjang l SET l.memo = :memo WHERE l.limjangId = :limjangId") + void updateMemo(@Param("limjangId") Long limjangId, @Param("memo") String memo); + + @Transactional + @Modifying + @Query(value = "DELETE FROM limjang l WHERE l.member_id = :memberId", nativeQuery = true) + void deleteAllByMemberId(@Param("memberId") Long memberId); + + @Query("SELECT l FROM Limjang l join fetch l.limjangPrice WHERE l.limjangId = :id AND l.memberId = :member AND l.deleted = false") + Optional findByLimjangIdAndMemberIdWithLimjangPriceAndDeletedIsFalse(@Param("id") Long id, + @Param("member") Member member); + + @Query("SELECT l FROM Limjang l join fetch l.limjangPrice left join fetch l.report WHERE l.limjangId = :id AND l.memberId = :member AND l.deleted = false") + Optional findByLimjangIdAndMemberAndDeletedIsFalse(@Param("id") Long id, @Param("member") Member member); + + @Query("SELECT l FROM Limjang l WHERE l.limjangId = :id AND l.deleted = false") + Optional findByLimjangIdAndDeletedIsFalse(@Param("id") Long id); + + @Query("SELECT l FROM Limjang l join fetch l.addressEntity join fetch l.limjangPrice WHERE l.limjangId = :id AND l.deleted = false") + Optional findByIdWithAddressAndNotePriceWhereDeletedIsFalse(@Param("id") Long id); + + @Query("SELECT l FROM Limjang l join fetch l.addressEntity join fetch l.limjangPrice left join fetch l.report WHERE l.memberId = :member AND l.deleted = false AND l.isSharable = true") + List findAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalse( + @Param("member") Member member); + + @Query("SELECT l FROM Limjang l join fetch l.addressEntity join fetch l.limjangPrice left join fetch l.report WHERE l.memberId = :member AND l.deleted = false AND l.addressEntity.bcode is not null AND l.isSharable = true") + List findAllByMemberWithAddressAndNotePriceWhereIsSharableIsTrueAndDeletedIsFalseAndAddressBcodeIsNotNull( + @Param("member") Member member); +} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/domain/limjang/repository/NotePriceFactory.java b/src/main/java/umc/th/juinjang/domain/limjang/repository/NotePriceFactory.java new file mode 100644 index 00000000..2258e725 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/limjang/repository/NotePriceFactory.java @@ -0,0 +1,53 @@ +package umc.th.juinjang.domain.limjang.repository; + +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LimjangHandler; +import umc.th.juinjang.domain.limjang.model.LimjangPrice; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; + +public class NotePriceFactory { + public static LimjangPrice create(LimjangPurpose purpose, LimjangPriceType priceType, String price, + String monthlyRent) { + checkValidPriceTypeAndPrice(priceType, monthlyRent); + if (purpose == LimjangPurpose.INVESTMENT) { + return createInvestmentPrice(price); + } else { + return createResidencePrice(priceType, price, monthlyRent); + } + } + + private static void checkValidPriceTypeAndPrice(LimjangPriceType priceType, String monthlyRent) { + if (priceType == LimjangPriceType.MONTHLY_RENT && monthlyRent == null) { + throw new LimjangHandler(ErrorStatus.LIMJANG_POST_PRICE_ERROR); + } + + if (priceType != LimjangPriceType.MONTHLY_RENT && monthlyRent != null) { + throw new LimjangHandler(ErrorStatus.LIMJANG_POST_PRICE_ERROR); + } + } + + private static LimjangPrice createInvestmentPrice(String price) { + return LimjangPrice.builder() + .marketPrice(price) + .build(); + } + + private static LimjangPrice createResidencePrice(LimjangPriceType priceType, String price, String monthlyRent) { + return switch (priceType) { + case SALE -> LimjangPrice.builder() + .sellingPrice(price) + .build(); + case PULL_RENT -> LimjangPrice.builder() + .pullRent(price) + .build(); + case MONTHLY_RENT -> LimjangPrice.builder() + .depositPrice(price) + .monthlyRent(monthlyRent) + .build(); + case MARKET_PRICE -> LimjangPrice.builder() + .marketPrice(price) + .build(); + }; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/member/model/Member.java b/src/main/java/umc/th/juinjang/domain/member/model/Member.java new file mode 100644 index 00000000..2ee95581 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/member/model/Member.java @@ -0,0 +1,231 @@ +package umc.th.juinjang.domain.member.model; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Member extends BaseEntity implements UserDetails { + + @Id + @Column(name = "member_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long memberId; + + private String email; + + private String nickname; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private MemberProvider provider; + + @Column(name = "agree_version") + private String agreeVersion; + + // apple client id값을 의미 + @Column(name = "apple_sub") + private String appleSub; + + // kakao target id값 의미 (카카오의 유저 식별값. 탈퇴할 때 필요) + @Column(name = "target_id") + private Long kakaoTargetId; + + @Lob + private String imageUrl; + + @Column(nullable = false) + private String refreshToken; + + @Column(nullable = false) + private LocalDateTime refreshTokenExpiresAt; + + private String introduction; + + @Enumerated(EnumType.STRING) + private MemberStatus status; + + private LocalDateTime deletedAt; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List pencilAccounts = new ArrayList<>(); + + @OneToMany(mappedBy = "memberId", cascade = CascadeType.ALL, orphanRemoval = false) + private List limjangList = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List purchasedPencils = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List usedPencils = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List sharedNotes = new ArrayList<>(); + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List likedNotes = new ArrayList<>(); + + public static Member createKakaoMember(String email, Long targetId, String nickname, String agreeVersion) { + String introduction = String.format("안녕하세요, %s 입니다.", nickname); + + Member member = Member.builder() + .email(email) + .provider(MemberProvider.KAKAO) + .kakaoTargetId(targetId) + .nickname(nickname) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now()) + .agreeVersion(agreeVersion) + .introduction(introduction) + .status(MemberStatus.ACTIVE) + .build(); + + PencilAccount createAccount = PencilAccount.createPencilAccount(member); + member.addPencilAccount(createAccount); + + return member; + } + + // 애플 회원 생성 팩토리 메서드 + public static Member createAppleMember(String email, String sub, String nickname, String agreeVersion) { + String introduction = String.format("안녕하세요, %s 입니다.", nickname); + + Member member = Member.builder() + .email(email) + .nickname(nickname) + .provider(MemberProvider.APPLE) + .appleSub(sub) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now()) + .agreeVersion(agreeVersion) + .introduction(introduction) + .status(MemberStatus.ACTIVE) + .build(); + + PencilAccount createAccount = PencilAccount.createPencilAccount(member); + member.addPencilAccount(createAccount); + + return member; + } + + // refreshToken 재발급 + public void updateRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + this.refreshTokenExpiresAt = LocalDateTime.now().plusDays(7); + } + + // 로그아웃 시 토큰 만료 + public void refreshTokenExpires() { + this.refreshToken = ""; + this.refreshTokenExpiresAt = LocalDateTime.now(); + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return this.email; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + public void updateNickname(String nickname) { + this.nickname = nickname; + } + + public void updateImage(String imageUrl) { + this.imageUrl = imageUrl; + } + + public void updateAgreeVersion(final String agreeVersion) { + this.agreeVersion = agreeVersion; + } + + public PencilAccount getAccount() { + if (this.pencilAccounts == null || this.pencilAccounts.isEmpty()) { + return null; + } + + return this.pencilAccounts.get(0); + } + + public void addPencilAccount(PencilAccount pencilAccount) { + if (this.pencilAccounts == null) { + this.pencilAccounts = new ArrayList<>(); + } + this.pencilAccounts.add(pencilAccount); + } + + public void kakaoWithdraw() { + this.status = MemberStatus.WITHDRAWN; + this.email = null; + this.kakaoTargetId = null; + this.nickname = null; + this.deletedAt = LocalDateTime.now(); + } + + public void appleWithdraw() { + this.status = MemberStatus.WITHDRAWN; + this.email = null; + this.appleSub = null; + this.nickname = null; + this.deletedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/MemberProvider.java b/src/main/java/umc/th/juinjang/domain/member/model/MemberProvider.java similarity index 51% rename from src/main/java/umc/th/juinjang/model/entity/enums/MemberProvider.java rename to src/main/java/umc/th/juinjang/domain/member/model/MemberProvider.java index 329c94e0..dde7bcb7 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/MemberProvider.java +++ b/src/main/java/umc/th/juinjang/domain/member/model/MemberProvider.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.member.model; public enum MemberProvider { APPLE, KAKAO diff --git a/src/main/java/umc/th/juinjang/domain/member/model/MemberStatus.java b/src/main/java/umc/th/juinjang/domain/member/model/MemberStatus.java new file mode 100644 index 00000000..69d937e7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/member/model/MemberStatus.java @@ -0,0 +1,6 @@ +package umc.th.juinjang.domain.member.model; + +public enum MemberStatus { + ACTIVE, + WITHDRAWN +} diff --git a/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java b/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java new file mode 100644 index 00000000..f066cc52 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/member/repository/MemberRepository.java @@ -0,0 +1,36 @@ +package umc.th.juinjang.domain.member.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberStatus; + +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); + + Optional findByRefreshToken(String refreshToken); + + Optional findByKakaoTargetId(Long targetId); + + Member findByNickname(String nickname); + + Optional findByAppleSub(String sub); + + @Modifying + @Query("UPDATE Member m SET m.introduction = :introduction WHERE m.memberId = :id") + void patchIntroduction(@Param("id") Long id, @Param("introduction") String introduction); + + boolean existsByNickname(String nickname); + + Optional findByEmailAndKakaoTargetIdAndStatus(String email, Long kakaoTargetId, MemberStatus status); + + Optional findByEmailAndAppleSubAndStatus(String email, String sub, MemberStatus status); + + Optional findByMemberIdAndStatus(long id, MemberStatus memberStatus); +} diff --git a/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java b/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java new file mode 100644 index 00000000..b3fab114 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java @@ -0,0 +1,54 @@ +package umc.th.juinjang.domain.note.liked.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = { + @UniqueConstraint( + columnNames = { + "member_id", + "shared_note_id" + } + ) +}) +@Entity +public class LikedNote { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long likedNoteId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "shared_note_id", nullable = false) + private SharedNote sharedNote; + + @Builder + private LikedNote(Member member, SharedNote sharedNote) { + this.member = member; + this.sharedNote = sharedNote; + } + + public static LikedNote create(Member member, SharedNote sharedNote) { + return LikedNote.builder().member(member).sharedNote(sharedNote).build(); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteQueryDSLRepository.java b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteQueryDSLRepository.java new file mode 100644 index 00000000..76fb225d --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteQueryDSLRepository.java @@ -0,0 +1,14 @@ +package umc.th.juinjang.domain.note.liked.model.repository; + +import java.util.List; + +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; + +public interface LikedNoteQueryDSLRepository { + + List findAllByMemberAndDynamicWhereDeletedAtIsNull(Member user, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword); +} diff --git a/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteQueryDSLRepositoryImpl.java b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteQueryDSLRepositoryImpl.java new file mode 100644 index 00000000..a132d12e --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteQueryDSLRepositoryImpl.java @@ -0,0 +1,91 @@ +package umc.th.juinjang.domain.note.liked.model.repository; + +import static umc.th.juinjang.domain.limjang.model.QAddress.*; +import static umc.th.juinjang.domain.limjang.model.QLimjang.*; +import static umc.th.juinjang.domain.limjang.model.QLimjangPrice.*; +import static umc.th.juinjang.domain.member.model.QMember.*; +import static umc.th.juinjang.domain.note.liked.model.QLikedNote.*; +import static umc.th.juinjang.domain.note.shared.model.QSharedNote.*; +import static umc.th.juinjang.domain.report.model.QReport.*; + +import java.util.List; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPQLTemplates; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; + +public class LikedNoteQueryDSLRepositoryImpl implements LikedNoteQueryDSLRepository { + private final JPAQueryFactory queryFactory; + + public LikedNoteQueryDSLRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em); + } + + @Override + public List findAllByMemberAndDynamicWhereDeletedAtIsNull(Member user, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword) { + return queryFactory.selectFrom(likedNote) + .join(likedNote.sharedNote, sharedNote).fetchJoin() + .join(likedNote.member, member).fetchJoin() + .join(sharedNote.limjang, limjang).fetchJoin() + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .join(limjang.addressEntity, address).fetchJoin() + .leftJoin(limjang.report, report).fetchJoin() + .where( + likedNote.member.eq(user), + getWhereByPropertyType(propertyType), + getWhereByPriceType(priceType), + keywordCondition(keyword), + sharedNote.deletedAt.isNull(), + limjang.deleted.isFalse() + ) + .orderBy(likedNote.likedNoteId.desc()) + .fetch(); + + } + + private BooleanExpression keywordCondition(String keyword) { + if (keyword == null || keyword.isBlank()) { + return null; + } + return keywordOf( + removeBlank(sharedNote.buildingName).containsIgnoreCase(keyword), + removeBlank(address.roadAddress).containsIgnoreCase(keyword) + ); + } + + private BooleanExpression keywordOf(BooleanExpression... conditions) { + BooleanExpression result = null; + for (BooleanExpression condition : conditions) { + result = result == null ? condition : result.or(condition); + } + return result; + } + + private StringExpression removeBlank(StringExpression origin) { + return Expressions.stringTemplate("function('replace', {0}, ' ', '')", origin); + } + + private BooleanExpression getWhereByPriceType(LimjangPriceType priceType) { + if (priceType == null) { + return null; + } + return limjang.priceType.eq(priceType); + } + + private BooleanExpression getWhereByPropertyType(LimjangPropertyType propertyType) { + if (propertyType == null) { + return null; + } + return limjang.propertyType.eq(propertyType); + } + +} diff --git a/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java new file mode 100644 index 00000000..2801cc4d --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java @@ -0,0 +1,25 @@ +package umc.th.juinjang.domain.note.liked.model.repository; + +import java.util.Optional; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +public interface LikedNoteRepository extends JpaRepository, LikedNoteQueryDSLRepository { + + boolean existsByMemberAndSharedNote(Member member, SharedNote sharedNote); + + Optional findByMemberAndSharedNote(Member member, SharedNote sharedNote); + + @Query("SELECT l.sharedNote.sharedNoteId FROM LikedNote l WHERE l.member = :member AND l.sharedNote IN :sharedNotes") + List findLikedSharedNoteIds(@Param("member") Member member, + @Param("sharedNotes") List sharedNotes); + + List findAllByMember(Member member); +} diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java b/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java new file mode 100644 index 00000000..0761580c --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java @@ -0,0 +1,105 @@ +package umc.th.juinjang.domain.note.shared.model; + +import java.sql.Timestamp; + +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.api.note.shared.controller.request.SharedNotePostRequest; +import umc.th.juinjang.domain.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.member.model.Member; + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Entity +@DynamicUpdate +public class SharedNote extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long sharedNoteId; + + private Long viewCount; + + private Timestamp deletedAt; + + private String buildingName; + + private boolean isImageShared; + + @Comment("임장시기 연도") + @Column(name = "note_year") + private Integer year; + + @Comment("임장시기 월") + @Column(name = "note_month") + private Integer month; + + // TODO : 임장시기 - 시기 추후에, ENUM 으로 변경 필요 + private String period; + + private String review; + + @Comment("임장 가격") + private Long price; + + private Long likeCount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "limjang_id", nullable = false) + private Limjang limjang; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + public Long increaseLikedCount() { + return this.likeCount = (likeCount == null ? 1L : likeCount + 1); + } + + public static SharedNote toSharedNote(Member member, Limjang limjang, SharedNotePostRequest dto, Long price) { + return SharedNote.builder() + .member(member) + .limjang(limjang) + .buildingName(dto.buildingName()) + .review(dto.review()) + .year(dto.year()) + .month(dto.month()) + .period(dto.period()) + .isImageShared(dto.isImageShared()) + .viewCount(0L) + .likeCount(0L) + .price(price) + .build(); + } + + public void updatePrice(long price) { + this.price = price; + } + + public void updateDeletedAt(Timestamp deletedAt) { + this.deletedAt = deletedAt; + } + + public String getPullPeriod() { + String shortYear = String.valueOf(this.year).substring(2); + return shortYear + "년 " + this.month + "월 " + this.period + " 임장"; + } +} + diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/model/ViewCountPolicy.java b/src/main/java/umc/th/juinjang/domain/note/shared/model/ViewCountPolicy.java new file mode 100644 index 00000000..af9f05cc --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/shared/model/ViewCountPolicy.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.domain.note.shared.model; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@RequiredArgsConstructor +@Slf4j +public class ViewCountPolicy { + + private final Map milestoneRewardMap = Map.of( + 10L, 1L, + 25L, 2L, + 50L, 3L + ); + + private final Map milestoneMessageMap = Map.of( + 10L, "조회수 10회 달성!", + 25L, "조회수 25회 달성!", + 50L, "조회수 50회 달성!" + ); + + public Long getRewardForExactMilestone(long viewCount) { + return milestoneRewardMap.get(viewCount); + } + + public String getMessageForMilestone(long milestone) { + String message = milestoneMessageMap.get(milestone); + if (message == null) { + log.info("정의되지 않은 milestone입니다: milestoen={}", milestone); + } + return message; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteQueryDSLRepository.java b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteQueryDSLRepository.java new file mode 100644 index 00000000..c6983bbd --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteQueryDSLRepository.java @@ -0,0 +1,22 @@ +package umc.th.juinjang.domain.note.shared.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import umc.th.juinjang.api.note.shared.controller.request.ExploreSortType; +import umc.th.juinjang.api.note.shared.controller.request.NoteType; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +public interface SharedNoteQueryDSLRepository { + + Page findSharedNoteInExployer(List code, ExploreSortType sort, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword, Pageable pageable); + + List findUserSharedNotes(Member member, NoteType noteType, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword, List filterIds); +} diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteQueryDSLRepositoryImpl.java b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteQueryDSLRepositoryImpl.java new file mode 100644 index 00000000..9e374247 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteQueryDSLRepositoryImpl.java @@ -0,0 +1,194 @@ +package umc.th.juinjang.domain.note.shared.repository; + +import static com.querydsl.core.types.Order.*; +import static umc.th.juinjang.domain.limjang.model.QAddress.*; +import static umc.th.juinjang.domain.limjang.model.QLimjang.*; +import static umc.th.juinjang.domain.limjang.model.QLimjangPrice.*; +import static umc.th.juinjang.domain.member.model.QMember.*; +import static umc.th.juinjang.domain.note.shared.model.QSharedNote.*; +import static umc.th.juinjang.domain.pencil.used.model.QUsedPencil.*; +import static umc.th.juinjang.domain.report.model.QReport.*; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.JPQLTemplates; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import jakarta.persistence.EntityManager; +import umc.th.juinjang.api.note.shared.controller.request.ExploreSortType; +import umc.th.juinjang.api.note.shared.controller.request.NoteType; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.shared.model.SharedNote; +import umc.th.juinjang.domain.pencil.used.model.Usedtype; + +public class SharedNoteQueryDSLRepositoryImpl implements SharedNoteQueryDSLRepository { + private final JPAQueryFactory queryFactory; + + public SharedNoteQueryDSLRepositoryImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em); + } + + @Override + public Page findSharedNoteInExployer(List code, ExploreSortType sort, + LimjangPropertyType propertyType, LimjangPriceType priceType, String keyword, Pageable pageable) { + + List content = queryFactory.selectFrom(sharedNote) + .join(sharedNote.limjang, limjang).fetchJoin() + .join(sharedNote.member, member).fetchJoin() + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .join(limjang.addressEntity, address).fetchJoin() + .leftJoin(limjang.report, report).fetchJoin() + .where( + getBcodesStartsWith(code), + getWhereByPropertyType(propertyType), + getWhereByPriceType(priceType), + keywordCondition(keyword), + sharedNote.deletedAt.isNull(), + limjang.deleted.isFalse() + ) + .orderBy(getOrderBySortOptions(sort)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(sharedNote.count()).from(sharedNote) + .join(sharedNote.limjang, limjang) + .join(sharedNote.member, member) + .join(limjang.limjangPrice, limjangPrice) + .join(limjang.addressEntity, address) + .leftJoin(limjang.report, report) + .where( + getBcodesStartsWith(code), + getWhereByPropertyType(propertyType), + getWhereByPriceType(priceType), + keywordCondition(keyword), + sharedNote.deletedAt.isNull(), + limjang.deleted.isFalse() + ); + long totalCount = Optional.ofNullable(countQuery.fetchOne()).orElse(0L); + return new PageImpl<>(content, pageable, totalCount); + } + + @Override + public List findUserSharedNotes(Member user, NoteType noteType, LimjangPropertyType propertyType, + LimjangPriceType priceType, String keyword, List filterIds) { + + return queryFactory.selectFrom(sharedNote) + .join(sharedNote.limjang, limjang).fetchJoin() + .join(sharedNote.member, member).fetchJoin() + .join(limjang.limjangPrice, limjangPrice).fetchJoin() + .join(limjang.addressEntity, address).fetchJoin() + .leftJoin(limjang.report, report).fetchJoin() + .where( + getWhereByNoteType(user, noteType, filterIds), + getWhereByPropertyType(propertyType), + getWhereByPriceType(priceType), + keywordCondition(keyword) + ) + .orderBy(getOrderByNoteType(noteType)) + .fetch(); + } + + private OrderSpecifier[] getOrderByNoteType(NoteType noteType) { + if (noteType == NoteType.SHARED) { + return new OrderSpecifier[] {sharedNote.createdAt.desc()}; + } + return new OrderSpecifier[0]; + } + + private BooleanExpression getWhereByNoteType(Member user, NoteType noteType, List ids) { + return switch (noteType) { + case OWNED -> sharedNote.sharedNoteId.in(ids); + case SHARED -> sharedNote.member.eq(user).and(sharedNote.deletedAt.isNull()).and(limjang.deleted.isFalse()); + default -> null; + }; + } + + private BooleanExpression keywordCondition(String keyword) { + if (keyword == null || keyword.isBlank()) { + return null; + } + return keywordOf( + removeBlank(sharedNote.buildingName).containsIgnoreCase(keyword), + removeBlank(address.roadAddress).containsIgnoreCase(keyword) + ); + } + + private BooleanExpression keywordOf(BooleanExpression... conditions) { + BooleanExpression result = null; + for (BooleanExpression condition : conditions) { + result = result == null ? condition : result.or(condition); + } + return result; + } + + private StringExpression removeBlank(StringExpression origin) { + return Expressions.stringTemplate("function('replace', {0}, ' ', '')", origin); + } + + private BooleanExpression getWhereByPriceType(LimjangPriceType priceType) { + if (priceType == null) { + return null; + } + return limjang.priceType.eq(priceType); + } + + private BooleanExpression getWhereByPropertyType(LimjangPropertyType propertyType) { + if (propertyType == null) { + return null; + } + return limjang.propertyType.eq(propertyType); + } + + public BooleanExpression getBcodesStartsWith(List bcodes) { + if (bcodes == null || bcodes.isEmpty()) { + return null; + } + + BooleanExpression result = null; + for (String bcode : bcodes) { + BooleanExpression condition = address.bcode.startsWith(bcode); + if (result == null) { + result = condition; + } else { + result = result.or(condition); + } + } + return result; + } + + private OrderSpecifier[] getOrderBySortOptions(ExploreSortType sort) { + return switch (sort) { + case LATEST -> new OrderSpecifier[] { + new OrderSpecifier<>(DESC, limjang.updatedAt) + }; + case POPULAR -> new OrderSpecifier[] { + new OrderSpecifier<>( + DESC, + JPAExpressions + .select(usedPencil.count()) + .from(usedPencil) + .where( + usedPencil.sharedNoteId.eq(sharedNote.sharedNoteId), + usedPencil.type.eq(Usedtype.OWNED) + ) + ) + }; + default -> new OrderSpecifier[0]; + }; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java new file mode 100644 index 00000000..06c675c3 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java @@ -0,0 +1,55 @@ +package umc.th.juinjang.domain.note.shared.repository; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +public interface SharedNoteRepository extends JpaRepository, SharedNoteQueryDSLRepository { + + @Query("select s from SharedNote s join fetch s.limjang l join fetch l.addressEntity join fetch l.limjangPrice where s.sharedNoteId = :sharedNoteId") + Optional findByIdWithNoteAndAddress(@Param("sharedNoteId") Long sharedNoteId); + + @Modifying + @Query("UPDATE SharedNote s SET s.viewCount = s.viewCount + 1 WHERE s.sharedNoteId = :sharedNoteId") + void incrementViewCount(@Param("sharedNoteId") Long sharedNoteId); + + @Modifying + @Query("UPDATE SharedNote sn SET sn.likeCount = sn.likeCount + 1 WHERE sn.sharedNoteId = :sharedNoteId") + void incrementLikedCountById(@Param("sharedNoteId") Long sharedNoteId); + + @Query("SELECT sn.likeCount FROM SharedNote sn WHERE sn.sharedNoteId = :sharedNoteId") + Long getLikeCountById(@Param("sharedNoteId") Long sharedNoteId); + + @Modifying + @Query("UPDATE SharedNote sn SET sn.likeCount = sn.likeCount - 1 WHERE sn.sharedNoteId = :sharedNoteId") + void decrementLikedCountById(@Param("sharedNoteId") Long sharedNoteId); + + Optional findTop1ByLimjang_LimjangIdOrderByCreatedAtDesc(Long limjangId); + + Optional getBySharedNoteIdAndMemberAndDeletedAtIsNull(Long sharedNoteId, Member member); + + @Query("SELECT s.sharedNoteId, s.viewCount FROM SharedNote s WHERE s.sharedNoteId IN :ids") + List findAllViewCountById(@Param("ids") List ids); + + @Query("SELECT s.viewCount FROM SharedNote s WHERE s.sharedNoteId = :id") + Long findViewCountById(@Param("id") Long id); + + Optional findBySharedNoteIdAndDeletedAtIsNull(Long id); + + boolean existsByDeletedAtIsNullAndLimjang(Limjang limjang); + + @Query("SELECT s.limjang.limjangId FROM SharedNote s WHERE s.limjang in :limjangs AND s.deletedAt is null") + Set findLimjangIdsByDeletedAtIsNullAndLimjang(@Param("limjangs") List limjangs); + + @Query("SELECT s.limjang.limjangId FROM SharedNote s WHERE s.limjang in :limjangs AND s.deletedAt is not null") + Set findLimjangIdsByDeletedAtIsNotNullAndLimjang(@Param("limjangs") List limjangs); +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/acquired/model/AcquiredPencil.java b/src/main/java/umc/th/juinjang/domain/pencil/acquired/model/AcquiredPencil.java new file mode 100644 index 00000000..f65ef692 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/acquired/model/AcquiredPencil.java @@ -0,0 +1,86 @@ +package umc.th.juinjang.domain.pencil.acquired.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; +import umc.th.juinjang.domain.member.model.Member; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class AcquiredPencil extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "acquired_pencil_id") + private Long id; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + private String content; + + private Long sharedNoteId; + + private Long acquiredQuantity; + + private boolean isRead; + + @Enumerated(EnumType.STRING) + private AcquiredType type; // Note, Add, Sold + + @Builder + private AcquiredPencil(Member member, String content, Long sharedNoteId, Long acquiredQuantity, Long remainQuantity, + boolean isRead, AcquiredType type, LocalDateTime createdAt) { + this.member = member; + this.content = content; + this.sharedNoteId = sharedNoteId; + this.acquiredQuantity = acquiredQuantity; + this.isRead = isRead; + this.type = type; + setCreatedAt(createdAt); + } + + public static AcquiredPencil create(Member member, String content, Long sharedNoteId, Long acquiredQuantity, + boolean isRead, AcquiredType type) { + return AcquiredPencil.builder() + .member(member) + .content(content) + .sharedNoteId(sharedNoteId) + .acquiredQuantity(acquiredQuantity) + .isRead(isRead) + .type(type) + .build(); + } + + public static AcquiredPencil createWithDate(Member member, String content, Long sharedNoteId, Long acquiredQuantity, + boolean isRead, AcquiredType type, LocalDateTime createdAt) { + return AcquiredPencil.builder() + .member(member) + .content(content) + .sharedNoteId(sharedNoteId) + .acquiredQuantity(acquiredQuantity) + .isRead(isRead) + .type(type) + .createdAt(createdAt) + .build(); + } + + public void updateIsReadAsTrue() { + this.isRead = true; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/acquired/model/AcquiredType.java b/src/main/java/umc/th/juinjang/domain/pencil/acquired/model/AcquiredType.java new file mode 100644 index 00000000..93bc1ea7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/acquired/model/AcquiredType.java @@ -0,0 +1,5 @@ +package umc.th.juinjang.domain.pencil.acquired.model; + +public enum AcquiredType { + NOTE, AD, SOLD, VIEWCOUNT +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java b/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java new file mode 100644 index 00000000..dadba5b2 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/acquired/repository/AcquiredPencilRepository.java @@ -0,0 +1,27 @@ +package umc.th.juinjang.domain.pencil.acquired.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; + +public interface AcquiredPencilRepository extends JpaRepository { + + @Query("SELECT new umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse(" + + "ap.id, ap.content, ap.sharedNoteId, ap.acquiredQuantity, " + + "ap.isRead, ap.type, ap.createdAt, sn.buildingName) " + + "FROM AcquiredPencil ap " + + "LEFT JOIN SharedNote sn ON ap.sharedNoteId = sn.sharedNoteId " + + "WHERE ap.member = :member " + + "ORDER BY ap.createdAt DESC") + List findAllByMemberWithBuildingNameOrderByCreatedAtDesc(@Param("member") Member member); + + boolean existsByMemberAndIsReadFalse(Member member); + + boolean existsByMember(Member member); +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/DeliveryStatus.java b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/DeliveryStatus.java new file mode 100644 index 00000000..8b0c96d6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/DeliveryStatus.java @@ -0,0 +1,16 @@ +package umc.th.juinjang.domain.pencil.purchased.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DeliveryStatus { + + DELIVERY_SUCCESS(0), //The app delivered the consumable in-app purchase and it’s working properly. + SERVER_ERROR(3), // The app didn’t deliver the consumable in-app purchase due to a server outage. + OTHER_REASONS(5); // The app didn't deliver the consumable in-app purchase for other reasons. + + private final int appleCode; + +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/DeliveryStatusConverter.java b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/DeliveryStatusConverter.java new file mode 100644 index 00000000..832a998c --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/DeliveryStatusConverter.java @@ -0,0 +1,30 @@ +package umc.th.juinjang.domain.pencil.purchased.model; + +import jakarta.persistence.AttributeConverter; + +public class DeliveryStatusConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(DeliveryStatus deliveryStatus) { + if (deliveryStatus == null) { + return null; + } + return deliveryStatus.getAppleCode(); + } + + @Override + public DeliveryStatus convertToEntityAttribute(Integer dbData) { + if (dbData == null) { + return null; + } + + for (DeliveryStatus status : DeliveryStatus.values()) { + if (status.getAppleCode() == dbData) { + return status; + } + } + + throw new IllegalArgumentException("Unknown database value: " + dbData); + } + +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/PurchasedPencil.java b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/PurchasedPencil.java new file mode 100644 index 00000000..276a0023 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/PurchasedPencil.java @@ -0,0 +1,157 @@ +package umc.th.juinjang.domain.pencil.purchased.model; + +import java.time.LocalDateTime; +import java.util.UUID; + +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; +import org.springframework.data.annotation.LastModifiedDate; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicUpdate +@Entity +public class PurchasedPencil { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "purchased_pencil_id") + private Long id; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + private String title; + private Long purchaseQuantity; + @Comment("구매 후 사용자에게 남은 연필의 개수") + private Long remainQuantity; + @Comment("구매 후 사용하고 남은 연필의 개수") + private Long usedQuantity; + private Long price; + + @Comment("애플 인앱 결제에서, 프론트에서 전달해주는 트랜잭션 아이디") + private String transactionId; + + @Comment("애플 인앱 결제에서, 프론트에서 전달해주는 애플 앱 토큰") + private UUID appAccountToken; + + @Comment("해당 결제한 연필이 정상적으로 고객에게 전달됐는 지 여부") + @Convert(converter = DeliveryStatusConverter.class) + private DeliveryStatus deliveryStatus; + + @Comment("트랜잭션이 정상적으로 진행됐는 가 여부") + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private TransactionStatus transactionStatus; + + private Integer playTime; + + private Long retryCount = 0L; + + private LocalDateTime purchasedAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @Builder + public PurchasedPencil(Member member, String title, Long purchaseQuantity, Long usedQuantity, + Long remainQuantity, TransactionStatus transactionStatus, Integer playTime, + Long price, String transactionId, UUID appAccountToken, DeliveryStatus deliveryStatus, + LocalDateTime purchasedAt) { + this.member = member; + this.title = title; + this.purchaseQuantity = purchaseQuantity; + this.remainQuantity = remainQuantity; + this.usedQuantity = usedQuantity; + this.price = price; + this.playTime = playTime; + this.transactionId = transactionId; + this.appAccountToken = appAccountToken; + this.deliveryStatus = deliveryStatus; + this.transactionStatus = transactionStatus; + this.purchasedAt = purchasedAt; + } + + private static PurchasedPencilBuilder baseBuilder( + Member member, String title, Long quantity, Long price, + Integer playTime, String transactionId, UUID token, LocalDateTime purchasedAt + ) { + return PurchasedPencil.builder() + .member(member) + .title(title) + .purchaseQuantity(quantity) + .usedQuantity(quantity) + .price(price) + .playTime(playTime) + .transactionId(transactionId) + .appAccountToken(token) + .purchasedAt(purchasedAt); + } + + // ✅ 결제 성공 + public static PurchasedPencil successOf(Member member, String title, Long quantity, Long remainQuantity, + Long price, Integer playTime, String transactionId, + UUID token, LocalDateTime purchasedAt) { + return baseBuilder(member, title, quantity, price, playTime, transactionId, token, purchasedAt) + .transactionStatus(TransactionStatus.SUCCESS) + .deliveryStatus(DeliveryStatus.DELIVERY_SUCCESS) + .remainQuantity(remainQuantity) + .build(); + } + + // ✅ 서버 에러 + public static PurchasedPencil failedDueToServerError(Member member, String title, Long quantity, + Long price, Integer playTime, String transactionId, UUID token, LocalDateTime purchasedAt) { + return baseBuilder(member, title, quantity, price, playTime, transactionId, token, purchasedAt) + .transactionStatus(TransactionStatus.DB_FAILED) + .deliveryStatus(DeliveryStatus.SERVER_ERROR) + .remainQuantity(0L) + .build(); + } + + // ✅ 검증 실패 + public static PurchasedPencil failedDueToValidation(Member member, String title, Long quantity, + Long price, Integer playTime, String transactionId, UUID token, LocalDateTime purchasedAt) { + return baseBuilder(member, title, quantity, price, playTime, transactionId, token, purchasedAt) + .transactionStatus(TransactionStatus.VALIDATION_FAILED) + .deliveryStatus(DeliveryStatus.OTHER_REASONS) + .remainQuantity(0L) + .build(); + } + + public void decreaseUsedQuantity(long quantity) { + this.usedQuantity -= quantity; + } + + public void markAsSuccess() { + this.transactionStatus = TransactionStatus.SUCCESS; + this.deliveryStatus = DeliveryStatus.DELIVERY_SUCCESS; + } + + public void markAsRefund() { + this.transactionStatus = TransactionStatus.REFUNDED; + } + + public void updateRetryCount(Long retryCount) { + this.retryCount = retryCount; + } +} + + diff --git a/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/TransactionStatus.java b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/TransactionStatus.java new file mode 100644 index 00000000..e50c0c78 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/purchased/model/TransactionStatus.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.domain.pencil.purchased.model; + +public enum TransactionStatus { + // PENDING, + SUCCESS, + VALIDATION_FAILED, + REFUNDED, + DB_FAILED; +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/purchased/repository/PurchasedPencilRepository.java b/src/main/java/umc/th/juinjang/domain/pencil/purchased/repository/PurchasedPencilRepository.java new file mode 100644 index 00000000..746e0821 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/purchased/repository/PurchasedPencilRepository.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.domain.pencil.purchased.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; + +public interface PurchasedPencilRepository extends JpaRepository { + + @Query("SELECT p FROM PurchasedPencil p WHERE p.member = :member AND p.deliveryStatus = 0 ORDER BY p.purchasedAt DESC") + List findAllByMemberWhereDeliverySuccessOrderByCreatedAtDesc(@Param("member") Member member); + + @Query("select p from PurchasedPencil p where p.member = :member and p.remainQuantity > :remainQuantity AND p.deliveryStatus = 0 order by p.purchasedAt asc") + List findByMemberAndDeliverySuccessAndRemainQuantityGreaterThanOrderByCreatedAtAsc( + @Param("member") Member buyer, + @Param("remainQuantity") Long remainQuantity); + + Optional findByTransactionIdAndMember(String transactionId, Member member); + + Optional findByTransactionId(String transactionId); + + @Query("SELECT SUM(p.price) FROM PurchasedPencil p " + + "WHERE p.member = :member " + + "AND p.deliveryStatus = 0 " + + "AND p.transactionStatus = 'SUCCESS'") + Optional getSumPriceWhereMemberAndSuccess(Member member); + + @Query("SELECT SUM(p.price) FROM PurchasedPencil p " + + "WHERE p.member = :member " + + "AND p.deliveryStatus = 0 " + + "AND p.transactionStatus = 'REFUNDED'") + Optional getSumPriceWhereMemberAndRefund(Member member); +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/used/model/UsedPencil.java b/src/main/java/umc/th/juinjang/domain/pencil/used/model/UsedPencil.java new file mode 100644 index 00000000..3e8b566e --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/used/model/UsedPencil.java @@ -0,0 +1,76 @@ +package umc.th.juinjang.domain.pencil.used.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; +import umc.th.juinjang.domain.member.model.Member; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table( + name = "used_pencil", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"member_id", "shared_note_id"}) + } +) +public class UsedPencil extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long usedPencilId; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(name = "shared_note_id") + private Long sharedNoteId; + + private Long usedQuantity; + + // TODO: 우선 OWNED(소장) 하나만 추가 + @Enumerated(EnumType.STRING) + private Usedtype type; + + private String buildingName; + + private Long remainQuantity; + + public static UsedPencil create(Member member, Long sharedNoteId, Long usedQuantity, Usedtype type, + String buildingName, Long remainQuantity) { + return new UsedPencil( + member, + sharedNoteId, + usedQuantity, + type, + buildingName, + remainQuantity + ); + } + + @Builder + private UsedPencil(Member member, Long sharedNoteId, Long usedQuantity, Usedtype type, + String buildingName, Long remainQuantity) { + this.member = member; + this.sharedNoteId = sharedNoteId; + this.usedQuantity = usedQuantity; + this.type = type; + this.buildingName = buildingName; + this.remainQuantity = remainQuantity; + } + +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/used/model/Usedtype.java b/src/main/java/umc/th/juinjang/domain/pencil/used/model/Usedtype.java new file mode 100644 index 00000000..e7ed6f9a --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/used/model/Usedtype.java @@ -0,0 +1,5 @@ +package umc.th.juinjang.domain.pencil.used.model; + +public enum Usedtype { + OWNED +} diff --git a/src/main/java/umc/th/juinjang/domain/pencil/used/repository/UsedPencilRepository.java b/src/main/java/umc/th/juinjang/domain/pencil/used/repository/UsedPencilRepository.java new file mode 100644 index 00000000..e2131f22 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencil/used/repository/UsedPencilRepository.java @@ -0,0 +1,29 @@ +package umc.th.juinjang.domain.pencil.used.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.used.model.UsedPencil; + +@Repository +public interface UsedPencilRepository extends JpaRepository { + + boolean existsByMemberAndSharedNoteId(Member member, Long sharedNoteId); + + int countBySharedNoteId(Long sharedNoteId); + + List findAllByMemberOrderByCreatedAtDesc(Member member); + + @Query("select u.sharedNoteId from UsedPencil u where u.member = :member and u.sharedNoteId in :sharedNoteIds and u.type = 'OWNED'") + List findByMemberInSharedNoteIdsAndTypeIsOwned(@Param("member") Member member, + @Param("sharedNoteIds") List sharedNoteIds); + + @Query("select u from UsedPencil u where u.member = :member and u.type = 'OWNED' order by u.createdAt desc ") + List findAllByMemberAndTypeIsOwnedOrderByCreatedAtDesc(@Param("member") Member member); + +} diff --git a/src/main/java/umc/th/juinjang/domain/pencilaccount/model/PencilAccount.java b/src/main/java/umc/th/juinjang/domain/pencilaccount/model/PencilAccount.java new file mode 100644 index 00000000..48338cd7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencilaccount/model/PencilAccount.java @@ -0,0 +1,91 @@ +package umc.th.juinjang.domain.pencilaccount.model; + +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.DynamicUpdate; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.common.BaseEntity; +import umc.th.juinjang.domain.member.model.Member; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@DynamicUpdate +public class PencilAccount extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long pencilAccountId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false, unique = true) + private Member member; + + private Long acquiredBalance; + + private Long purchasedBalance; + + @Comment("총 계좌 잔액 = 얻은 잔액 + 구매 잔액") + private Long totalBalance; + + private Long totalPurchaseAmount; + + @Comment("환불 시에, 해당 멤버가 현재까지 얼마나 환불했는지에 대한 정보가 필요함.") + private Long totalRefundAmount; + + @Builder + private PencilAccount(Member member) { + this.member = member; + this.acquiredBalance = 0L; + this.purchasedBalance = 0L; + this.totalBalance = 0L; + this.totalPurchaseAmount = 0L; + this.totalRefundAmount = 0L; + } + + public static PencilAccount createPencilAccount(Member member) { + return PencilAccount.builder() + .member(member).build(); + + } + + public void decreasePurchasedBalance(long price) { + this.purchasedBalance -= price; + this.totalBalance = this.purchasedBalance + this.acquiredBalance; + } + + public void recalculateTotalBalance() { + this.totalBalance = this.acquiredBalance + this.purchasedBalance; + } + + public void increaseAcquiredBalance(long price) { + this.acquiredBalance += price; + this.totalBalance = this.purchasedBalance + this.acquiredBalance; + } + + public void increasePurchasedBalance(long price) { + this.purchasedBalance += price; + this.totalPurchaseAmount += price; + + this.totalBalance = this.purchasedBalance + this.acquiredBalance; + } + + public void decreaseAcquiredBalance(long price) { + this.acquiredBalance -= price; + this.totalBalance = this.purchasedBalance + this.acquiredBalance; + } + + public void increaseTotalRefundAmount(long price) { + this.totalRefundAmount += price; + } +} diff --git a/src/main/java/umc/th/juinjang/domain/pencilaccount/repository/PencilAccountRepository.java b/src/main/java/umc/th/juinjang/domain/pencilaccount/repository/PencilAccountRepository.java new file mode 100644 index 00000000..2d85d840 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/pencilaccount/repository/PencilAccountRepository.java @@ -0,0 +1,23 @@ +package umc.th.juinjang.domain.pencilaccount.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.LockModeType; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; + +@Repository +public interface PencilAccountRepository extends JpaRepository { + + Optional findByMember(Member member); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT p FROM PencilAccount p WHERE p.member = :member") + Optional findByMemberWithLock(@Param("member") Member member); +} diff --git a/src/main/java/umc/th/juinjang/model/entity/Record.java b/src/main/java/umc/th/juinjang/domain/record/model/Record.java similarity index 89% rename from src/main/java/umc/th/juinjang/model/entity/Record.java rename to src/main/java/umc/th/juinjang/domain/record/model/Record.java index ea5fd983..f1fec918 100644 --- a/src/main/java/umc/th/juinjang/model/entity/Record.java +++ b/src/main/java/umc/th/juinjang/domain/record/model/Record.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.record.model; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -10,8 +10,8 @@ import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; import lombok.*; -import umc.th.juinjang.model.dto.record.RecordRequestDTO; -import umc.th.juinjang.model.entity.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.common.BaseEntity; @Entity @Getter diff --git a/src/main/java/umc/th/juinjang/repository/record/RecordRepository.java b/src/main/java/umc/th/juinjang/domain/record/repository/RecordRepository.java similarity index 83% rename from src/main/java/umc/th/juinjang/repository/record/RecordRepository.java rename to src/main/java/umc/th/juinjang/domain/record/repository/RecordRepository.java index da4ad324..eabe5d21 100644 --- a/src/main/java/umc/th/juinjang/repository/record/RecordRepository.java +++ b/src/main/java/umc/th/juinjang/domain/record/repository/RecordRepository.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.repository.record; +package umc.th.juinjang.domain.record.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -6,9 +6,8 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Record; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.record.model.Record; import java.util.List; diff --git a/src/main/java/umc/th/juinjang/model/entity/Report.java b/src/main/java/umc/th/juinjang/domain/report/model/Report.java similarity index 88% rename from src/main/java/umc/th/juinjang/model/entity/Report.java rename to src/main/java/umc/th/juinjang/domain/report/model/Report.java index c1e52740..be04c64c 100644 --- a/src/main/java/umc/th/juinjang/model/entity/Report.java +++ b/src/main/java/umc/th/juinjang/domain/report/model/Report.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.report.model; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -8,10 +8,10 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; -import java.util.Random; import lombok.*; -import umc.th.juinjang.model.entity.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.common.BaseEntity; @Entity @Getter diff --git a/src/main/java/umc/th/juinjang/repository/checklist/ReportRepository.java b/src/main/java/umc/th/juinjang/domain/report/repository/ReportRepository.java similarity index 76% rename from src/main/java/umc/th/juinjang/repository/checklist/ReportRepository.java rename to src/main/java/umc/th/juinjang/domain/report/repository/ReportRepository.java index 6a1f8eb2..56b95582 100644 --- a/src/main/java/umc/th/juinjang/repository/checklist/ReportRepository.java +++ b/src/main/java/umc/th/juinjang/domain/report/repository/ReportRepository.java @@ -1,15 +1,13 @@ -package umc.th.juinjang.repository.checklist; +package umc.th.juinjang.domain.report.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.ChecklistAnswer; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Report; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.report.model.Report; -import java.util.List; import java.util.Optional; public interface ReportRepository extends JpaRepository { diff --git a/src/main/java/umc/th/juinjang/domain/reward/model/Reward.java b/src/main/java/umc/th/juinjang/domain/reward/model/Reward.java new file mode 100644 index 00000000..bdc20a4a --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/reward/model/Reward.java @@ -0,0 +1,63 @@ +package umc.th.juinjang.domain.reward.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Reward { + + @Id + @Column(name = "reward_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long rewardId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Enumerated(EnumType.STRING) + private RewardType type; + + private Long milestone; + + private Long sharedNoteId; + + private Long rewardPencil; + + @Builder + private Reward(Member member, RewardType type, Long milestone, Long sharedNoteId, Long rewardPencil) { + this.member = member; + this.type = type; + this.milestone = milestone; + this.sharedNoteId = sharedNoteId; + this.rewardPencil = rewardPencil; + } + + public static Reward create(Member member, RewardType type, Long milestone, Long sharedNoteId, Long rewardPencil) { + return Reward.builder() + .member(member) + .type(type) + .milestone(milestone) + .sharedNoteId(sharedNoteId) + .rewardPencil(rewardPencil) + .build(); + } + +} diff --git a/src/main/java/umc/th/juinjang/domain/reward/model/RewardType.java b/src/main/java/umc/th/juinjang/domain/reward/model/RewardType.java new file mode 100644 index 00000000..54baf823 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/reward/model/RewardType.java @@ -0,0 +1,5 @@ +package umc.th.juinjang.domain.reward.model; + +public enum RewardType { + VIEWCOUNT, AD +} diff --git a/src/main/java/umc/th/juinjang/domain/reward/repository/RewardRepository.java b/src/main/java/umc/th/juinjang/domain/reward/repository/RewardRepository.java new file mode 100644 index 00000000..e8c48778 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/reward/repository/RewardRepository.java @@ -0,0 +1,11 @@ +package umc.th.juinjang.domain.reward.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import umc.th.juinjang.domain.reward.model.Reward; +import umc.th.juinjang.domain.reward.model.RewardType; + +public interface RewardRepository extends JpaRepository { + + boolean existsByTypeAndMilestoneAndSharedNoteId(RewardType type, Long milestone, Long sharedNoteId); +} diff --git a/src/main/java/umc/th/juinjang/model/entity/Scrap.java b/src/main/java/umc/th/juinjang/domain/scrap/model/Scrap.java similarity index 86% rename from src/main/java/umc/th/juinjang/model/entity/Scrap.java rename to src/main/java/umc/th/juinjang/domain/scrap/model/Scrap.java index d89d8eb3..2aa0aad1 100644 --- a/src/main/java/umc/th/juinjang/model/entity/Scrap.java +++ b/src/main/java/umc/th/juinjang/domain/scrap/model/Scrap.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.scrap.model; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -13,7 +13,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.common.BaseEntity; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.common.BaseEntity; @Entity diff --git a/src/main/java/umc/th/juinjang/repository/limjang/ScrapRepository.java b/src/main/java/umc/th/juinjang/domain/scrap/repository/ScrapRepository.java similarity index 82% rename from src/main/java/umc/th/juinjang/repository/limjang/ScrapRepository.java rename to src/main/java/umc/th/juinjang/domain/scrap/repository/ScrapRepository.java index 38e81b87..48911cca 100644 --- a/src/main/java/umc/th/juinjang/repository/limjang/ScrapRepository.java +++ b/src/main/java/umc/th/juinjang/domain/scrap/repository/ScrapRepository.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.repository.limjang; +package umc.th.juinjang.domain.scrap.repository; import java.util.List; import java.util.Optional; @@ -7,8 +7,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Scrap; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.scrap.model.Scrap; public interface ScrapRepository extends JpaRepository { diff --git a/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsAgreement.java b/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsAgreement.java new file mode 100644 index 00000000..283440a5 --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsAgreement.java @@ -0,0 +1,44 @@ +package umc.th.juinjang.domain.termsAgreement.repository; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TermsAgreement { + + @Id + @Column(name = "terms_agreement_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long memberId; + + @Enumerated(EnumType.STRING) + private TermsType termsType; + + private LocalDateTime agreedAt; + + public static TermsAgreement create(Long memberId, TermsType termsType) { + return TermsAgreement.builder() + .memberId(memberId) + .termsType(termsType) + .agreedAt(LocalDateTime.now()) + .build(); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsAgreementRepository.java b/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsAgreementRepository.java new file mode 100644 index 00000000..9367564c --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsAgreementRepository.java @@ -0,0 +1,12 @@ +package umc.th.juinjang.domain.termsAgreement.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TermsAgreementRepository extends JpaRepository { + // 멤버가 특정 약관에 동의 했는 여부를 체크하는 메서드 + Optional findByMemberIdAndTermsType(Long memberId, TermsType termsType); +} diff --git a/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsType.java b/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsType.java new file mode 100644 index 00000000..0fc768be --- /dev/null +++ b/src/main/java/umc/th/juinjang/domain/termsAgreement/repository/TermsType.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.domain.termsAgreement.repository; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum TermsType { + PENCIL_SHOP_SERVICE("연필상점 이용약관"); + + private final String text; +} diff --git a/src/main/java/umc/th/juinjang/model/entity/Withdraw.java b/src/main/java/umc/th/juinjang/domain/withdraw/model/Withdraw.java similarity index 79% rename from src/main/java/umc/th/juinjang/model/entity/Withdraw.java rename to src/main/java/umc/th/juinjang/domain/withdraw/model/Withdraw.java index 28024dda..bc6280cb 100644 --- a/src/main/java/umc/th/juinjang/model/entity/Withdraw.java +++ b/src/main/java/umc/th/juinjang/domain/withdraw/model/Withdraw.java @@ -1,10 +1,9 @@ -package umc.th.juinjang.model.entity; +package umc.th.juinjang.domain.withdraw.model; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.ColumnDefault; -import umc.th.juinjang.model.entity.common.BaseEntity; -import umc.th.juinjang.model.entity.enums.WithdrawReason; +import umc.th.juinjang.domain.common.BaseEntity; @Entity @Getter diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/WithdrawReason.java b/src/main/java/umc/th/juinjang/domain/withdraw/model/WithdrawReason.java similarity index 89% rename from src/main/java/umc/th/juinjang/model/entity/enums/WithdrawReason.java rename to src/main/java/umc/th/juinjang/domain/withdraw/model/WithdrawReason.java index fa71cc53..ed5ce50b 100644 --- a/src/main/java/umc/th/juinjang/model/entity/enums/WithdrawReason.java +++ b/src/main/java/umc/th/juinjang/domain/withdraw/model/WithdrawReason.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.entity.enums; +package umc.th.juinjang.domain.withdraw.model; public enum WithdrawReason { diff --git a/src/main/java/umc/th/juinjang/repository/withdraw/WithdrawRepository.java b/src/main/java/umc/th/juinjang/domain/withdraw/repository/WithdrawRepository.java similarity index 59% rename from src/main/java/umc/th/juinjang/repository/withdraw/WithdrawRepository.java rename to src/main/java/umc/th/juinjang/domain/withdraw/repository/WithdrawRepository.java index 89ceec6f..d234014e 100644 --- a/src/main/java/umc/th/juinjang/repository/withdraw/WithdrawRepository.java +++ b/src/main/java/umc/th/juinjang/domain/withdraw/repository/WithdrawRepository.java @@ -1,8 +1,8 @@ -package umc.th.juinjang.repository.withdraw; +package umc.th.juinjang.domain.withdraw.repository; import org.springframework.data.jpa.repository.JpaRepository; -import umc.th.juinjang.model.entity.Withdraw; -import umc.th.juinjang.model.entity.enums.WithdrawReason; +import umc.th.juinjang.domain.withdraw.model.Withdraw; +import umc.th.juinjang.domain.withdraw.model.WithdrawReason; import java.util.Optional; diff --git a/src/main/java/umc/th/juinjang/event/FlagSharedNoteEvent.java b/src/main/java/umc/th/juinjang/event/FlagSharedNoteEvent.java new file mode 100644 index 00000000..c812cccb --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/FlagSharedNoteEvent.java @@ -0,0 +1,17 @@ +package umc.th.juinjang.event; + +import umc.th.juinjang.domain.flag.model.FlagSharedNoteType; + +public record FlagSharedNoteEvent( + Long flaggedByMemberId, + Long targetSharedNoteId, + Long targetMemberId, + FlagSharedNoteType flagSharedNoteType +) { + public static FlagSharedNoteEvent of(Long flaggedByMemberId, + Long targetSharedNoteId, + Long targetMemberId, + FlagSharedNoteType flagSharedNoteType) { + return new FlagSharedNoteEvent(flaggedByMemberId, targetSharedNoteId, targetMemberId, flagSharedNoteType); + } +} diff --git a/src/main/java/umc/th/juinjang/event/PaymentEvent.java b/src/main/java/umc/th/juinjang/event/PaymentEvent.java new file mode 100644 index 00000000..678bcbe8 --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/PaymentEvent.java @@ -0,0 +1,15 @@ +package umc.th.juinjang.event; + +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; + +public record PaymentEvent( + Long memberId, + String nickname, + Long price, + Long pencilQuantity, + TransactionStatus transactionStatus +) { + public static PaymentEvent of(Long memberId, String nickname, Long price, Long pencilQuantity, TransactionStatus transactionStatus) { + return new PaymentEvent(memberId, nickname, price, pencilQuantity,transactionStatus); + } +} diff --git a/src/main/java/umc/th/juinjang/event/RewardViewCountEvent.java b/src/main/java/umc/th/juinjang/event/RewardViewCountEvent.java new file mode 100644 index 00000000..3fc1d375 --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/RewardViewCountEvent.java @@ -0,0 +1,13 @@ +package umc.th.juinjang.event; + +import umc.th.juinjang.domain.member.model.Member; + +public record RewardViewCountEvent( + Member member, + Long sharedNoteId, + Long viewCount +) { + public static RewardViewCountEvent of(Member member, Long sharedNoteId, Long viewCount) { + return new RewardViewCountEvent(member, sharedNoteId, viewCount); + } +} diff --git a/src/main/java/umc/th/juinjang/event/SignUpEvent.java b/src/main/java/umc/th/juinjang/event/SignUpEvent.java index 83fb3707..8adecc64 100644 --- a/src/main/java/umc/th/juinjang/event/SignUpEvent.java +++ b/src/main/java/umc/th/juinjang/event/SignUpEvent.java @@ -1,6 +1,6 @@ package umc.th.juinjang.event; -import umc.th.juinjang.model.entity.enums.MemberProvider; +import umc.th.juinjang.domain.member.model.MemberProvider; public record SignUpEvent( MemberProvider memberProvider, diff --git a/src/main/java/umc/th/juinjang/event/publisher/ApplicationFlagSharedNoteEventPublisher.java b/src/main/java/umc/th/juinjang/event/publisher/ApplicationFlagSharedNoteEventPublisher.java new file mode 100644 index 00000000..9e007aec --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/publisher/ApplicationFlagSharedNoteEventPublisher.java @@ -0,0 +1,24 @@ +package umc.th.juinjang.event.publisher; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.flag.model.FlagSharedNoteType; +import umc.th.juinjang.event.FlagSharedNoteEvent; + +@RequiredArgsConstructor +@Component +public class ApplicationFlagSharedNoteEventPublisher implements FlagSharedNoteEventPublisher { + + private final ApplicationEventPublisher applicationEventPublisher; + + @Override + public void publishFlagSharedNoteEvent(Long flaggedByMemberId, + Long targetSharedNoteId, + Long targetMemberId, + FlagSharedNoteType flagSharedNoteType) { + applicationEventPublisher.publishEvent(FlagSharedNoteEvent.of(flaggedByMemberId, targetSharedNoteId, + targetMemberId, flagSharedNoteType)); + } +} diff --git a/src/main/java/umc/th/juinjang/event/publisher/ApplicationMemberEventPublisherAdapter.java b/src/main/java/umc/th/juinjang/event/publisher/ApplicationMemberEventPublisherAdapter.java index 661e4385..a0c84ead 100644 --- a/src/main/java/umc/th/juinjang/event/publisher/ApplicationMemberEventPublisherAdapter.java +++ b/src/main/java/umc/th/juinjang/event/publisher/ApplicationMemberEventPublisherAdapter.java @@ -4,7 +4,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import umc.th.juinjang.event.SignUpEvent; -import umc.th.juinjang.model.entity.Member; +import umc.th.juinjang.domain.member.model.Member; @RequiredArgsConstructor @Component diff --git a/src/main/java/umc/th/juinjang/event/publisher/ApplicationPaymentEventPublisherAdapter.java b/src/main/java/umc/th/juinjang/event/publisher/ApplicationPaymentEventPublisherAdapter.java new file mode 100644 index 00000000..fd71001b --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/publisher/ApplicationPaymentEventPublisherAdapter.java @@ -0,0 +1,29 @@ +package umc.th.juinjang.event.publisher; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; +import umc.th.juinjang.event.PaymentEvent; + +@RequiredArgsConstructor +@Component +public class ApplicationPaymentEventPublisherAdapter implements PaymentEventPublisher { + + private final ApplicationEventPublisher applicationEventPublisher; + + @Override + public void publishPaymentEvent(Member buyer, Long price, Long pencilQuantity, TransactionStatus transactionStatus) { + applicationEventPublisher.publishEvent( + PaymentEvent.of( + buyer.getMemberId(), + buyer.getNickname(), + price, + pencilQuantity, + transactionStatus + ) + ); + } +} diff --git a/src/main/java/umc/th/juinjang/event/publisher/ApplicationRewardViewCountPublisherAdapter.java b/src/main/java/umc/th/juinjang/event/publisher/ApplicationRewardViewCountPublisherAdapter.java new file mode 100644 index 00000000..980758d7 --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/publisher/ApplicationRewardViewCountPublisherAdapter.java @@ -0,0 +1,20 @@ +package umc.th.juinjang.event.publisher; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.event.RewardViewCountEvent; + +@RequiredArgsConstructor +@Component +public class ApplicationRewardViewCountPublisherAdapter implements RewardViewCountPublisher { + + private final ApplicationEventPublisher applicationEventPublisher; + + @Override + public void checkViewCountRewardPolicy(Member member, Long sharedNoteId, Long viewCount) { + applicationEventPublisher.publishEvent(RewardViewCountEvent.of(member, sharedNoteId, viewCount)); + } +} diff --git a/src/main/java/umc/th/juinjang/event/publisher/FlagSharedNoteEventPublisher.java b/src/main/java/umc/th/juinjang/event/publisher/FlagSharedNoteEventPublisher.java new file mode 100644 index 00000000..3c134cd6 --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/publisher/FlagSharedNoteEventPublisher.java @@ -0,0 +1,11 @@ +package umc.th.juinjang.event.publisher; + +import umc.th.juinjang.domain.flag.model.FlagSharedNoteType; + +public interface FlagSharedNoteEventPublisher { + void publishFlagSharedNoteEvent( + Long flaggedByMemberId, + Long targetSharedNoteId, + Long targetMemberId, + FlagSharedNoteType flagSharedNoteType); +} diff --git a/src/main/java/umc/th/juinjang/event/publisher/MemberEventPublisher.java b/src/main/java/umc/th/juinjang/event/publisher/MemberEventPublisher.java index cb570379..0ac43ae6 100644 --- a/src/main/java/umc/th/juinjang/event/publisher/MemberEventPublisher.java +++ b/src/main/java/umc/th/juinjang/event/publisher/MemberEventPublisher.java @@ -1,6 +1,6 @@ package umc.th.juinjang.event.publisher; -import umc.th.juinjang.model.entity.Member; +import umc.th.juinjang.domain.member.model.Member; public interface MemberEventPublisher { void publishSignUpEvent(Member member); diff --git a/src/main/java/umc/th/juinjang/event/publisher/PaymentEventPublisher.java b/src/main/java/umc/th/juinjang/event/publisher/PaymentEventPublisher.java new file mode 100644 index 00000000..ee760ca9 --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/publisher/PaymentEventPublisher.java @@ -0,0 +1,8 @@ +package umc.th.juinjang.event.publisher; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; + +public interface PaymentEventPublisher { + void publishPaymentEvent(Member buyer, Long price, Long pencilQuantity, TransactionStatus transactionStatus); +} diff --git a/src/main/java/umc/th/juinjang/event/publisher/RewardViewCountPublisher.java b/src/main/java/umc/th/juinjang/event/publisher/RewardViewCountPublisher.java new file mode 100644 index 00000000..a380fe92 --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/publisher/RewardViewCountPublisher.java @@ -0,0 +1,7 @@ +package umc.th.juinjang.event.publisher; + +import umc.th.juinjang.domain.member.model.Member; + +public interface RewardViewCountPublisher { + void checkViewCountRewardPolicy(Member member, Long sharedNoteId, Long viewCount); +} diff --git a/src/main/java/umc/th/juinjang/event/subscriber/DiscordEventListener.java b/src/main/java/umc/th/juinjang/event/subscriber/DiscordEventListener.java index 0e33efc6..b7edc074 100644 --- a/src/main/java/umc/th/juinjang/event/subscriber/DiscordEventListener.java +++ b/src/main/java/umc/th/juinjang/event/subscriber/DiscordEventListener.java @@ -1,31 +1,57 @@ package umc.th.juinjang.event.subscriber; -import lombok.RequiredArgsConstructor; -import org.springframework.core.env.Environment; -import org.springframework.core.env.Profiles; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; +import umc.th.juinjang.event.FlagSharedNoteEvent; +import umc.th.juinjang.event.PaymentEvent; import umc.th.juinjang.event.SignUpEvent; -import umc.th.juinjang.external.discord.DiscordAlertProvider; +import umc.th.juinjang.external.openfeign.discord.DiscordAlertProvider; @Component @RequiredArgsConstructor public class DiscordEventListener { - private final DiscordAlertProvider discordAlertProvider; - private final Environment environment; + private final DiscordAlertProvider discordAlertProvider; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + public void handleSignUpEvent(SignUpEvent event) { + discordAlertProvider.sendMemberCreateAlertToDiscord( + String.format(EventMessage.SIGN_UP_MESSAGE.getMessage(), event.memberProvider(), event.count(), + event.name())); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + public void handleFlagSharedNoteEvent(FlagSharedNoteEvent event) { + discordAlertProvider.sendReportSharedNoteAlertToDiscord(String.format( + EventMessage.FLAG_SHARED_NOTE_MESSAGE.getMessage(), + event.flaggedByMemberId(), + event.flagSharedNoteType().getDescription(), + event.targetMemberId(), + event.targetSharedNoteId() + )); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + public void handlePaymentEvent(PaymentEvent event) { + String message = event.transactionStatus().equals(TransactionStatus.SUCCESS) ? + String.format( + EventMessage.PAYMENT_COMPLETED_MESSAGE.getMessage(), + event.memberId(), event.nickname(), event.pencilQuantity(), event.price(), event.transactionStatus() + ) + : String.format( + EventMessage.PAYMENT_REFUNDED_MESSAGE.getMessage(), + event.memberId(), event.nickname(), event.pencilQuantity(), event.price(), event.transactionStatus() + ); - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - @Async - public void handleSignUpEvent (SignUpEvent event){ - if (isProdEnv()) { - discordAlertProvider.sendAlertToDiscord(String.format(EventMessage.SIGN_UP_MESSAGE.getMessage(), event.memberProvider(), event.count(), event.name())); - } - } + discordAlertProvider.sendPaymentAlertToDiscord(message); + } - private boolean isProdEnv() { - return environment.acceptsProfiles(Profiles.of("prod")); - } } diff --git a/src/main/java/umc/th/juinjang/event/subscriber/EventMessage.java b/src/main/java/umc/th/juinjang/event/subscriber/EventMessage.java index ebc8e988..c5daa519 100644 --- a/src/main/java/umc/th/juinjang/event/subscriber/EventMessage.java +++ b/src/main/java/umc/th/juinjang/event/subscriber/EventMessage.java @@ -7,7 +7,10 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter public enum EventMessage { - SIGN_UP_MESSAGE("주인장에 %s %d번째 유저 < %s >님이 생겼어요!"); + SIGN_UP_MESSAGE("주인장에 %s %d번째 유저 < %s >님이 생겼어요!"), + FLAG_SHARED_NOTE_MESSAGE("< %d >번 유저가 [ %s ]의 사유로 < %d >번 유저의 < %d >번 공유 노트를 신고했습니다."), + PAYMENT_COMPLETED_MESSAGE("<%d>번 유저 < %s >님이 %d개의 연필을 %d원에 결제했습니다. (상태: %s)"), + PAYMENT_REFUNDED_MESSAGE("<%d>번 유저 < %s >님의 연필 %d개 구매가 환불되었습니다. (환불금액: %d원)"); - private final String message; + private final String message; } diff --git a/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java b/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java new file mode 100644 index 00000000..be91db3f --- /dev/null +++ b/src/main/java/umc/th/juinjang/event/subscriber/RewardViewCountEventListener.java @@ -0,0 +1,39 @@ +package umc.th.juinjang.event.subscriber; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.reward.service.RewardService; +import umc.th.juinjang.domain.note.shared.model.ViewCountPolicy; +import umc.th.juinjang.event.RewardViewCountEvent; + +@Component +@RequiredArgsConstructor +@Slf4j +public class RewardViewCountEventListener { + + private final ViewCountPolicy viewCountPolicy; + private final RewardService rewardService; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Async + public void handleRewardViewCountEvent(RewardViewCountEvent rewardViewCountEvent) { + + Long reward = viewCountPolicy.getRewardForExactMilestone(rewardViewCountEvent.viewCount()); + + if (reward == null) { + return; + } + + try { + rewardService.giveViewCountReward(rewardViewCountEvent.member(), rewardViewCountEvent.sharedNoteId(), + rewardViewCountEvent.viewCount(), reward); + } catch (Exception e) { + log.error("조회수 리워드 지급 실패", e); + } + } +} diff --git a/src/main/java/umc/th/juinjang/external/discord/DiscordAlertProvider.java b/src/main/java/umc/th/juinjang/external/discord/DiscordAlertProvider.java deleted file mode 100644 index 5e8c455b..00000000 --- a/src/main/java/umc/th/juinjang/external/discord/DiscordAlertProvider.java +++ /dev/null @@ -1,23 +0,0 @@ -package umc.th.juinjang.external.discord; - -import feign.FeignException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import umc.th.juinjang.external.discord.dto.DiscordAlert; - -@RequiredArgsConstructor -@Component -@Slf4j -public class DiscordAlertProvider { - - private final DiscordFeignClient discordFeignClient; - - public void sendAlertToDiscord(String content) { - try { - discordFeignClient.sendAlert(DiscordAlert.createAlert(content)); - } catch (FeignException e) { - log.info(StatusMessage.DISCORD_ALERT_ERROR.getMessage()+ " " +e.getMessage()); - } - } -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/external/discord/DiscordFeignClient.java b/src/main/java/umc/th/juinjang/external/discord/DiscordFeignClient.java deleted file mode 100644 index 4bc8ed7f..00000000 --- a/src/main/java/umc/th/juinjang/external/discord/DiscordFeignClient.java +++ /dev/null @@ -1,13 +0,0 @@ -package umc.th.juinjang.external.discord; - -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import umc.th.juinjang.external.discord.dto.DiscordAlert; - -@FeignClient(name = "${discord.name}", url = "${discord.webhook-url}") -public interface DiscordFeignClient { - @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) - void sendAlert(@RequestBody DiscordAlert discordMessage); -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/config/FeignClientConfig.java b/src/main/java/umc/th/juinjang/external/openfeign/FeignClientConfig.java similarity index 86% rename from src/main/java/umc/th/juinjang/config/FeignClientConfig.java rename to src/main/java/umc/th/juinjang/external/openfeign/FeignClientConfig.java index 093a8f85..b4ddf7b8 100644 --- a/src/main/java/umc/th/juinjang/config/FeignClientConfig.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/FeignClientConfig.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.config; +package umc.th.juinjang.external.openfeign; import org.springframework.cloud.openfeign.EnableFeignClients; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleClient.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleClient.java similarity index 96% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleClient.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/AppleClient.java index 0370548f..915a231f 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleClient.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleClient.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.external.openfeign.apple; import org.springframework.cloud.openfeign.FeignClient; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleClientSecretGenerator.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleClientSecretGenerator.java similarity index 96% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleClientSecretGenerator.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/AppleClientSecretGenerator.java index b58c0e26..2d93b48e 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleClientSecretGenerator.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleClientSecretGenerator.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.external.openfeign.apple; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleOAuthProvider.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleOAuthProvider.java similarity index 85% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleOAuthProvider.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/AppleOAuthProvider.java index 553733ae..9d417317 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleOAuthProvider.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleOAuthProvider.java @@ -1,13 +1,13 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.external.openfeign.apple; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import umc.th.juinjang.apiPayload.exception.handler.MemberHandler; +import umc.th.juinjang.common.exception.handler.MemberHandler; -import static umc.th.juinjang.apiPayload.code.status.ErrorStatus.FAILED_TO_LOAD_PRIVATE_KEY; +import static umc.th.juinjang.common.code.status.ErrorStatus.FAILED_TO_LOAD_PRIVATE_KEY; @Component @RequiredArgsConstructor diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/ApplePrivateKeyGenerator.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePrivateKeyGenerator.java similarity index 97% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/ApplePrivateKeyGenerator.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePrivateKeyGenerator.java index 77d921c0..567afe2d 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/ApplePrivateKeyGenerator.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePrivateKeyGenerator.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.external.openfeign.apple; import org.apache.commons.codec.binary.Base64; import org.springframework.core.io.ClassPathResource; diff --git a/src/main/java/umc/th/juinjang/utils/ApplePublicKeyGenerator.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePublicKeyGenerator.java similarity index 85% rename from src/main/java/umc/th/juinjang/utils/ApplePublicKeyGenerator.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePublicKeyGenerator.java index 4d6c240a..758273ac 100644 --- a/src/main/java/umc/th/juinjang/utils/ApplePublicKeyGenerator.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePublicKeyGenerator.java @@ -1,13 +1,11 @@ -package umc.th.juinjang.utils; +package umc.th.juinjang.external.openfeign.apple; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.model.dto.auth.apple.ApplePublicKeyResponse; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; -import javax.naming.AuthenticationException; import java.math.BigInteger; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; @@ -16,7 +14,6 @@ import java.security.spec.RSAPublicKeySpec; import java.util.Base64; import java.util.Map; -import java.util.Optional; @Component @RequiredArgsConstructor diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/ApplePublicKeyResponse.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePublicKeyResponse.java similarity index 76% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/ApplePublicKeyResponse.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePublicKeyResponse.java index a19ab264..84771b69 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/ApplePublicKeyResponse.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/ApplePublicKeyResponse.java @@ -1,12 +1,10 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.external.openfeign.apple; import lombok.Getter; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; +import umc.th.juinjang.common.ExceptionHandler; +import umc.th.juinjang.common.code.status.ErrorStatus; -import javax.naming.AuthenticationException; import java.util.List; -import java.util.Optional; @Getter public class ApplePublicKeyResponse { diff --git a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleTokenResponse.java b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleTokenResponse.java similarity index 95% rename from src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleTokenResponse.java rename to src/main/java/umc/th/juinjang/external/openfeign/apple/AppleTokenResponse.java index 5a1d73fe..a07e844f 100644 --- a/src/main/java/umc/th/juinjang/model/dto/auth/apple/AppleTokenResponse.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/apple/AppleTokenResponse.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.model.dto.auth.apple; +package umc.th.juinjang.external.openfeign.apple; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/umc/th/juinjang/external/openfeign/discord/DiscordAlertProvider.java b/src/main/java/umc/th/juinjang/external/openfeign/discord/DiscordAlertProvider.java new file mode 100644 index 00000000..0faeff47 --- /dev/null +++ b/src/main/java/umc/th/juinjang/external/openfeign/discord/DiscordAlertProvider.java @@ -0,0 +1,65 @@ +package umc.th.juinjang.external.openfeign.discord; + +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import umc.th.juinjang.external.openfeign.discord.dto.DiscordAlert; + +@Component +@Slf4j +public class DiscordAlertProvider { + private final WebClient webClient; + + @Value("${discord.member-create}") + private String memberCreateWebhookUrl; + + @Value("${discord.report-shared-note}") + private String reportSharedNoteWebhookUrl; + + @Value("${discord.execute-payment}") + private String executePaymentWebhookUrl; + + public DiscordAlertProvider(WebClient.Builder builder) { + this.webClient = builder.build(); + } + + private void sendWebClient(String url, String content) { + webClient.post() + .uri(url) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(DiscordAlert.createAlert(content)) + .retrieve() + .bodyToMono(Void.class) + .subscribe(); + } + + public void sendMemberCreateAlertToDiscord(String content) { + try { + sendWebClient(memberCreateWebhookUrl, content); + } catch (Exception e) { + log.info(StatusMessage.DISCORD_ALERT_ERROR.getMessage() + " " + e.getMessage()); + } + } + + public void sendReportSharedNoteAlertToDiscord(String content) { + try { + sendWebClient(reportSharedNoteWebhookUrl, content); + } catch (FeignException e) { + log.info(StatusMessage.DISCORD_ALERT_ERROR.getMessage() + " " + e.getMessage()); + } + } + + public void sendPaymentAlertToDiscord(String content) { + try { + sendWebClient(executePaymentWebhookUrl, content); + } catch (FeignException e) { + log.info("{} {}", StatusMessage.DISCORD_ALERT_ERROR.getMessage(), e.getMessage()); + } + } +} diff --git a/src/main/java/umc/th/juinjang/external/discord/StatusMessage.java b/src/main/java/umc/th/juinjang/external/openfeign/discord/StatusMessage.java similarity index 87% rename from src/main/java/umc/th/juinjang/external/discord/StatusMessage.java rename to src/main/java/umc/th/juinjang/external/openfeign/discord/StatusMessage.java index 26f98c86..7621eabc 100644 --- a/src/main/java/umc/th/juinjang/external/discord/StatusMessage.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/discord/StatusMessage.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.external.discord; +package umc.th.juinjang.external.openfeign.discord; import lombok.AccessLevel; import lombok.Getter; diff --git a/src/main/java/umc/th/juinjang/external/discord/dto/DiscordAlert.java b/src/main/java/umc/th/juinjang/external/openfeign/discord/dto/DiscordAlert.java similarity index 72% rename from src/main/java/umc/th/juinjang/external/discord/dto/DiscordAlert.java rename to src/main/java/umc/th/juinjang/external/openfeign/discord/dto/DiscordAlert.java index 842aa6d4..1a6b922b 100644 --- a/src/main/java/umc/th/juinjang/external/discord/dto/DiscordAlert.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/discord/dto/DiscordAlert.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.external.discord.dto; +package umc.th.juinjang.external.openfeign.discord.dto; public record DiscordAlert(String content) { public static DiscordAlert createAlert(String content) { diff --git a/src/main/java/umc/th/juinjang/controller/KakaoUnlinkClient.java b/src/main/java/umc/th/juinjang/external/openfeign/kakao/KakaoUnlinkClient.java similarity index 93% rename from src/main/java/umc/th/juinjang/controller/KakaoUnlinkClient.java rename to src/main/java/umc/th/juinjang/external/openfeign/kakao/KakaoUnlinkClient.java index 7cc2faae..e9a379bc 100644 --- a/src/main/java/umc/th/juinjang/controller/KakaoUnlinkClient.java +++ b/src/main/java/umc/th/juinjang/external/openfeign/kakao/KakaoUnlinkClient.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.controller; +package umc.th.juinjang.external.openfeign.kakao; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/umc/th/juinjang/config/AWSS3Config.java b/src/main/java/umc/th/juinjang/external/s3/AWSS3Config.java similarity index 97% rename from src/main/java/umc/th/juinjang/config/AWSS3Config.java rename to src/main/java/umc/th/juinjang/external/s3/AWSS3Config.java index d32e2523..25822b97 100644 --- a/src/main/java/umc/th/juinjang/config/AWSS3Config.java +++ b/src/main/java/umc/th/juinjang/external/s3/AWSS3Config.java @@ -1,4 +1,4 @@ -package umc.th.juinjang.config; +package umc.th.juinjang.external.s3; import ch.qos.logback.classic.Logger; import com.amazonaws.auth.AWSStaticCredentialsProvider; diff --git a/src/main/java/umc/th/juinjang/service/external/S3Service.java b/src/main/java/umc/th/juinjang/external/s3/S3Service.java similarity index 67% rename from src/main/java/umc/th/juinjang/service/external/S3Service.java rename to src/main/java/umc/th/juinjang/external/s3/S3Service.java index 7a3113f5..71d6a62d 100644 --- a/src/main/java/umc/th/juinjang/service/external/S3Service.java +++ b/src/main/java/umc/th/juinjang/external/s3/S3Service.java @@ -1,11 +1,10 @@ -package umc.th.juinjang.service.external; +package umc.th.juinjang.external.s3; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.DeleteObjectRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import java.io.File; -import java.io.IOException; import java.io.FileOutputStream; import java.io.InputStream; import java.net.URI; @@ -13,29 +12,33 @@ import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; -import org.springframework.beans.factory.annotation.Value; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.S3Handler; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.S3Handler; @Slf4j -@RequiredArgsConstructor // final 멤버변수가 있으면 생성자 항목에 포함시킴 -@Component +@RequiredArgsConstructor @Service public class S3Service { - + private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final AmazonS3Client amazonS3Client; @Value("${cloud.aws.s3.bucket}") private String bucket; - // MultipartFile을 전달받아 File로 전환한 후 S3에 업로드 - public String upload(MultipartFile multipartFile, String dirName) throws IOException { - File uploadFile = convert(multipartFile) - .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패")); - return upload(uploadFile, dirName, multipartFile.getInputStream(), multipartFile.getSize(), multipartFile.getContentType()); + public String upload(MultipartFile multipartFile, String dirName) { + File uploadFile = convert(multipartFile).orElseThrow(() -> new S3Handler(ErrorStatus.IMAGE_EMPTY)); + + try { + return upload(uploadFile, dirName, multipartFile.getInputStream(), multipartFile.getSize(), multipartFile.getContentType()); + } catch (Exception e) { + logger.error("파일 업로드 중 error 발생"); + throw new S3Handler(ErrorStatus._INTERNAL_SERVER_ERROR); + } } private String upload(File uploadFile,String dirName, InputStream inputStream, Long fileSize, String contentType) { @@ -72,16 +75,22 @@ private void removeNewFile(File targetFile) { } } - private Optional convert(MultipartFile file) throws IOException { - String originalFilename = file.getOriginalFilename(); - String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9.-]", "_"); - File convertFile = new File(safeFilename); - - if(convertFile.createNewFile()) { - try (FileOutputStream fos = new FileOutputStream(convertFile)) { - fos.write(file.getBytes()); + private Optional convert(MultipartFile file) { + try { + String originalFilename = file.getOriginalFilename(); + String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9.-]", "_"); + File convertFile = new File(safeFilename); + + if (convertFile.createNewFile()) { + try (FileOutputStream fos = new FileOutputStream(convertFile)) { + fos.write(file.getBytes()); + } + return Optional.of(convertFile); + } else { + return Optional.empty(); } - return Optional.of(convertFile); + } catch (Exception e) { + logger.error("파일 변환 중 error 발생" +e); } return Optional.empty(); } diff --git a/src/main/java/umc/th/juinjang/external/safeSearch/SafeSearchClient.java b/src/main/java/umc/th/juinjang/external/safeSearch/SafeSearchClient.java new file mode 100644 index 00000000..61a6c201 --- /dev/null +++ b/src/main/java/umc/th/juinjang/external/safeSearch/SafeSearchClient.java @@ -0,0 +1,78 @@ +package umc.th.juinjang.external.safeSearch; + +import java.util.Collections; + +import org.springframework.stereotype.Component; + +import com.google.cloud.vision.v1.AnnotateImageRequest; +import com.google.cloud.vision.v1.AnnotateImageResponse; +import com.google.cloud.vision.v1.BatchAnnotateImagesResponse; +import com.google.cloud.vision.v1.Feature; +import com.google.cloud.vision.v1.Feature.Type; +import com.google.cloud.vision.v1.Image; +import com.google.cloud.vision.v1.ImageAnnotatorClient; +import com.google.cloud.vision.v1.ImageSource; +import com.google.cloud.vision.v1.Likelihood; +import com.google.cloud.vision.v1.SafeSearchAnnotation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SafeSearchClient { + + public boolean isSafeImage(String imageUrl, + Likelihood adultThreshold, + Likelihood spoofThreshold, + Likelihood medicalThreshold, + Likelihood violenceThreshold, + Likelihood racyThreshold) { + try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) { + + ImageSource imgSource = ImageSource.newBuilder().setImageUri(imageUrl).build(); + Image img = Image.newBuilder().setSource(imgSource).build(); + + Feature feature = Feature.newBuilder().setType(Type.SAFE_SEARCH_DETECTION).build(); + AnnotateImageRequest request = AnnotateImageRequest.newBuilder() + .addFeatures(feature) + .setImage(img) + .build(); + + BatchAnnotateImagesResponse response = vision.batchAnnotateImages(Collections.singletonList(request)); + AnnotateImageResponse res = response.getResponsesList().get(0); + + if (res.hasError()) { + throw new RuntimeException("Vision API Error: " + res.getError().getMessage()); + } + + SafeSearchAnnotation annotation = res.getSafeSearchAnnotation(); + log.info("SafeSearch 분석 결과 for [{}]", imageUrl); + log.info(" - adult: {}", annotation.getAdult()); + log.info(" - spoof: {}", annotation.getSpoof()); + log.info(" - medical: {}", annotation.getMedical()); + log.info(" - violence: {}", annotation.getViolence()); + log.info(" - racy: {}", annotation.getRacy()); + return isAnnotationSafe(annotation, adultThreshold, spoofThreshold, medicalThreshold, violenceThreshold, + racyThreshold); + + } catch (Exception e) { + log.error("Vision API 호출 실패 (Exception)", e); + throw new RuntimeException("Vision API 호출 실패", e); + } + } + + public boolean isAnnotationSafe(SafeSearchAnnotation annotation, + Likelihood adultThreshold, + Likelihood spoofThreshold, + Likelihood medicalThreshold, + Likelihood violenceThreshold, + Likelihood racyThreshold) { + return annotation.getAdult().compareTo(adultThreshold) < 0 && + annotation.getSpoof().compareTo(spoofThreshold) < 0 && + annotation.getMedical().compareTo(medicalThreshold) < 0 && + annotation.getViolence().compareTo(violenceThreshold) < 0 && + annotation.getRacy().compareTo(racyThreshold) < 0; + } +} diff --git a/src/main/java/umc/th/juinjang/model/dto/TempRequest.java b/src/main/java/umc/th/juinjang/model/dto/TempRequest.java deleted file mode 100644 index 10acd71b..00000000 --- a/src/main/java/umc/th/juinjang/model/dto/TempRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package umc.th.juinjang.model.dto; - -public class TempRequest { -} diff --git a/src/main/java/umc/th/juinjang/model/dto/TempResponse.java b/src/main/java/umc/th/juinjang/model/dto/TempResponse.java deleted file mode 100644 index b70d7db9..00000000 --- a/src/main/java/umc/th/juinjang/model/dto/TempResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package umc.th.juinjang.model.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -public class TempResponse { - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class TempTestDTO{ - String testString; - } - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class TempExceptionDTO{ - Integer flag; - } -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerResponseDTO.java b/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerResponseDTO.java deleted file mode 100644 index 0ddeb9f2..00000000 --- a/src/main/java/umc/th/juinjang/model/dto/checklist/ChecklistAnswerResponseDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package umc.th.juinjang.model.dto.checklist; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionCategory; -import umc.th.juinjang.model.entity.enums.ChecklistQuestionType; - -public class ChecklistAnswerResponseDTO { - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class AnswerDto { - private Long answerId; - private Long questionId; -// private ChecklistQuestionCategory category; - private Long limjangId; - private String answer; - private ChecklistQuestionType answerType; - } -} diff --git a/src/main/java/umc/th/juinjang/model/dto/checklist/ReportResponseDTO.java b/src/main/java/umc/th/juinjang/model/dto/checklist/ReportResponseDTO.java deleted file mode 100644 index 1446b09c..00000000 --- a/src/main/java/umc/th/juinjang/model/dto/checklist/ReportResponseDTO.java +++ /dev/null @@ -1,28 +0,0 @@ -package umc.th.juinjang.model.dto.checklist; - -import lombok.*; -import umc.th.juinjang.model.dto.limjang.response.LimjangDetailResponseDTO; - -@AllArgsConstructor -@Getter -@Setter -public class ReportResponseDTO { - private ReportDTO reportDTO; - private LimjangDetailResponseDTO.DetailDto limjangDto; - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class ReportDTO { - private Long reportId; - private String indoorKeyWord; - private String publicSpaceKeyWord; - private String locationConditionsWord; - private Float indoorRate; - private Float publicSpaceRate; - private Float locationConditionsRate; - private Float totalRate; - } -} diff --git a/src/main/java/umc/th/juinjang/model/dto/member/MemberRequestDto.java b/src/main/java/umc/th/juinjang/model/dto/member/MemberRequestDto.java deleted file mode 100644 index 7d8edfe7..00000000 --- a/src/main/java/umc/th/juinjang/model/dto/member/MemberRequestDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package umc.th.juinjang.model.dto.member; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class MemberRequestDto { - private String nickname; -} diff --git a/src/main/java/umc/th/juinjang/model/dto/member/MemberResponseDto.java b/src/main/java/umc/th/juinjang/model/dto/member/MemberResponseDto.java deleted file mode 100644 index 4fd4627c..00000000 --- a/src/main/java/umc/th/juinjang/model/dto/member/MemberResponseDto.java +++ /dev/null @@ -1,29 +0,0 @@ -package umc.th.juinjang.model.dto.member; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -public class MemberResponseDto { - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class nicknameDto { - private String nickname; - } - - @Builder - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static class profileDto { - private String nickname; - private String email; - private String provider; - private String image; - } - -} diff --git a/src/main/java/umc/th/juinjang/model/entity/Limjang.java b/src/main/java/umc/th/juinjang/model/entity/Limjang.java deleted file mode 100644 index 78f9d668..00000000 --- a/src/main/java/umc/th/juinjang/model/entity/Limjang.java +++ /dev/null @@ -1,125 +0,0 @@ -package umc.th.juinjang.model.entity; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.OneToOne; -import java.util.ArrayList; -import java.util.List; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.BatchSize; -import org.hibernate.annotations.ColumnDefault; -import org.hibernate.annotations.Where; -import umc.th.juinjang.model.entity.common.BaseEntity; -import umc.th.juinjang.model.entity.enums.LimjangPropertyType; -import umc.th.juinjang.model.entity.enums.LimjangPriceType; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Where(clause = "deleted = false") -public class Limjang extends BaseEntity { - - @Id - @Column(name="limjang_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long limjangId; - - // 회원 ID - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member memberId; - - // 가격 ID - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "price_id", referencedColumnName = "price_id") - private LimjangPrice limjangPrice; - - // 거래 목적 - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private LimjangPurpose purpose; - - // 매물 유형 - @Enumerated(EnumType.STRING) - private LimjangPropertyType propertyType; - - // 가격 유형 - @Enumerated(EnumType.STRING) - private LimjangPriceType priceType; - - // 도로명 주소 - @Column(nullable = false) - private String address; - - private String addressDetail; - - // 집 별명 - @Column(nullable = false) - private String nickname; - - @Column(columnDefinition = "text") - private String memo; - - // 양방향 매핑 - @OneToMany(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) - private List answerList = new ArrayList<>(); - - @OneToOne(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) - private Report report; - - @OneToMany(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) - private List recordList = new ArrayList<>(); - - @OneToMany(mappedBy = "limjangId", cascade = CascadeType.ALL, orphanRemoval = true) - @BatchSize(size = 100) - private List imageList = new ArrayList<>(); - - @Column(name = "record_count") - @ColumnDefault("0") //default 0 - private int recordCount; - - @Column(nullable = false, name = "deleted") - private boolean deleted = Boolean.FALSE; - - public void saveMemberAndPrice(Member member, LimjangPrice limjangPrice){ - this.limjangPrice = limjangPrice; - this.memberId = member; - } - - public void updateLimjang(String address, String addressDetail, String nickname, LimjangPriceType priceType){ - this.address = address; - this.addressDetail = addressDetail; - this.nickname = nickname; - this.priceType = priceType; - } - - public void updateMemo(String memo){ - this.memo = memo; - } - public void saveImages(Image image){ - this.imageList.add(image); - } - - public String getDefaultImage() { - return this.imageList.isEmpty() ? null :this.imageList.get(0).getImageUrl(); - } - - -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/model/entity/LimjangPrice.java b/src/main/java/umc/th/juinjang/model/entity/LimjangPrice.java deleted file mode 100644 index 22ac2497..00000000 --- a/src/main/java/umc/th/juinjang/model/entity/LimjangPrice.java +++ /dev/null @@ -1,43 +0,0 @@ -package umc.th.juinjang.model.entity; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import umc.th.juinjang.model.entity.common.BaseEntity; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class LimjangPrice extends BaseEntity { - - @Id - @Column(name="price_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long priceId; - - private String marketPrice; - - private String sellingPrice; - - private String depositPrice; - - private String monthlyRent; - - private String pullRent; - - @OneToOne(mappedBy = "limjangPrice", cascade = CascadeType.ALL, orphanRemoval = true) - private Limjang limjang; - - public void updateLimjangPrice(LimjangPrice newLimjangPrice){ - this.marketPrice = newLimjangPrice.getMarketPrice(); - this.sellingPrice = newLimjangPrice.getSellingPrice(); - this.depositPrice = newLimjangPrice.getDepositPrice(); - this.monthlyRent = newLimjangPrice.getMonthlyRent(); - this.pullRent = newLimjangPrice.getPullRent(); - } -} diff --git a/src/main/java/umc/th/juinjang/model/entity/Member.java b/src/main/java/umc/th/juinjang/model/entity/Member.java deleted file mode 100644 index 96792496..00000000 --- a/src/main/java/umc/th/juinjang/model/entity/Member.java +++ /dev/null @@ -1,118 +0,0 @@ -package umc.th.juinjang.model.entity; - -import jakarta.persistence.*; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import umc.th.juinjang.model.entity.common.BaseEntity; -import umc.th.juinjang.model.entity.enums.MemberProvider; - -@Entity -@Getter -@Builder -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -public class Member extends BaseEntity implements UserDetails { - - @Id - @Column(name="member_id") - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long memberId; - - @Column(nullable = false) - private String email; - - private String nickname; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private MemberProvider provider; - - @Column(name = "agree_version") - private String agreeVersion; - - // apple client id값을 의미 - @Column(name = "apple_sub", unique = true) - private String appleSub; - - // kakao target id값 의미 (카카오의 유저 식별값. 탈퇴할 때 필요) - @Column(name="target_id", unique = true) - private Long kakaoTargetId; - - @Lob - private String imageUrl; - - @Column(nullable = false) - private String refreshToken; - - @Column(nullable = false) - private LocalDateTime refreshTokenExpiresAt; - - @OneToMany(mappedBy = "memberId", cascade = CascadeType.ALL, orphanRemoval = true) - private List limjangList = new ArrayList<>(); - - // refreshToken 재발급 - public void updateRefreshToken(String refreshToken) { - this.refreshToken = refreshToken; - this.refreshTokenExpiresAt = LocalDateTime.now().plusDays(7); - } - - // 로그아웃 시 토큰 만료 - public void refreshTokenExpires() { - this.refreshToken = ""; - this.refreshTokenExpiresAt = LocalDateTime.now(); - } - - @Override - public Collection getAuthorities() { - return null; - } - - @Override - public String getPassword() { - return null; - } - - @Override - public String getUsername() { - return this.email; - } - - @Override - public boolean isAccountNonExpired() { - return false; - } - - @Override - public boolean isAccountNonLocked() { - return false; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } - - public void updateNickname(String nickname) { - this.nickname = nickname; - } - - public void updateImage(String imageUrl) { - this.imageUrl = imageUrl; - } - - public void updateAgreeVersion(final String agreeVersion) { this.agreeVersion = agreeVersion; } -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/model/entity/common/BaseEntity.java b/src/main/java/umc/th/juinjang/model/entity/common/BaseEntity.java deleted file mode 100644 index a0f98e14..00000000 --- a/src/main/java/umc/th/juinjang/model/entity/common/BaseEntity.java +++ /dev/null @@ -1,25 +0,0 @@ -package umc.th.juinjang.model.entity.common; - -import jakarta.persistence.Column; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.MappedSuperclass; -import java.time.LocalDateTime; -import lombok.Builder; -import lombok.Getter; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) -@Getter -public abstract class BaseEntity { - - @CreatedDate - @Column(name = "created_at") - private LocalDateTime createdAt; - - @LastModifiedDate - @Column(name = "updated_at") - private LocalDateTime updatedAt; -} diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPriceType.java b/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPriceType.java deleted file mode 100644 index 7d97a1d3..00000000 --- a/src/main/java/umc/th/juinjang/model/entity/enums/LimjangPriceType.java +++ /dev/null @@ -1,32 +0,0 @@ -package umc.th.juinjang.model.entity.enums; - -import java.util.Arrays; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; - -public enum LimjangPriceType { - SALE(0), // 매매 - PULL_RENT(1), // 전세 - MONTHLY_RENT(2), //월세 - - MARKET_PRICE(3); // 실거래가 - - - private final int value; - - LimjangPriceType(int value) { - this.value = value; - } - - // 숫자 리턴 - public int getValue() { - return value; - } - - public static LimjangPriceType find(int inputValue) { - return Arrays.stream(LimjangPriceType.values()) - .filter(it -> it.value == inputValue) - .findAny() - .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_POST_TYPE_ERROR)); - } -} diff --git a/src/main/java/umc/th/juinjang/model/entity/enums/ScrapActionType.java b/src/main/java/umc/th/juinjang/model/entity/enums/ScrapActionType.java deleted file mode 100644 index b78deba0..00000000 --- a/src/main/java/umc/th/juinjang/model/entity/enums/ScrapActionType.java +++ /dev/null @@ -1,8 +0,0 @@ -package umc.th.juinjang.model.entity.enums; - -public enum ScrapActionType { - - SCRAP, - UNSCRAP - -} diff --git a/src/main/java/umc/th/juinjang/monitoring/ApiFilterConfig.java b/src/main/java/umc/th/juinjang/monitoring/ApiFilterConfig.java new file mode 100644 index 00000000..3e7a1466 --- /dev/null +++ b/src/main/java/umc/th/juinjang/monitoring/ApiFilterConfig.java @@ -0,0 +1,30 @@ +package umc.th.juinjang.monitoring; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; + +import umc.th.juinjang.monitoring.ApiLoggerFilter; + +@Configuration +@Slf4j +public class ApiFilterConfig { + + @Value("${logging.api.excluded-paths}") + private List excludedUrls; + + @Bean + public FilterRegistrationBean loggingFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new ApiLoggerFilter(excludedUrls)); + registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 순서 설정 + registrationBean.setUrlPatterns(List.of("/*")); + return registrationBean; + } +} diff --git a/src/main/java/umc/th/juinjang/monitoring/ApiLogGenerator.java b/src/main/java/umc/th/juinjang/monitoring/ApiLogGenerator.java index 2755f0e9..643368a3 100644 --- a/src/main/java/umc/th/juinjang/monitoring/ApiLogGenerator.java +++ b/src/main/java/umc/th/juinjang/monitoring/ApiLogGenerator.java @@ -1,6 +1,6 @@ package umc.th.juinjang.monitoring; -import static umc.th.juinjang.utils.LoggerProvider.getLogger; +import static umc.th.juinjang.common.LoggerProvider.getLogger; import org.slf4j.Logger; import org.slf4j.MDC; diff --git a/src/main/java/umc/th/juinjang/monitoring/ApiLogPrinter.java b/src/main/java/umc/th/juinjang/monitoring/ApiLogPrinter.java index bade2c7e..edf9b48b 100644 --- a/src/main/java/umc/th/juinjang/monitoring/ApiLogPrinter.java +++ b/src/main/java/umc/th/juinjang/monitoring/ApiLogPrinter.java @@ -1,6 +1,6 @@ package umc.th.juinjang.monitoring; -import static umc.th.juinjang.utils.LoggerProvider.getLogger; +import static umc.th.juinjang.common.LoggerProvider.getLogger; import org.slf4j.Logger; import org.springframework.stereotype.Component; diff --git a/src/main/java/umc/th/juinjang/monitoring/ApiLogRequestLogGenerator.java b/src/main/java/umc/th/juinjang/monitoring/ApiLogRequestLogGenerator.java index 2082d004..54642c43 100644 --- a/src/main/java/umc/th/juinjang/monitoring/ApiLogRequestLogGenerator.java +++ b/src/main/java/umc/th/juinjang/monitoring/ApiLogRequestLogGenerator.java @@ -1,52 +1,61 @@ package umc.th.juinjang.monitoring; import jakarta.servlet.http.HttpServletRequest; + import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; + import org.springframework.web.util.ContentCachingRequestWrapper; public class ApiLogRequestLogGenerator extends ApiLogGenerator { - private final ContentCachingRequestWrapper request; - - public ApiLogRequestLogGenerator(ContentCachingRequestWrapper request) { - this.request = request; - } - - @Override - public String generateLog() { - StringBuilder logBuilder = new StringBuilder(); - - logBuilder.append("Request : ").append(" "); - logBuilder.append(getBaseLogInfo()); - logBuilder.append("[method] ").append(request.getMethod()).append(" "); - logBuilder.append("[uri] ").append(getQuery()).append(" "); - logBuilder.append("[headers] ").append(getHeadersAsString(request)).append(" "); - logBuilder.append("[requestBody] ").append(getBody(request.getContentAsByteArray())).append(" "); - - return logBuilder.toString(); - } - - private String getQuery() { - String uri = request.getRequestURI(); - String queryString = request.getQueryString(); - - if (queryString != null) { - uri += "?" + queryString; - } - return uri; - } - - private String getHeadersAsString(HttpServletRequest request) { - return Collections.list(request.getHeaderNames()).stream() - .filter(headerName -> !headerName.equalsIgnoreCase("Authorization")) - .filter(headerName -> !headerName.equalsIgnoreCase("refresh-token")) - .map(headerName -> headerName + "=" + request.getHeader(headerName)) - .collect(Collectors.joining(", ")); - } - - protected String getBody(byte[] info) { - return new String(info, StandardCharsets.UTF_8).replace("\n", "").replace("\r", ""); - } + private final ContentCachingRequestWrapper request; + private static final List ALLOWED_HEADERS = List.of( + "x-forwarded-for", "x-real-ip", "user-agent", "content-type", "accept", "host" + ); + + public ApiLogRequestLogGenerator(ContentCachingRequestWrapper request) { + this.request = request; + } + + @Override + public String generateLog() { + StringBuilder logBuilder = new StringBuilder(); + + logBuilder.append("Request : ").append(" "); + // logBuilder.append(getBaseLogInfo()); + logBuilder.append("[method] ").append(request.getMethod()).append(" "); + logBuilder.append("[uri] ").append(getQuery()).append("\n"); + logBuilder.append("[headers] ").append(getHeadersAsString(request)).append("\n"); + logBuilder.append("[requestBody] ").append(getBody(request.getContentAsByteArray())).append(" "); + + return logBuilder.toString(); + } + + private String getQuery() { + String uri = request.getRequestURI(); + String queryString = request.getQueryString(); + + if (queryString != null) { + uri += "?" + queryString; + } + return uri; + } + + private String getHeadersAsString(HttpServletRequest request) { + return ALLOWED_HEADERS.stream() + .map(header -> { + String value = request.getHeader(header); + return value != null ? header + "=" + value : null; + }) + .filter(Objects::nonNull) + .collect(Collectors.joining(", ")); + } + + protected String getBody(byte[] info) { + return new String(info, StandardCharsets.UTF_8).replace("\n", "").replace("\r", ""); + } } diff --git a/src/main/java/umc/th/juinjang/monitoring/ApiLogResponseGenerator.java b/src/main/java/umc/th/juinjang/monitoring/ApiLogResponseGenerator.java index 8a1856b9..91e23d60 100644 --- a/src/main/java/umc/th/juinjang/monitoring/ApiLogResponseGenerator.java +++ b/src/main/java/umc/th/juinjang/monitoring/ApiLogResponseGenerator.java @@ -2,42 +2,44 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; + import java.nio.charset.StandardCharsets; + import org.springframework.web.util.ContentCachingResponseWrapper; public class ApiLogResponseGenerator extends ApiLogGenerator { - private final ContentCachingResponseWrapper response; - - public ApiLogResponseGenerator(ContentCachingResponseWrapper response) { - this.response = response; - } - - @Override - public String generateLog() { - StringBuilder logBuilder = new StringBuilder(); - - logBuilder.append("Response : ").append(" "); - logBuilder.append(getBaseLogInfo()); - logBuilder.append("[status] ").append(response.getStatus()).append(" "); - logBuilder.append("[responseBody] ").append(getBody(response.getContentAsByteArray())).append(" "); - - return logBuilder.toString(); - } - - protected String getBody(byte[] info) { - String responseBody = ""; - try { - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode rootNode = objectMapper.readTree(new String(info, StandardCharsets.UTF_8)); - responseBody = replaceToken(rootNode.path("message").asText("N/A")); - } catch (Exception e) { - logger.error("responseBody 추출 오류"); - } - return responseBody; - } - - private String replaceToken(String responseBody) { - return responseBody.replaceAll("\"accessToken\":\"[^\"]*\"", "\"accessToken\":\"[TOKEN]\"") - .replaceAll("\"refreshToken\":\"[^\"]*\"", "\"refreshToken\":\"[TOKEN]\""); - } + private final ContentCachingResponseWrapper response; + + public ApiLogResponseGenerator(ContentCachingResponseWrapper response) { + this.response = response; + } + + @Override + public String generateLog() { + StringBuilder logBuilder = new StringBuilder(); + + logBuilder.append("Response : ").append(" "); + // logBuilder.append(getBaseLogInfo()); + logBuilder.append("[status] ").append(response.getStatus()).append(" "); + logBuilder.append("[responseBody] ").append(getBody(response.getContentAsByteArray())).append(" "); + + return logBuilder.toString(); + } + + protected String getBody(byte[] info) { + String responseBody = ""; + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(new String(info, StandardCharsets.UTF_8)); + responseBody = replaceToken(rootNode.path("message").asText("N/A")); + } catch (Exception e) { + logger.error("responseBody 추출 오류"); + } + return responseBody; + } + + private String replaceToken(String responseBody) { + return responseBody.replaceAll("\"accessToken\":\"[^\"]*\"", "\"accessToken\":\"[TOKEN]\"") + .replaceAll("\"refreshToken\":\"[^\"]*\"", "\"refreshToken\":\"[TOKEN]\""); + } } diff --git a/src/main/java/umc/th/juinjang/monitoring/ApiLoggerFilter.java b/src/main/java/umc/th/juinjang/monitoring/ApiLoggerFilter.java index 3e2d6062..cc8a2145 100644 --- a/src/main/java/umc/th/juinjang/monitoring/ApiLoggerFilter.java +++ b/src/main/java/umc/th/juinjang/monitoring/ApiLoggerFilter.java @@ -1,16 +1,17 @@ package umc.th.juinjang.monitoring; -import static umc.th.juinjang.utils.LoggerProvider.getLogger; -import static umc.th.juinjang.utils.LoggerProvider.registerRequestId; +import static umc.th.juinjang.common.LoggerProvider.getLogger; +import static umc.th.juinjang.common.LoggerProvider.registerRequestId; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; + import java.util.List; import java.util.UUID; + import lombok.extern.slf4j.Slf4j; + import org.slf4j.Logger; import org.slf4j.MDC; import org.springframework.util.AntPathMatcher; @@ -20,40 +21,41 @@ @Slf4j public class ApiLoggerFilter extends OncePerRequestFilter { - private static final Logger logger = getLogger(ApiLoggerFilter.class); - private final ApiLoggerFactory apiLoggerFactory; - private final List EXCLUDED_URLS; - - public ApiLoggerFilter(List EXCLUDED_URLS) { - this.EXCLUDED_URLS = EXCLUDED_URLS; - this.apiLoggerFactory = new ApiLoggerFactory(); - } - - @Override - protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain chain) { - ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(servletRequest); - ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(servletResponse); - registerRequestId(UUID.randomUUID().toString()); - - try { - if (shouldNotFilter(request)) { - chain.doFilter(request, response); - return; - } - chain.doFilter(request, response); - apiLoggerFactory.createRequestLogger(request); - apiLoggerFactory.createResponseLogger(response); - response.copyBodyToResponse(); - } catch (Exception e) { - logger.error("APILogger 필터 오류"); - } finally { - MDC.clear(); - } - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - AntPathMatcher pathMatcher = new AntPathMatcher(); - return EXCLUDED_URLS.stream().anyMatch(pattern -> pathMatcher.match(pattern, request.getRequestURI())); - } + private static final Logger logger = getLogger(ApiLoggerFilter.class); + private final ApiLoggerFactory apiLoggerFactory; + private final List EXCLUDED_URLS; + + public ApiLoggerFilter(List EXCLUDED_URLS) { + this.EXCLUDED_URLS = EXCLUDED_URLS; + this.apiLoggerFactory = new ApiLoggerFactory(); + } + + @Override + protected void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, + FilterChain chain) { + ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(servletRequest); + ContentCachingResponseWrapper response = new ContentCachingResponseWrapper(servletResponse); + registerRequestId(UUID.randomUUID().toString()); + + try { + if (shouldNotFilter(request)) { + chain.doFilter(request, response); + return; + } + chain.doFilter(request, response); + apiLoggerFactory.createRequestLogger(request); + apiLoggerFactory.createResponseLogger(response); + response.copyBodyToResponse(); + } catch (Exception e) { + logger.error("APILogger 필터 오류, message : {} ", e.getMessage()); + } finally { + MDC.clear(); + } + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + AntPathMatcher pathMatcher = new AntPathMatcher(); + return EXCLUDED_URLS.stream().anyMatch(pattern -> pathMatcher.match(pattern, request.getRequestURI())); + } } diff --git a/src/main/java/umc/th/juinjang/repository/image/ImageRepository.java b/src/main/java/umc/th/juinjang/repository/image/ImageRepository.java deleted file mode 100644 index 7896097b..00000000 --- a/src/main/java/umc/th/juinjang/repository/image/ImageRepository.java +++ /dev/null @@ -1,23 +0,0 @@ -package umc.th.juinjang.repository.image; - -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Record; - -public interface ImageRepository extends JpaRepository { - - List findImagesByLimjangId(Limjang limjang); - - @Transactional - @Modifying - @Query(value = "DELETE FROM image i WHERE i.limjang_id = :limjangId", nativeQuery = true) - void deleteByLimjangId(@Param("limjangId") Long limjangId); - -} diff --git a/src/main/java/umc/th/juinjang/repository/limjang/LimjangQueryDslRepository.java b/src/main/java/umc/th/juinjang/repository/limjang/LimjangQueryDslRepository.java deleted file mode 100644 index c5250b63..00000000 --- a/src/main/java/umc/th/juinjang/repository/limjang/LimjangQueryDslRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package umc.th.juinjang.repository.limjang; - -import java.util.List; -import umc.th.juinjang.model.dto.limjang.enums.LimjangSortOptions; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; - -public interface LimjangQueryDslRepository { - - List searchLimjangsWhereDeletedIsFalse(Member member, String keyword); - - List findAllByMemberAndDeletedIsFalseWithReportAndLimjangPriceOrderByUpdateAtLimit5(Member member); - - List findAllByMemberAndDeletedIsFalseOrderByParam(Member member, LimjangSortOptions sort); -} diff --git a/src/main/java/umc/th/juinjang/repository/limjang/LimjangQueryDslRepositoryImpl.java b/src/main/java/umc/th/juinjang/repository/limjang/LimjangQueryDslRepositoryImpl.java deleted file mode 100644 index fcb3615e..00000000 --- a/src/main/java/umc/th/juinjang/repository/limjang/LimjangQueryDslRepositoryImpl.java +++ /dev/null @@ -1,105 +0,0 @@ -package umc.th.juinjang.repository.limjang; - -import static com.querydsl.core.types.Order.DESC; -import static umc.th.juinjang.model.entity.QImage.image; -import static umc.th.juinjang.model.entity.QLimjang.limjang; -import static umc.th.juinjang.model.entity.QLimjangPrice.limjangPrice; -import static umc.th.juinjang.model.entity.QReport.report; -import static umc.th.juinjang.model.entity.QScrap.scrap; -import static com.querydsl.core.group.GroupBy.list; - -import com.querydsl.core.types.Order; -import com.querydsl.core.types.OrderSpecifier; -import com.querydsl.core.types.dsl.BooleanExpression; -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.core.types.dsl.StringExpression; -import com.querydsl.jpa.JPQLTemplates; -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import java.util.ArrayList; -import java.util.List; -import org.springframework.data.domain.Pageable; -import umc.th.juinjang.model.dto.limjang.enums.LimjangSortOptions; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.QReport; - -public class LimjangQueryDslRepositoryImpl implements LimjangQueryDslRepository { - private final JPAQueryFactory queryFactory; - - public LimjangQueryDslRepositoryImpl(EntityManager em) { - this.queryFactory = new JPAQueryFactory(JPQLTemplates.DEFAULT, em); - } - - @Override - public List searchLimjangsWhereDeletedIsFalse(Member member, String keyword) { - String rKeyword = removeKeywordBlank(keyword); - return queryFactory - .selectFrom(limjang) - .leftJoin(limjang.report, report).fetchJoin() - .join(limjang.limjangPrice, limjangPrice).fetchJoin() - .leftJoin(limjang.imageList, image).fetchJoin() - .where(limjang.deleted.isFalse()) - .where(limjang.memberId.eq(member), - keywordOf( - removeBlank(limjang.nickname).containsIgnoreCase(rKeyword), - removeBlank(limjang.address).containsIgnoreCase(rKeyword), - removeBlank(limjang.addressDetail).containsIgnoreCase(rKeyword) - )) - .fetch(); - } - - private String removeKeywordBlank(String keyword) { - return keyword.replaceAll(" ", ""); - } - - public List findAllByMemberAndDeletedIsFalseOrderByParam(Member member, LimjangSortOptions sort) { - return queryFactory - .selectFrom(limjang) - .join(limjang.limjangPrice, limjangPrice).fetchJoin() - .leftJoin(limjang.report, report).fetchJoin() - .leftJoin(limjang.imageList, image).fetchJoin() - .where(limjang.memberId.eq(member)) - .where(limjang.deleted.isFalse()) - .orderBy(getOrderByLimjangSortOptions(sort)) - .fetch(); - } - - private OrderSpecifier[] getOrderByLimjangSortOptions(LimjangSortOptions sort) { - List orders = new ArrayList<>(); - switch (sort) { - case UPDATED -> orders.add(new OrderSpecifier(DESC, limjang.updatedAt)); - case STAR -> { - orders.add(new OrderSpecifier<>(DESC, report.totalRate.coalesce(0f), OrderSpecifier.NullHandling.NullsLast)); - orders.add(new OrderSpecifier<>(DESC, limjang.createdAt)); - } - case CREATED -> orders.add(new OrderSpecifier<>(DESC, limjang.createdAt)); - } - return orders.toArray(new OrderSpecifier[orders.size()]); - } - - private BooleanExpression keywordOf(BooleanExpression... conditions) { - BooleanExpression result = null; - for (BooleanExpression condition : conditions) { - result = result == null ? condition : result.or(condition); - } - return result; - } - - private StringExpression removeBlank(StringExpression origin) { - return Expressions.stringTemplate("function('replace', {0}, ' ', '')", origin); - } - - @Override - public List findAllByMemberAndDeletedIsFalseWithReportAndLimjangPriceOrderByUpdateAtLimit5(Member member) { - return queryFactory - .selectFrom(limjang) - .leftJoin(limjang.report, report).fetchJoin() - .join(limjang.limjangPrice, limjangPrice).fetchJoin() - .where(limjang.memberId.eq(member)) - .where(limjang.deleted.isFalse()) - .orderBy(limjang.updatedAt.desc()) - .limit(5) - .fetch(); - } -} diff --git a/src/main/java/umc/th/juinjang/repository/limjang/LimjangRepository.java b/src/main/java/umc/th/juinjang/repository/limjang/LimjangRepository.java deleted file mode 100644 index 2e213f4e..00000000 --- a/src/main/java/umc/th/juinjang/repository/limjang/LimjangRepository.java +++ /dev/null @@ -1,53 +0,0 @@ -package umc.th.juinjang.repository.limjang; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; - -@Repository -public interface LimjangRepository extends JpaRepository, LimjangQueryDslRepository { - - @Query(value = "SELECT * FROM limjang l WHERE l.member_id = :memberId", nativeQuery = true) - List findLimjangByMemberIdIgnoreDeleted(@Param("memberId") Long memberId); - - List findAllByLimjangIdInAndMemberIdAndDeletedIsFalse(List id, Member member); - - @Modifying - @Query("UPDATE Limjang l SET l.deleted = true WHERE l.limjangId in :ids") - void softDeleteByIds(@Param("ids") List ids); - - @Modifying - @Query(value = "DELETE FROM limjang l WHERE l.deleted = true AND l.updated_at < :dateTime", nativeQuery = true) - void hardDelete(@Param("dateTime") LocalDateTime dateTime); - - Optional findLimjangByLimjangIdAndMemberIdAndDeletedIsFalse(Long limjangId, Member member); - - @Modifying - @Transactional - @Query("UPDATE Limjang l SET l.recordCount = l.recordCount + 1 WHERE l.limjangId = :limjangId") - void incrementRecordCount(@Param("limjangId") Long limjangId); - - @Modifying - @Transactional - @Query("UPDATE Limjang l SET l.memo = :memo WHERE l.limjangId = :limjangId") - void updateMemo(@Param("limjangId") Long limjangId, @Param("memo") String memo); - - @Transactional - @Modifying - @Query(value = "DELETE FROM limjang l WHERE l.member_id = :memberId", nativeQuery = true) - void deleteAllByMemberId(@Param("memberId") Long memberId); - - @Query("SELECT l FROM Limjang l join fetch l.limjangPrice WHERE l.limjangId = :id AND l.memberId = :member AND l.deleted = false") - Optional findByLimjangIdAndMemberIdWithLimjangPriceAndDeletedIsFalse(@Param("id") Long id, @Param("member") Member member); - - @Query("SELECT l FROM Limjang l join fetch l.limjangPrice left join fetch l.report WHERE l.limjangId = :id AND l.memberId = :member AND l.deleted = false") - Optional findByLimjangIdAndDeletedIsFalse(@Param("id") Long id, @Param("member") Member member); -} \ No newline at end of file diff --git a/src/main/java/umc/th/juinjang/repository/limjang/MemberRepository.java b/src/main/java/umc/th/juinjang/repository/limjang/MemberRepository.java deleted file mode 100644 index 8e584ca7..00000000 --- a/src/main/java/umc/th/juinjang/repository/limjang/MemberRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package umc.th.juinjang.repository.limjang; - -import org.springframework.data.jpa.repository.JpaRepository; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; - -import java.util.Optional; - -public interface MemberRepository extends JpaRepository { - - Optional findByEmail(String email); - - Optional findByRefreshToken(String refreshToken); - - Optional findByKakaoTargetId(Long targetId); - - Member findByNickname(String nickname); - - Optional findByAppleSub(String sub); -} diff --git a/src/main/java/umc/th/juinjang/service/auth/OAuthService.java b/src/main/java/umc/th/juinjang/service/auth/OAuthService.java deleted file mode 100644 index 6641f694..00000000 --- a/src/main/java/umc/th/juinjang/service/auth/OAuthService.java +++ /dev/null @@ -1,587 +0,0 @@ -package umc.th.juinjang.service.auth; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.exception.handler.MemberHandler; -import umc.th.juinjang.controller.KakaoUnlinkClient; -import umc.th.juinjang.event.publisher.MemberEventPublisher; -import umc.th.juinjang.model.dto.auth.LoginResponseDto; -import umc.th.juinjang.model.dto.auth.LoginResponseVersion2Dto; -import umc.th.juinjang.model.dto.auth.TokenDto; -import umc.th.juinjang.model.dto.auth.apple.*; -import umc.th.juinjang.model.dto.auth.kakao.KakaoLoginRequestDto; -import umc.th.juinjang.model.dto.auth.kakao.KakaoSignUpRequestDto; -import umc.th.juinjang.model.dto.auth.kakao.KakaoSignUpRequestVersion2Dto; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.Record; -import umc.th.juinjang.model.entity.enums.MemberProvider; -import umc.th.juinjang.repository.checklist.ChecklistAnswerRepository; -import umc.th.juinjang.repository.checklist.ReportRepository; -import umc.th.juinjang.repository.image.ImageRepository; -import umc.th.juinjang.repository.limjang.LimjangPriceRepository; -import umc.th.juinjang.repository.limjang.LimjangRepository; -import umc.th.juinjang.repository.limjang.MemberRepository; -import umc.th.juinjang.repository.limjang.ScrapRepository; -import umc.th.juinjang.repository.record.RecordRepository; -import umc.th.juinjang.service.external.S3Service; - -import java.time.LocalDateTime; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import static umc.th.juinjang.apiPayload.code.status.ErrorStatus.*; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class OAuthService { - - private final MemberRepository memberRepository; - private final JwtService jwtService; - private final AppleClientSecretGenerator appleClientSecretGenerator; - private final AppleOAuthProvider appleOAuthProvider; - private final ScrapRepository scrapRepository; - private final LimjangRepository limjangRepository; - private final ChecklistAnswerRepository checklistAnswerRepository; - private final RecordRepository recordRepository; - private final ImageRepository imageRepository; - private final ReportRepository reportRepository; - private final S3Service s3Service; - private final LimjangPriceRepository limjangPriceRepository; - private final MemberEventPublisher memberEventPublisher; - - @Autowired - private KakaoUnlinkClient kakaoUnlinkClient; - - @Value("${security.oauth2.client.registration.kakao.admin-key}") - private String kakaoAdminKey; - - // 카카오 로그인 (회원가입된 경우) - // 프론트에서 받은 사용자 정보로 accessToken, refreshToken 발급 - @Transactional - public LoginResponseDto kakaoLogin(Long targetId, KakaoLoginRequestDto kakaoReqDto) { - String email = kakaoReqDto.getEmail(); - log.info(kakaoReqDto.getEmail()); - - if(email == null) - throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); - - Optional getMemberByEmail = memberRepository.findByEmail(email); - Optional getMemberByTargetId = memberRepository.findByKakaoTargetId(targetId); - Member member = null; - - - if(getMemberByEmail.isPresent() && getMemberByTargetId.isEmpty()){ - if(!getMemberByEmail.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); - } else { // 잘못된 target_id가 들어왔을때(db에 없는) - throw new MemberHandler(UNCORRECTED_TARGET_ID); - } - } else if(getMemberByEmail.isPresent() && getMemberByTargetId.isPresent()){ // 이미 회원가입한 회원인 경우 - if(!getMemberByEmail.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); - } else if(getMemberByEmail.get().getMemberId() != getMemberByTargetId.get().getMemberId()) { - throw new MemberHandler(FAILED_TO_LOGIN); - } - member = getMemberByEmail.get(); - } else if(getMemberByEmail.isEmpty() && getMemberByTargetId.isEmpty()){ // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 - throw new MemberHandler(MEMBER_NOT_FOUND); - } - - if(member == null) { - throw new MemberHandler(FAILED_TO_LOGIN); - } - - // accessToken, refreshToken 발급 후 반환 - return createToken(member); - } - - // 카카오 로그인 (회원가입 해야하는 경우) - @Transactional - public LoginResponseDto kakaoSignUp (Long targetId, KakaoSignUpRequestDto kakaoSignUpReqDto) { - String email = kakaoSignUpReqDto.getEmail(); - log.info(kakaoSignUpReqDto.getEmail()); - - if(email == null) - throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); - - Optional getMember = memberRepository.findByEmail(email); - Optional getTargetId = memberRepository.findByKakaoTargetId(targetId); - - Member member = null; - - if(getMember.isPresent() && getTargetId.isEmpty() && getMember.get().getProvider().equals(MemberProvider.APPLE)) { - throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); - } else if(getMember.isPresent() && getTargetId.isPresent()) { -// if(!getMember.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 -// throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); -// } else - if((getTargetId.get().getMemberId() != getMember.get().getMemberId())) { - throw new MemberHandler(FAILED_TO_LOGIN); - } else if(getMember.get().getProvider().equals(MemberProvider.KAKAO)) { - throw new MemberHandler(ALREADY_MEMBER); - } - } else if (getMember.isPresent() || getTargetId.isPresent()) { // 둘 중 하나만 존재할 때 실행될 코드 - throw new MemberHandler(FAILED_TO_LOGIN); - } else if (!getMember.isPresent() && !getTargetId.isPresent()) { // 두 값 모두 존재하지 않을 때 실행될 코드, 아직 회원가입 하지 않은 회원인 경우 - member = memberRepository.save( - Member.builder() - .email(email) - .provider(MemberProvider.KAKAO) - .kakaoTargetId(targetId) - .nickname(kakaoSignUpReqDto.getNickname()) - .refreshToken("") - .refreshTokenExpiresAt(LocalDateTime.now()) - .build() - ); - } - - if(member == null) { - throw new MemberHandler(FAILED_TO_SIGNUP); - } - - // accessToken, refreshToken 발급 후 반환 - publishDiscordAlert(member); - return createToken(member); - } - - private void publishDiscordAlert(Member member) { - memberEventPublisher.publishSignUpEvent(member); - } - - // accessToken, refreshToken 발급 - @Transactional - public LoginResponseDto createToken(Member member) { - String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); - String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); - - // DB에 refreshToken 저장 - member.updateRefreshToken(newRefreshToken); - memberRepository.save(member); - - return new LoginResponseDto(newAccessToken, newRefreshToken, member.getEmail()); - } - - //ver2 - // accessToken, refreshToken 발급 - @Transactional - public LoginResponseVersion2Dto createTokenVersion2(Member member) { - String newAccessToken = jwtService.encodeJwtToken(new TokenDto(member.getMemberId())); - String newRefreshToken = jwtService.encodeJwtRefreshToken(member.getMemberId()); - - // DB에 refreshToken 저장 - member.updateRefreshToken(newRefreshToken); - - return new LoginResponseVersion2Dto(newAccessToken, newRefreshToken, member.getEmail(), member.getAgreeVersion()); - } - - - // refreshToken으로 accessToken 발급하기 - @Transactional - public LoginResponseDto regenerateAccessToken(String accessToken, String refreshToken) { - if(jwtService.validateTokenBoolean(accessToken)) // access token 유효성 검사 - throw new ExceptionHandler(ACCESS_TOKEN_AUTHORIZED); - - if(!jwtService.validateTokenBoolean(refreshToken)) // refresh token 유효성 검사 - throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); - - Long memberId = jwtService.getMemberIdFromJwtToken(refreshToken); - - Optional getMember = memberRepository.findById(memberId); - if(getMember.isEmpty()) - throw new MemberHandler(MEMBER_NOT_FOUND); - - Member member = getMember.get(); - if(!refreshToken.equals(member.getRefreshToken())) - throw new ExceptionHandler(REFRESH_TOKEN_UNAUTHORIZED); - - String newRefreshToken = jwtService.encodeJwtRefreshToken(memberId); - String newAccessToken = jwtService.encodeJwtToken(new TokenDto(memberId)); - - member.updateRefreshToken(newRefreshToken); - memberRepository.save(member); - - return new LoginResponseDto(newAccessToken, newRefreshToken, member.getNickname()); - } - - // 로그아웃 - @Transactional - public String logout(String refreshToken) { - Optional getMember = memberRepository.findByRefreshToken(refreshToken); - if(getMember.isEmpty()) - throw new MemberHandler(MEMBER_NOT_FOUND); - - Member member = getMember.get(); - if(member.getRefreshToken().equals("")) - throw new MemberHandler(ALREADY_LOGOUT); - - member.refreshTokenExpires(); - memberRepository.save(member); - - return "로그아웃 성공"; - } - - // 애플 로그인 (회원가입된 경우) - @Transactional - public LoginResponseDto appleLogin(AppleLoginRequestDto appleLoginRequest) { - // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find - // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token - // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) - // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) - // 4. db에 email, sub값 둘 다 없으면 회원가입 - // 탈퇴 처리는 추후에 - log.info("Oauth service 까지 들어옴"+ appleLoginRequest.getIdentityToken()); - AppleInfo appleInfo = jwtService.getAppleAccountId(appleLoginRequest.getIdentityToken().replaceAll("\\n", "")); - String email = appleInfo.getEmail(); - String sub = appleInfo.getSub(); - - if(email == null || sub == null) - throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); - - - Optional findSub = memberRepository.findByAppleSub(sub); - Optional findEmail = memberRepository.findByEmail(email); - - Member member = null; - if(findSub.isEmpty() && findEmail.isPresent() && findEmail.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Apple 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - } else if(findSub.isPresent() && findEmail.isPresent()) { // 재로그인 - if(!findEmail.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입했지만 apple이 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - } else if(findSub.get().getMemberId() != findEmail.get().getMemberId()) { - throw new MemberHandler(FAILED_TO_LOGIN); - } - member = findEmail.get(); - } else if(!findSub.isPresent() && !findEmail.isPresent()) { // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 - throw new MemberHandler(MEMBER_NOT_FOUND); - } - - // accessToken, refreshToken 발급 - if(member == null) - throw new MemberHandler(MEMBER_NOT_FOUND); - return createToken(member); - } - - // 애플 로그인 (회원가입 해야하는 경우) - @Transactional - public LoginResponseDto appleSignUp(AppleSignUpRequestDto appleSignUpRequestDto) { - // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find - // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token - // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) - // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) - // 4. db에 email, sub값 둘 다 없으면 회원가입 - // 탈퇴 처리는 추후에 - - AppleInfo appleInfo = jwtService.getAppleAccountId(appleSignUpRequestDto.getIdentityToken()); - String email = appleInfo.getEmail(); - String sub = appleInfo.getSub(); - - if(email == null || sub == null) - throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); - - - Optional findSub = memberRepository.findByAppleSub(sub); - Optional findEmail = memberRepository.findByEmail(email); - - Member member = null; - if(findSub.isPresent() && findEmail.isPresent() && (findSub.get().getMemberId() == findEmail.get().getMemberId()) - && findSub.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입한 회원인 경우 -> 에러 발생 - throw new MemberHandler(ALREADY_MEMBER); - } else if(!findSub.isPresent() && findEmail.isPresent() && findEmail.get().getProvider().equals(MemberProvider.KAKAO)){ - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - }else if(!findSub.isPresent() && !findEmail.isPresent()) { - member = memberRepository.save( - Member.builder() - .email(email) - .nickname(appleSignUpRequestDto.getNickname()) - .provider(MemberProvider.APPLE) - .appleSub(sub) - .refreshToken("") - .refreshTokenExpiresAt(LocalDateTime.now()) - .build() - ); - System.out.println("member id : " + member.getMemberId()); - System.out.println("member email : " + member.getEmail()); - } - - // accessToken, refreshToken 발급 - if(member == null) - throw new MemberHandler(FAILED_TO_LOGIN); - - publishDiscordAlert(member); - return createToken(member); - } - - // 카카오 탈퇴 (카카오 연결 끊기) - @Transactional - public boolean kakaoWithdraw(Member member, Long kakaoTargetId) { - ResponseEntity response = kakaoUnlinkClient.unlinkUser("KakaoAK " + kakaoAdminKey, "user_id", kakaoTargetId); - - if (response.getStatusCode().is2xxSuccessful()) { // 성공 처리 로직 - log.info("카카오 탈퇴 성공"); - log.info("member id :: " + member.getMemberId()); - - deleteMemberData(member); - - return true; - } else { // 실패 처리 로직 - return false; - } - } - - @Transactional - public void appleWithdraw(Member member, String code) { - - if(member.getProvider() != MemberProvider.APPLE){ - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - } - try { - String clientSecret = appleClientSecretGenerator.generateClientSecret(); - String refreshToken = appleOAuthProvider.getAppleRefreshToken(code, clientSecret); - appleOAuthProvider.requestRevoke(refreshToken, clientSecret); - } catch (Exception e) { - throw new MemberHandler(FAILED_TO_LOAD_PRIVATE_KEY); - } - log.info("애플 탈퇴 성공"); - log.info("member id :: " + member.getMemberId()); - - deleteMemberData(member); - } - @Transactional - public void deleteAllByLimjangId(Limjang limjang) { - scrapRepository.deleteByLimjangId(limjang.getLimjangId()); - checklistAnswerRepository.deleteByLimjangId(limjang.getLimjangId()); - limjangPriceRepository.deleteAllByLimjang(limjang); - List imageList = limjang.getImageList() - .stream() - .map(Image::getImageUrl) - .collect(Collectors.toList()); - List recordList = limjang.getRecordList() - .stream() - .map(Record::getRecordUrl) - .collect(Collectors.toList()); - - - deleteFromS3(imageList); - deleteFromS3(recordList); - - imageRepository.deleteByLimjangId(limjang.getLimjangId()); - recordRepository.deleteByLimjangId(limjang.getLimjangId()); - reportRepository.deleteByLimjangId(limjang.getLimjangId()); - - } - - - @Transactional - public void deleteMemberData(Member member) { - List limjangList = limjangRepository.findLimjangByMemberIdIgnoreDeleted(member.getMemberId()); - - for (Limjang limjang : limjangList) { - deleteAllByLimjangId(limjang); - } - - - if (member.getImageUrl() != null) { - deleteFromS3(Collections.singletonList(member.getImageUrl())); - } - - limjangRepository.deleteAllByMemberId(member.getMemberId()); - memberRepository.deleteById(member.getMemberId()); - } - - @Transactional - public void deleteFromS3(List urlList){ - for (String url : urlList) { - s3Service.deleteFile(url); - } - } - - // V2 - // 카카오 - @Transactional - public LoginResponseVersion2Dto kakaoLoginVersion2(Long targetId, KakaoLoginRequestDto kakaoReqDto) { - String email = kakaoReqDto.getEmail(); - log.info(kakaoReqDto.getEmail()); - - if(email == null) - throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); - - Optional getMemberByEmail = memberRepository.findByEmail(email); - Optional getMemberByTargetId = memberRepository.findByKakaoTargetId(targetId); - Member member = null; - - - if(getMemberByEmail.isPresent() && getMemberByTargetId.isEmpty()){ - if(!getMemberByEmail.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); - } else { // 잘못된 target_id가 들어왔을때(db에 없는) - throw new MemberHandler(UNCORRECTED_TARGET_ID); - } - } else if(getMemberByEmail.isPresent() && getMemberByTargetId.isPresent()){ // 이미 회원가입한 회원인 경우 - if(!getMemberByEmail.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); - } else if(getMemberByEmail.get().getMemberId() != getMemberByTargetId.get().getMemberId()) { - throw new MemberHandler(FAILED_TO_LOGIN); - } - member = getMemberByEmail.get(); - } else if(getMemberByEmail.isEmpty() && getMemberByTargetId.isEmpty()){ // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 - throw new MemberHandler(MEMBER_NOT_FOUND); - } - - if(member == null) { - throw new MemberHandler(FAILED_TO_LOGIN); - } - - // accessToken, refreshToken 발급 후 반환 - return createTokenVersion2(member); - } - - // 카카오 로그인 (회원가입 해야하는 경우) - @Transactional - public LoginResponseVersion2Dto kakaoSignUpVersion2(Long targetId, KakaoSignUpRequestVersion2Dto kakaoSignUpReqDto) { - String email = kakaoSignUpReqDto.getEmail(); - log.info(kakaoSignUpReqDto.getEmail()); - - if(email == null) - throw new MemberHandler(MEMBER_EMAIL_NOT_FOUND); - - Optional getMember = memberRepository.findByEmail(email); - Optional getTargetId = memberRepository.findByKakaoTargetId(targetId); - - Member member = null; - - if(getMember.isPresent() && getTargetId.isEmpty() && getMember.get().getProvider().equals(MemberProvider.APPLE)) { - throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); - } else if(getMember.isPresent() && getTargetId.isPresent()) { -// if(!getMember.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Kakao가 아닌 다른 소셜 로그인 사용 -// throw new MemberHandler(MEMBER_NOT_FOUND_IN_KAKAO); -// } else - if((getTargetId.get().getMemberId() != getMember.get().getMemberId())) { - throw new MemberHandler(FAILED_TO_LOGIN); - } else if(getMember.get().getProvider().equals(MemberProvider.KAKAO)) { - throw new MemberHandler(ALREADY_MEMBER); - } - } else if (getMember.isPresent() || getTargetId.isPresent()) { // 둘 중 하나만 존재할 때 실행될 코드 - throw new MemberHandler(FAILED_TO_LOGIN); - } else if (!getMember.isPresent() && !getTargetId.isPresent()) { // 두 값 모두 존재하지 않을 때 실행될 코드, 아직 회원가입 하지 않은 회원인 경우 - member = memberRepository.save( - Member.builder() - .email(email) - .provider(MemberProvider.KAKAO) - .kakaoTargetId(targetId) - .nickname(kakaoSignUpReqDto.getNickname()) - .refreshToken("") - .refreshTokenExpiresAt(LocalDateTime.now()) - .agreeVersion(kakaoSignUpReqDto.getAgreeVersion()) - .build() - ); - } - - if(member == null) { - throw new MemberHandler(FAILED_TO_SIGNUP); - } - - // accessToken, refreshToken 발급 후 반환 - publishDiscordAlert(member); - return createTokenVersion2(member); - } - - // 애플 - public LoginResponseVersion2Dto appleLoginVersion2(AppleLoginRequestDto appleLoginRequest) { - // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find - // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token - // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) - // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) - // 4. db에 email, sub값 둘 다 없으면 회원가입 - - AppleInfo appleInfo = jwtService.getAppleAccountId(appleLoginRequest.getIdentityToken().replaceAll("\\n", "")); - String email = appleInfo.getEmail(); - String sub = appleInfo.getSub(); - - if(email == null || sub == null) - throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); - - - Optional findSub = memberRepository.findByAppleSub(sub); - Optional findEmail = memberRepository.findByEmail(email); - - Member member = null; - if(findSub.isEmpty() && findEmail.isPresent() && findEmail.get().getProvider().equals(MemberProvider.KAKAO)) { // 이미 회원가입했지만 Apple 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - } else if(findSub.isPresent() && findEmail.isPresent()) { // 재로그인 - if(!findEmail.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입했지만 apple이 아닌 다른 소셜 로그인 사용 - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - } else if(findSub.get().getMemberId() != findEmail.get().getMemberId()) { - throw new MemberHandler(FAILED_TO_LOGIN); - } - member = findEmail.get(); - } else if(!findSub.isPresent() && !findEmail.isPresent()) { // 회원가입이 안되어있는 경우 -> 에러 발생. 회원가입 해야 함 - throw new MemberHandler(MEMBER_NOT_FOUND); - } - - // accessToken, refreshToken 발급 - if(member == null) - throw new MemberHandler(MEMBER_NOT_FOUND); - return createTokenVersion2(member); - - } - - @Transactional - public LoginResponseVersion2Dto appleSignUpVersion2(AppleSignUpRequestVersion2Dto appleSignUpRequestDto) { - // email, sub값 추출 후 db에서 해당 email값 그리고 sub값을 가진 유저가 있는지 find - // 1. 추출한 email, sub 값이 null이면 -> 잘못된 apple token - // 2. db에서 각각 find한 회원 id가 다르면 에러 (올바르지 않은 정보) - // 3. db에 email, sub값 둘 다 있으면 재로그인 (혹시 provider가 다르다면 에러) - // 4. db에 email, sub값 둘 다 없으면 회원가입 - // 탈퇴 처리는 추후에 - - AppleInfo appleInfo = jwtService.getAppleAccountId(appleSignUpRequestDto.getIdentityToken()); - String email = appleInfo.getEmail(); - String sub = appleInfo.getSub(); - - if(email == null || sub == null) - throw new ExceptionHandler(INVALID_APPLE_ID_TOKEN); - - - Optional findSub = memberRepository.findByAppleSub(sub); - Optional findEmail = memberRepository.findByEmail(email); - - Member member = null; - if(findSub.isPresent() && findEmail.isPresent() && (findSub.get().getMemberId() == findEmail.get().getMemberId()) - && findSub.get().getProvider().equals(MemberProvider.APPLE)) { // 이미 회원가입한 회원인 경우 -> 에러 발생 - throw new MemberHandler(ALREADY_MEMBER); - } else if(!findSub.isPresent() && findEmail.isPresent() && findEmail.get().getProvider().equals(MemberProvider.KAKAO)){ - throw new MemberHandler(MEMBER_NOT_FOUND_IN_APPLE); - }else if(!findSub.isPresent() && !findEmail.isPresent()) { - member = memberRepository.save( - Member.builder() - .email(email) - .nickname(appleSignUpRequestDto.getNickname()) - .provider(MemberProvider.APPLE) - .appleSub(sub) - .refreshToken("") - .refreshTokenExpiresAt(LocalDateTime.now()) - .agreeVersion(appleSignUpRequestDto.getAgreeVersion()) - .build() - ); - } - - // accessToken, refreshToken 발급 - if(member == null) - throw new MemberHandler(FAILED_TO_LOGIN); - - publishDiscordAlert(member); - return createTokenVersion2(member); - } -} diff --git a/src/main/java/umc/th/juinjang/service/auth/UserDetailServiceImpl.java b/src/main/java/umc/th/juinjang/service/auth/UserDetailServiceImpl.java deleted file mode 100644 index edc681fb..00000000 --- a/src/main/java/umc/th/juinjang/service/auth/UserDetailServiceImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package umc.th.juinjang.service.auth; - - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.coyote.BadRequestException; -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 umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.repository.limjang.MemberRepository; - -@Slf4j -@RequiredArgsConstructor -@Service -public class UserDetailServiceImpl implements UserDetailsService { - private final MemberRepository memberRepository; - - - public UserDetails loadUserByUsername(String memberId) throws UsernameNotFoundException { - System.out.println("로그인한 memberId : " + memberId); - UserDetails result = (UserDetails) memberRepository.findById(Long.parseLong(memberId)) - .orElseThrow(() -> new ExceptionHandler(ErrorStatus.MEMBER_NOT_FOUND)); - log.info("UserDetails: 여기ㅣㅣㅣㅣㅣㅣㅣㅣㅣㅣ"); - //로그인할 때 result.getUsername() 여기서 에러남 -// log.info("UserDetails: " + result.getUsername()); - log.info("UserDetails: " + result.toString()); - - return result; - } -} diff --git a/src/main/java/umc/th/juinjang/service/checklist/ChecklistCommandService.java b/src/main/java/umc/th/juinjang/service/checklist/ChecklistCommandService.java deleted file mode 100644 index 8aeb1767..00000000 --- a/src/main/java/umc/th/juinjang/service/checklist/ChecklistCommandService.java +++ /dev/null @@ -1,10 +0,0 @@ -package umc.th.juinjang.service.checklist; - -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerAndReportResponseDTO; -import umc.th.juinjang.model.dto.checklist.ChecklistAnswerRequestDTO; - -import java.util.List; - -public interface ChecklistCommandService { - public ChecklistAnswerAndReportResponseDTO saveChecklistAnswerList(Long limjangId, List answerDtoList); -} diff --git a/src/main/java/umc/th/juinjang/service/image/ImageCommandServiceImpl.java b/src/main/java/umc/th/juinjang/service/image/ImageCommandServiceImpl.java deleted file mode 100644 index f7b6c97f..00000000 --- a/src/main/java/umc/th/juinjang/service/image/ImageCommandServiceImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -package umc.th.juinjang.service.image; - -import java.io.IOException; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.converter.image.ImageUploadConverter; -import umc.th.juinjang.model.dto.image.ImageDeleteRequestDTO; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.repository.image.ImageRepository; -import umc.th.juinjang.repository.limjang.LimjangRepository; -import umc.th.juinjang.service.external.S3Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ImageCommandServiceImpl implements ImageCommandService { - - private final ImageRepository imageRepository; - private final LimjangRepository limjangRepository; - private final S3Service s3Service; - - @Override - @Transactional - public void uploadImages(Long limjangId, List images) { - - Limjang limjang = limjangRepository.findById(limjangId) - .orElseThrow(()-> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); - - images.forEach(it -> { - try { - if (!it.isEmpty()) { - String storedFileName = s3Service.upload(it, "image"); - Image image = ImageUploadConverter.toImageDto(storedFileName, limjang); - limjang.saveImages(image); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - - } - - @Override - @Transactional - public void deleteImages(ImageDeleteRequestDTO.DeleteDto ids - ) { //이미지 id로 삭제한다...! - List deleteIds = ids.getImageIdList(); - - try { - - //s3에서 삭제 - List imageList = imageRepository.findAllById(deleteIds); - - imageList.forEach(image -> { - s3Service.deleteFile(image.getImageUrl()); - imageRepository.deleteById(image.getImageId()); - }); - } catch (DataIntegrityViolationException e) { - throw new LimjangHandler(ErrorStatus.IMAGE_DELETE_NOT_COMPLETE); - } catch (EmptyResultDataAccessException e) { - throw new LimjangHandler(ErrorStatus.IMAGE_DELETE_NOT_FOUND); - } - } -} diff --git a/src/main/java/umc/th/juinjang/service/image/ImageQueryService.java b/src/main/java/umc/th/juinjang/service/image/ImageQueryService.java deleted file mode 100644 index b2cfd10f..00000000 --- a/src/main/java/umc/th/juinjang/service/image/ImageQueryService.java +++ /dev/null @@ -1,7 +0,0 @@ -package umc.th.juinjang.service.image; - -import umc.th.juinjang.model.dto.image.ImageListResponseDTO; - -public interface ImageQueryService { - ImageListResponseDTO.ImagesListDTO getImageList(Long limjangId); -} diff --git a/src/main/java/umc/th/juinjang/service/image/ImageQueryServiceImpl.java b/src/main/java/umc/th/juinjang/service/image/ImageQueryServiceImpl.java deleted file mode 100644 index 73b2a4ca..00000000 --- a/src/main/java/umc/th/juinjang/service/image/ImageQueryServiceImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package umc.th.juinjang.service.image; - -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.LimjangHandler; -import umc.th.juinjang.converter.image.ImageListConverter; -import umc.th.juinjang.model.dto.image.ImageListResponseDTO; -import umc.th.juinjang.model.entity.Image; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.repository.image.ImageRepository; -import umc.th.juinjang.repository.limjang.LimjangRepository; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ImageQueryServiceImpl implements ImageQueryService { - - private final ImageRepository imageRepository; - private final LimjangRepository limjangRepository; - - @Override - @Transactional(readOnly = true) - public ImageListResponseDTO.ImagesListDTO getImageList(Long limjangId) { - Limjang findLimjang = limjangRepository.findById(limjangId) - .orElseThrow(() -> new LimjangHandler(ErrorStatus.LIMJANG_NOTFOUND_ERROR)); - - List imageList = imageRepository.findImagesByLimjangId(findLimjang); - - return ImageListConverter.toImageListDto(imageList); - - } -} diff --git a/src/main/java/umc/th/juinjang/service/limjang/LimjangCommandService.java b/src/main/java/umc/th/juinjang/service/limjang/LimjangCommandService.java deleted file mode 100644 index 4515ac16..00000000 --- a/src/main/java/umc/th/juinjang/service/limjang/LimjangCommandService.java +++ /dev/null @@ -1,16 +0,0 @@ -package umc.th.juinjang.service.limjang; - -import umc.th.juinjang.model.dto.limjang.request.LimjangPatchRequest; -import umc.th.juinjang.model.dto.limjang.request.LimjangPostRequest; -import umc.th.juinjang.model.dto.limjang.request.LimjangsDeleteRequest; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; - -public interface LimjangCommandService { - - Limjang postLimjang(LimjangPostRequest request, Member member); - - void deleteLimjangs(LimjangsDeleteRequest deleteIds, Member member); - - void updateLimjang(Member member, long limjangId, LimjangPatchRequest request); -} diff --git a/src/main/java/umc/th/juinjang/service/limjang/LimjangQueryService.java b/src/main/java/umc/th/juinjang/service/limjang/LimjangQueryService.java deleted file mode 100644 index 112a1577..00000000 --- a/src/main/java/umc/th/juinjang/service/limjang/LimjangQueryService.java +++ /dev/null @@ -1,23 +0,0 @@ -package umc.th.juinjang.service.limjang; - -import umc.th.juinjang.model.dto.limjang.response.LimjangDetailGetResponse; - -import umc.th.juinjang.model.dto.limjang.enums.LimjangSortOptions; -import umc.th.juinjang.model.dto.limjang.response.LimjangsGetByKeywordResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetResponse; -import umc.th.juinjang.model.dto.limjang.response.LimjangsMainGetVersion2Response; -import umc.th.juinjang.model.entity.Member; - -public interface LimjangQueryService { - - LimjangsGetResponse getLimjangTotalList(Member member, LimjangSortOptions sort); - - LimjangsMainGetResponse getLimjangsMain(Member member); - - LimjangsGetByKeywordResponse getLimjangSearchList(Member member, String keyword); - - LimjangDetailGetResponse getDetail(long id, Member member); - - LimjangsMainGetVersion2Response getLimjangsMainVersion2(Member member); -} diff --git a/src/main/java/umc/th/juinjang/service/limjang/LimjangSchedulerService.java b/src/main/java/umc/th/juinjang/service/limjang/LimjangSchedulerService.java deleted file mode 100644 index 9961cd53..00000000 --- a/src/main/java/umc/th/juinjang/service/limjang/LimjangSchedulerService.java +++ /dev/null @@ -1,36 +0,0 @@ -package umc.th.juinjang.service.limjang; - - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import lombok.RequiredArgsConstructor; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; -import umc.th.juinjang.repository.limjang.LimjangRepository; - -@Component -@RequiredArgsConstructor -public class LimjangSchedulerService { - - private final LimjangRepository limjangRepository; - - @Transactional - // @Scheduled(fixedRate = 60000) // 1분 간격으로 실행 - test용 -// @Scheduled(fixedRate = 7 * 24 * 60 * 60 * 1000) // 일주일 간격으로 실행 <- 나중에 출시하면 이걸로 - @Scheduled(fixedRate = 31557600000L) // 일년 간격으로 실행(임시) - public void cleanUpData() { - // 삭제 필드 true 된지 1달된거 삭제 - LocalDateTime deletionCycle = LocalDateTime.now().minusMonths(1); - - DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); - System.out.println("스케줄러 테스트 " + LocalDateTime.now().format(dtf)); - - try { - limjangRepository.hardDelete(deletionCycle); - }catch (Exception e) { - System.out.println("hardDelete 중 에러발생함.."); - } - } - -} diff --git a/src/main/java/umc/th/juinjang/service/member/MemberService.java b/src/main/java/umc/th/juinjang/service/member/MemberService.java deleted file mode 100644 index dfd0abb9..00000000 --- a/src/main/java/umc/th/juinjang/service/member/MemberService.java +++ /dev/null @@ -1,101 +0,0 @@ -package umc.th.juinjang.service.member; - -import static umc.th.juinjang.apiPayload.code.status.ErrorStatus.MEMBER_NOT_FOUND; - -import com.amazonaws.AmazonServiceException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectMetadata; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; -import umc.th.juinjang.apiPayload.ExceptionHandler; -import umc.th.juinjang.apiPayload.code.status.ErrorStatus; -import umc.th.juinjang.apiPayload.exception.handler.MemberHandler; -import umc.th.juinjang.model.dto.member.MemberAgreeVersionPostRequest; -import umc.th.juinjang.model.dto.member.MemberRequestDto; -import umc.th.juinjang.model.dto.member.MemberResponseDto; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.repository.limjang.MemberRepository; - -import java.io.IOException; -import java.util.UUID; - -@Slf4j -@Service -@Transactional -@RequiredArgsConstructor -public class MemberService { - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - @Value("${cloud.aws.s3.uploadPath}") - private String defaultUrl; - - @Autowired - private final AmazonS3Client amazonS3Client; - private final MemberRepository memberRepository; - - // 닉네임 수정 - public MemberResponseDto.nicknameDto patchNickname(Member member, MemberRequestDto memberRequestDto) { - // Member 받아오면 해당 member의 nickname 변경 - member.updateNickname(memberRequestDto.getNickname()); - memberRepository.save(member); // 변수 없이 member 그대로 저장 - - return new MemberResponseDto.nicknameDto(member.getNickname()); - } - - // 프로필 조회 - public MemberResponseDto.profileDto getProfile(Member member) { - String provider = member.getProvider().toString(); - return new MemberResponseDto.profileDto(member.getNickname(), member.getEmail(), provider, member.getImageUrl()); - } - - // 프로필 이미지 수정 - public MemberResponseDto.profileDto updateProfileImage(Member member, MultipartFile multipartFile) { - String newUrl = null; - String fileUrl = member.getImageUrl(); - - if(fileUrl != null) { - String[] url = fileUrl.split("/"); - amazonS3Client.deleteObject(bucket, url[3]); - } - - try { - String originalFilename = multipartFile.getOriginalFilename(); - String newfileName = UUID.randomUUID() + "_" + originalFilename; - - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentLength(multipartFile.getSize()); - metadata.setContentType(multipartFile.getContentType()); - - //S3에 저장 - amazonS3Client.putObject(bucket, "profile/" +newfileName, multipartFile.getInputStream(), metadata); - newUrl = amazonS3Client.getUrl(bucket, "profile/" + newfileName).toString(); - } catch (IOException e) { - e.printStackTrace(); - } catch(AmazonServiceException e){ - e.printStackTrace(); - } - - if(newUrl == null) - throw new ExceptionHandler(ErrorStatus.IMAGE_NOT_SAVE); - - member.updateImage(newUrl); - memberRepository.save(member); - - return new MemberResponseDto.profileDto(member.getNickname(), member.getEmail(), member.getProvider().toString(), member.getImageUrl()); - } - - public void createMemberAgreeVersion(final Member member, final MemberAgreeVersionPostRequest memberAgreeVersionPostRequest) { - getMember(member).updateAgreeVersion(memberAgreeVersionPostRequest.agreeVersion()); - } - - private Member getMember(Member member) { - return memberRepository.findById(member.getMemberId()).orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index eb6ceacf..00000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- - -spring: - profiles: - active: dev - ---- - -spring: - profiles: - active: prod - ---- \ No newline at end of file diff --git a/src/main/resources/certs/AppleIncRootCertificate.cer b/src/main/resources/certs/AppleIncRootCertificate.cer new file mode 100644 index 00000000..8a9ff247 Binary files /dev/null and b/src/main/resources/certs/AppleIncRootCertificate.cer differ diff --git a/src/main/resources/certs/AppleRootCA-G2.cer b/src/main/resources/certs/AppleRootCA-G2.cer new file mode 100644 index 00000000..739b8141 Binary files /dev/null and b/src/main/resources/certs/AppleRootCA-G2.cer differ diff --git a/src/main/resources/certs/AppleRootCA-G3.cer b/src/main/resources/certs/AppleRootCA-G3.cer new file mode 100644 index 00000000..228bfa39 Binary files /dev/null and b/src/main/resources/certs/AppleRootCA-G3.cer differ diff --git a/src/main/resources/logback-appender.xml b/src/main/resources/logback-appender.xml index 68ca4fdd..43647c9f 100644 --- a/src/main/resources/logback-appender.xml +++ b/src/main/resources/logback-appender.xml @@ -1,92 +1,107 @@ - + - + - - - + - - + - - - ${CONSOLE_LOG_PATTERN} - - + + + ${CONSOLE_LOG_PATTERN} + + - - - ${API_CONSOLE_LOG_PATTERN} - - + + + ${CONSOLE_LOG_PATTERN} + + - - ${LOG_PATH}/console/info-${SERVER_INFO}-console.log - - INFO - ACCEPT - DENY - - - ${LOG_PATH}/console/info-${SERVER_INFO}-console-%d{yyyy-MM-dd}.%i.txt - 20MB - 7 - 100MB - - - ${INFO_LOG_PATTERN} - - + + ${LOG_PATH}/console/info-${SERVER_INFO}-console.log + + INFO + ACCEPT + DENY + + + ${LOG_PATH}/console/info-${SERVER_INFO}-console-%d{yyyy-MM-dd}.%i.txt + 20MB + 7 + 100MB + + + ${BASE_LOG_PATTERN} + + - - ${LOG_PATH}/console/warn-${SERVER_INFO}-console.log - - WARN - ACCEPT - DENY - - - ${LOG_PATH}/console/warn-${SERVER_INFO}-console-%d{yyyy-MM-dd}.%i.txt - 20MB - 7 - 100MB - - - ${WARN_LOG_PATTERN} - - + + ${LOG_PATH}/console/warn-${SERVER_INFO}-console.log + + WARN + ACCEPT + DENY + + + ${LOG_PATH}/console/warn-${SERVER_INFO}-console-%d{yyyy-MM-dd}.%i.txt + 20MB + 7 + 100MB + + + ${BASE_LOG_PATTERN} + + - - ${LOG_PATH}/console/error-${SERVER_INFO}-console.log - - ERROR - ACCEPT - DENY - - - ${LOG_PATH}/console/error-${SERVER_INFO}-console-%d{yyyy-MM-dd}.%i.txt - 20MB - 7 - 100MB - - - ${ERROR_LOG_PATTERN} - - + + ${LOG_PATH}/console/error-${SERVER_INFO}-console.log + + ERROR + ACCEPT + DENY + + + ${LOG_PATH}/console/error-${SERVER_INFO}-console-%d{yyyy-MM-dd}.%i.txt + 20MB + 7 + 100MB + + + ${BASE_LOG_PATTERN} + + + + + ${LOG_PATH}/api/${SERVER_INFO}-api.log + + ${LOG_PATH}/api/${SERVER_INFO}-api-%d{yyyy-MM-dd}.%i.txt + 20MB + 7 + 100MB + + + ${FILE_LOG_PATTERN} + + + + + + ${LOG_PATH}/sql/sql-${SERVER_INFO}.log + + ${LOG_PATH}/sql/sql-${SERVER_INFO}-%d{yyyy-MM-dd}.%i.txt + 20MB + 7 + 100MB + + + ${FILE_LOG_PATTERN} + + - - ${LOG_PATH}/api/${SERVER_INFO}-api.log - - ${LOG_PATH}/api/${SERVER_INFO}-api-%d{yyyy-MM-dd}.%i.txt - 20MB - 7 - 100MB - - - ${API_FILE_LOG_PATTERN} - - \ No newline at end of file diff --git a/src/main/resources/logback-dev.xml b/src/main/resources/logback-dev.xml index dee5fd9b..f3f6f6fe 100644 --- a/src/main/resources/logback-dev.xml +++ b/src/main/resources/logback-dev.xml @@ -1,22 +1,32 @@ - - - - + + + + - - - - - - + + + + + + - - - - + + + + + + + + + + + + + + + + - - \ No newline at end of file diff --git a/src/main/resources/logback-local.xml b/src/main/resources/logback-local.xml index 794a255e..53df0253 100644 --- a/src/main/resources/logback-local.xml +++ b/src/main/resources/logback-local.xml @@ -1,23 +1,31 @@ - - - - + + + + - - - - - - + + + + + + - - - - + + + + - - - + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback-prod.xml b/src/main/resources/logback-prod.xml index 7ce64d90..e7f28785 100644 --- a/src/main/resources/logback-prod.xml +++ b/src/main/resources/logback-prod.xml @@ -1,22 +1,30 @@ - - - - + + + + - - - - - - + + + + + + - - - - + + + + - - + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/umc/th/juinjang/api/ControllerTestSupport.java b/src/test/java/umc/th/juinjang/api/ControllerTestSupport.java new file mode 100644 index 00000000..1e5f2cb3 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/ControllerTestSupport.java @@ -0,0 +1,36 @@ +package umc.th.juinjang.api; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import umc.th.juinjang.api.pencil.controller.PencilController; +import umc.th.juinjang.api.pencil.service.PencilCommandService; +import umc.th.juinjang.api.pencil.service.PencilQueryService; +import umc.th.juinjang.api.pencilAccount.controller.PencilAccountController; +import umc.th.juinjang.api.pencilAccount.service.PencilAccountService; + +@WebMvcTest(controllers = { + PencilController.class, + PencilAccountController.class +}) +public abstract class ControllerTestSupport { + + @Autowired + protected MockMvc mockMvc; + + @Autowired + protected ObjectMapper objectMapper; + + @MockBean + protected PencilAccountService pencilAccountService; + + @MockBean + protected PencilQueryService pencilQueryService; + + @MockBean + protected PencilCommandService pencilCommandService; +} diff --git a/src/test/java/umc/th/juinjang/api/IntegrationTestSupport.java b/src/test/java/umc/th/juinjang/api/IntegrationTestSupport.java new file mode 100644 index 00000000..8ab802e4 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/IntegrationTestSupport.java @@ -0,0 +1,9 @@ +package umc.th.juinjang.api; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest +public abstract class IntegrationTestSupport { +} diff --git a/src/test/java/umc/th/juinjang/api/auth/service/OauthServiceTest.java b/src/test/java/umc/th/juinjang/api/auth/service/OauthServiceTest.java new file mode 100644 index 00000000..3512a1ef --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/auth/service/OauthServiceTest.java @@ -0,0 +1,63 @@ +package umc.th.juinjang.api.auth.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import umc.th.juinjang.api.IntegrationTestSupport; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberStatus; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.external.openfeign.kakao.KakaoUnlinkClient; + +public class OauthServiceTest extends IntegrationTestSupport { + + @MockBean + private KakaoUnlinkClient kakaoUnlinkClient; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private OAuthServiceV2 oauthService; + + @Test + @DisplayName("카카오 탈퇴 성공 - 카카오 연결 끊기 성공 시 회원 상태가 WITHDRAWN으로 변경") + void kakaoWithdraw_Success() { + // given + Long testTargetId = 123456789L; + + Member testMember = Member.createKakaoMember( + "test@example.com", + testTargetId, + "테스트유저", + "1.0" + ); + memberRepository.save(testMember); + + ResponseEntity successResponse = new ResponseEntity<>("success", HttpStatus.OK); + + BDDMockito.when( + kakaoUnlinkClient.unlinkUser(any(), any(), any()) + ).thenReturn(successResponse); + + // when + boolean result = oauthService.kakaoWithdraw(testMember, testTargetId); + + // then + Member updatedMember = memberRepository.findById(testMember.getMemberId()).orElseThrow(); + assertThat(updatedMember) + .extracting(Member::getMemberId, Member::getStatus, Member::getKakaoTargetId, Member::getNickname, + Member::getDeletedAt) + .containsExactly(testMember.getMemberId(), MemberStatus.WITHDRAWN, null, null, + updatedMember.getDeletedAt()); + } + +} diff --git a/src/test/java/umc/th/juinjang/api/member/controller/MemberControllerTest.java b/src/test/java/umc/th/juinjang/api/member/controller/MemberControllerTest.java new file mode 100644 index 00000000..b6185026 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/member/controller/MemberControllerTest.java @@ -0,0 +1,63 @@ +package umc.th.juinjang.api.member.controller; + +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import umc.th.juinjang.api.member.service.MemberService; + +@WebMvcTest(MemberController.class) +@WithMockUser +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private MemberService memberService; + + @DisplayName("닉네임 중복 체크 API - 중복되지 않은 닉네임") + @Test + void checkNickname_whenNicknameDoesNotExist_thenReturnFalse() throws Exception { + // given + String nickname = "newNickname"; + given(memberService.isNicknameExists(nickname)).willReturn(false); + + // when & then + mockMvc.perform(get("/api/members/nickname/exists") + .param("nickname", nickname) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.result.exists").value(false)); + } + + @DisplayName("닉네임 중복 체크 API - 중복된 닉네임") + @Test + void checkNickname_whenNicknameExists_thenReturnTrue() throws Exception { + // given + String nickname = "existingNickname"; + given(memberService.isNicknameExists(nickname)).willReturn(true); + + // when & then + mockMvc.perform(get("/api/members/nickname/exists") + .param("nickname", nickname)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.result.exists").value(true)); + } +} diff --git a/src/test/java/umc/th/juinjang/api/member/service/MemberServiceTest.java b/src/test/java/umc/th/juinjang/api/member/service/MemberServiceTest.java new file mode 100644 index 00000000..aa400992 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/member/service/MemberServiceTest.java @@ -0,0 +1,147 @@ +package umc.th.juinjang.api.member.service; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import umc.th.juinjang.api.IntegrationTestSupport; +import umc.th.juinjang.api.member.service.response.MemberResponseDto; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.domain.pencilaccount.repository.PencilAccountRepository; +import umc.th.juinjang.testutil.fixture.MemberFixture; + +public class MemberServiceTest extends IntegrationTestSupport { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private PencilAccountRepository pencilAccountRepository; + + @Autowired + private MemberService memberService; + + @AfterEach + void tearDown() { + pencilAccountRepository.deleteAllInBatch(); + memberRepository.deleteAllInBatch(); + } + + private final String DEFAULT_EMAIL = "test@naver.com"; + private final String DEFAULT_IMAGE_URL = "https://image.url.com"; + private final String DEFAULT_NICKNAME = "test"; + private final String DEFAULT_INTRODUCTION = ""; + + @DisplayName("프로필 조회 시에, (닉네임,이메일,이미지,한줄 소개) 들이 정상적으로 보이는 가?") + @Test + void getProfile() { + // given + Member member = createAndSaveMember(); + + // when + MemberResponseDto.profileDto profileDto = memberService.getProfile(member); + + // then + assertThat(profileDto) + .extracting("nickname", "email", "image", "introduction") + .containsExactly(DEFAULT_NICKNAME, DEFAULT_EMAIL, DEFAULT_IMAGE_URL, DEFAULT_INTRODUCTION); + } + + @DisplayName("한 줄 소개 변경이 정상적으로 작동하는 가?") + @Test + void patchIntroduction() { + // given + Member member = createAndSaveMember(); + String changedIntroduction = "잘 부탁드립니다.!! 여러분"; + + // when + memberService.updateIntroduction(member, changedIntroduction); + + Member updatedMember = memberRepository.findById(member.getMemberId()) + .orElseThrow(() -> new RuntimeException("Member not found")); + + // then + assertThat(updatedMember.getIntroduction()).isEqualTo(changedIntroduction); + } + + @Nested + @DisplayName("닉네임 중복 검사") + class NicknameExistsTest { + + // static 필드로 선언 + private static String existingNickname; + + @BeforeEach + void setUp() { + // given - 여러 멤버 데이터 한 번만 설정 + existingNickname = "테스트1"; + String nickname2 = "테스트2"; + String nickname3 = "테스트3"; + + Member member1 = MemberFixture.createMemberWithParams( + "custom1@example.com", 11111111L, existingNickname, + "안녕하세요", "https://custom.image.url"); + + Member member2 = MemberFixture.createMemberWithParams( + "custom2@example.com", 2222222L, nickname2, + "안녕하세요", "https://custom.image.url"); + + Member member3 = MemberFixture.createMemberWithParams( + "custom3@example.com", 3333333L, nickname3, + "안녕하세요", "https://custom.image.url"); + + memberRepository.saveAll(List.of(member1, member2, member3)); + } + + @DisplayName("닉네임이 중복되었을 때, 중복 여부를 True 로 반환한다") + @Test + void returnsTrueWhenNicknameExists() { + // when + boolean result = memberService.isNicknameExists(existingNickname); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("닉네임이 중복되지 않았을 때, 중복 여부를 False 로 반환한다") + @Test + void returnsFalseWhenNicknameDoesNotExist() { + // given + String nonExistingNickname = "존재하지않는닉네임"; + + // when + boolean result = memberService.isNicknameExists(nonExistingNickname); + + // then + assertThat(result).isFalse(); + } + } + + private Member createDefaultMember() { + return Member.builder() + .email(DEFAULT_EMAIL) + .provider(MemberProvider.KAKAO) + .kakaoTargetId(91681234L) + .nickname(DEFAULT_NICKNAME) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now().plusDays(7L)) + .introduction(DEFAULT_INTRODUCTION) + .imageUrl(DEFAULT_IMAGE_URL) + .build(); + } + + private Member createAndSaveMember() { + Member member = createDefaultMember(); + return memberRepository.save(member); + } +} diff --git a/src/test/java/umc/th/juinjang/api/pencil/service/PencilCommandServiceTest.java b/src/test/java/umc/th/juinjang/api/pencil/service/PencilCommandServiceTest.java new file mode 100644 index 00000000..6dac1e79 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/pencil/service/PencilCommandServiceTest.java @@ -0,0 +1,145 @@ +package umc.th.juinjang.api.pencil.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.mock.mockito.MockBean; + +import com.apple.itunes.storekit.client.APIException; +import com.apple.itunes.storekit.model.JWSTransactionDecodedPayload; +import com.apple.itunes.storekit.verification.VerificationException; + +import lombok.extern.slf4j.Slf4j; +import umc.th.juinjang.api.IntegrationTestSupport; +import umc.th.juinjang.api.apple.service.AppleService; +import umc.th.juinjang.api.apple.service.command.AppleTransactionVerifyCommand; +import umc.th.juinjang.api.pencil.controller.request.AppleIAPPurchaseRequest; +import umc.th.juinjang.api.pencil.service.response.AppleIAPPurchaseResponse; +import umc.th.juinjang.api.pencil.service.response.VerificationResult; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.domain.pencil.purchased.model.TransactionStatus; +import umc.th.juinjang.domain.pencil.purchased.repository.PurchasedPencilRepository; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.domain.pencilaccount.repository.PencilAccountRepository; +import umc.th.juinjang.testutil.fixture.MemberFixture; + +@Slf4j +public class PencilCommandServiceTest extends IntegrationTestSupport { + + @Value("${apple.iap.bundle-id}") + private String bundleId; + + private final String transactionId = "transactionId"; + private final UUID appAccountToken = UUID.randomUUID(); + private final String productId = "productId"; + + @Autowired + private PencilCommandService pencilService; + + @Autowired + private PurchasedPencilRepository purchasedPencilRepository; + + @Autowired + private PencilAccountRepository pencilAccountRepository; + + @Autowired + private MemberRepository memberRepository; + + @MockBean + private AppleService appleService; + + @AfterEach + void tearDown() { + purchasedPencilRepository.deleteAllInBatch(); + pencilAccountRepository.deleteAllInBatch(); + memberRepository.deleteAllInBatch(); + } + + @DisplayName("애플 인앱 결제과 정상적으로 진행됩니다.") + @Test + void processAppleIAPPurchase_Success() throws APIException, VerificationException, IOException { + // given + Member member = MemberFixture.createDefaultMember(); + memberRepository.save(member); + + PencilAccount pencilAccount = PencilAccount.createPencilAccount(member); + pencilAccountRepository.save(pencilAccount); + + AppleIAPPurchaseRequest request = createValidRequest(); + LocalDateTime now = LocalDateTime.now(); + + JWSTransactionDecodedPayload payload = new JWSTransactionDecodedPayload(); + payload.setProductId(productId); + payload.setAppAccountToken(appAccountToken); + payload.setBundleId(bundleId); + payload.setQuantity(20); + payload.setTransactionId(transactionId); + + when(appleService.verifyAppleTransaction(any(AppleTransactionVerifyCommand.class))) + .thenReturn(VerificationResult.ofSuccess(payload)); + + // when + AppleIAPPurchaseResponse response = pencilService.processAppleIAPPurchase(request, member,now); + + // then + log.info("[RESPONSE - TRANSACTION_ID]: {}", response.getTransactionId()); + + assertThat(response).isNotNull(); + assertThat(response.getTransactionId()).isEqualTo(transactionId); + assertThat(response.getStatus()).isEqualTo(TransactionStatus.SUCCESS); + } + + @DisplayName("애플 인앱 결제 중에 유효성 검증에서 실패한 경우 (트랜잭션 아이디가 불일치) 에 대한 테스트 진행") + @Test + void processAppleIAPPurchase_Validation_Fail() throws APIException, VerificationException, IOException { + // given + Member member = MemberFixture.createDefaultMember(); + memberRepository.save(member); + + PencilAccount pencilAccount = PencilAccount.createPencilAccount(member); + pencilAccountRepository.save(pencilAccount); + + // when + AppleIAPPurchaseRequest request = createValidRequest(); + LocalDateTime now = LocalDateTime.now(); + + String payloadTransactionId = "invalidTransactionId"; + JWSTransactionDecodedPayload payload = new JWSTransactionDecodedPayload(); + payload.setProductId(productId); + payload.setAppAccountToken(appAccountToken); + payload.setBundleId(bundleId); + payload.setQuantity(20); + payload.setTransactionId(payloadTransactionId); + + // then + when(appleService.verifyAppleTransaction(any(AppleTransactionVerifyCommand.class))) + .thenReturn(VerificationResult.ofVerificationError()); + + // when + AppleIAPPurchaseResponse response = pencilService.processAppleIAPPurchase(request, member,now); + + // then + assertThat(response).isNotNull(); + assertThat(response.getTransactionId()).isEqualTo(transactionId); + assertThat(response.getStatus()).isEqualTo(TransactionStatus.VALIDATION_FAILED); + } + + + + private AppleIAPPurchaseRequest createValidRequest() { + return AppleIAPPurchaseRequest + .of(transactionId, appAccountToken, 20L, 3000L, productId,10); + } +} + + diff --git a/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java b/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java new file mode 100644 index 00000000..a5f6c76b --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/pencil/service/PencilQueryServiceTest.java @@ -0,0 +1,312 @@ +// package umc.th.juinjang.api.pencil.service; +// +// import static org.assertj.core.api.Assertions.*; +// +// import java.time.LocalDateTime; +// import java.util.List; +// import java.util.UUID; +// +// import org.assertj.core.groups.Tuple; +// import org.junit.jupiter.api.AfterEach; +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// +// import com.apple.itunes.storekit.model.ConsumptionRequest; +// import com.apple.itunes.storekit.model.ConsumptionStatus; +// import com.apple.itunes.storekit.model.LifetimeDollarsPurchased; +// import com.apple.itunes.storekit.model.LifetimeDollarsRefunded; +// import com.apple.itunes.storekit.model.Platform; +// import com.apple.itunes.storekit.model.PlayTime; +// +// import lombok.extern.slf4j.Slf4j; +// import umc.th.juinjang.api.IntegrationTestSupport; +// import umc.th.juinjang.api.pencil.service.response.AcquiredPencilResponse; +// import umc.th.juinjang.api.pencil.service.response.PurchasedPencilResponse; +// import umc.th.juinjang.api.pencil.service.response.UsedPencilResponse; +// import umc.th.juinjang.domain.member.model.Member; +// import umc.th.juinjang.domain.member.repository.MemberRepository; +// import umc.th.juinjang.domain.pencil.acquired.model.AcquiredPencil; +// import umc.th.juinjang.domain.pencil.acquired.model.AcquiredType; +// import umc.th.juinjang.domain.pencil.acquired.repository.AcquiredPencilRepository; +// import umc.th.juinjang.domain.pencil.purchased.model.PurchasedPencil; +// import umc.th.juinjang.domain.pencil.purchased.repository.PurchasedPencilRepository; +// import umc.th.juinjang.domain.pencil.used.model.UsedPencil; +// import umc.th.juinjang.domain.pencil.used.model.Usedtype; +// import umc.th.juinjang.domain.pencil.used.repository.UsedPencilRepository; +// import umc.th.juinjang.testutil.fixture.MemberFixture; +// +// @Slf4j +// class PencilQueryServiceTest extends IntegrationTestSupport { +// +// @Autowired +// private MemberRepository memberRepository; +// +// @Autowired +// private AcquiredPencilRepository acquiredPencilRepository; +// +// @Autowired +// private PurchasedPencilRepository purchasedPencilRepository; +// +// @Autowired +// private UsedPencilRepository usedPencilRepository; +// +// @Autowired +// private PencilQueryService pencilService; +// +// @AfterEach +// void tearDown() { +// purchasedPencilRepository.deleteAllInBatch(); +// acquiredPencilRepository.deleteAllInBatch(); +// usedPencilRepository.deleteAllInBatch(); +// memberRepository.deleteAllInBatch(); +// } +// +// @DisplayName("얻은 연필 목록이 없는 경우에는 빈 배열이 반환된다.") +// @Test +// void getEmptyAcquiredPencilsList() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// // when +// List list = pencilService.getAcquiredPencils(member); +// +// // then +// assertThat(list).hasSize(0); +// } +// +// @DisplayName("얻은 연필 목록이 생성 시간(createdAt) 내림차순으로 정렬되어 반환된다.") +// @Test +// void getAcquiredPencilsOrderedByCreatedAtDesc() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// // 시간을 내림차순으로 생성 (최신 시간이 먼저 오도록) +// LocalDateTime now = LocalDateTime.now(); +// LocalDateTime time1 = now.minusHours(4); +// LocalDateTime time2 = now.minusHours(3); +// LocalDateTime time3 = now.minusHours(2); +// LocalDateTime time4 = now.minusHours(1); +// LocalDateTime time5 = now; +// +// // 명확한 순서로 데이터 생성 (시간 역순으로) +// AcquiredPencil pencil1 = createAcquiredPencilWithTime(time1, "노트 작성으로 연필 획득", 1L, 10L, false, AcquiredType.NOTE, +// member); +// AcquiredPencil pencil2 = createAcquiredPencilWithTime(time2, "연필팩 구매로 연필 추가", 2L, 20L, true, AcquiredType.SOLD, +// member); +// AcquiredPencil pencil3 = createAcquiredPencilWithTime(time3, "매물 판매로 연필 획득", 3L, 30L, false, AcquiredType.SOLD, +// member); +// AcquiredPencil pencil4 = createAcquiredPencilWithTime(time4, "다른 노트 작성으로 연필 획득", 4L, 15L, true, +// AcquiredType.NOTE, member); +// AcquiredPencil pencil5 = createAcquiredPencilWithTime(time5, "또 다른 매물 판매로 연필 획득", 5L, 25L, false, +// AcquiredType.SOLD, member); +// +// acquiredPencilRepository.saveAll(List.of(pencil1, pencil2, pencil3, pencil4, pencil5)); +// +// // when +// List foundPencils = pencilService.getAcquiredPencils(member); +// +// foundPencils.forEach(pencil -> +// log.info("[ACQUIRED PENCILS]: {}", pencil.getCreatedAt()) +// ); +// +// // then +// assertThat(foundPencils).hasSize(5) +// .extracting("content", "sharedNoteId", "acquiredQuantity") +// .containsExactly( +// // 최신 시간부터 나열 (내림차순) +// Tuple.tuple("또 다른 매물 판매로 연필 획득", 5L, 25L), +// Tuple.tuple("다른 노트 작성으로 연필 획득", 4L, 15L), +// Tuple.tuple("매물 판매로 연필 획득", 3L, 30L), +// Tuple.tuple("연필팩 구매로 연필 추가", 2L, 20L), +// Tuple.tuple("노트 작성으로 연필 획득", 1L, 10L) +// ); +// } +// +// @DisplayName("구매한 연필 목록이 없는 경우에는 빈 배열이 반환된다.") +// @Test +// void getEmptyPurchasedPencilsList() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// // when +// List list = pencilService.getPurchasedPencils(member); +// +// // then +// assertThat(list).hasSize(0); +// } +// +// @DisplayName("구매한 연필 목록이 생성 시간(createdAt) 내림차순으로 정렬되어 반환된다.") +// @Test +// void getPurchasedPencilsOrderedByCreatedAtDesc() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// // 시간을 내림차순으로 생성 (최신 시간이 먼저 오도록) +// LocalDateTime now = LocalDateTime.now(); +// LocalDateTime time1 = now.minusHours(4); +// LocalDateTime time2 = now.minusHours(3); +// LocalDateTime time3 = now.minusHours(2); +// LocalDateTime time4 = now.minusHours(1); +// LocalDateTime time5 = now; +// +// // 랜덤 UUID 생성을 위한 도우미 +// UUID uuid1 = UUID.randomUUID(); +// UUID uuid2 = UUID.randomUUID(); +// UUID uuid3 = UUID.randomUUID(); +// UUID uuid4 = UUID.randomUUID(); +// UUID uuid5 = UUID.randomUUID(); +// +// // 명확한 순서로 데이터 생성 (시간 역순으로) +// PurchasedPencil pencil1 = PurchasedPencil.successOf(member, "10개 연필팩", 10L, 1000L, 10L, 0, "transaction1", +// uuid1, time1); +// PurchasedPencil pencil2 = PurchasedPencil.successOf(member, "20개 연필팩", 20L, 2000L, 30L, 0, "transaction2", +// uuid2, +// time2); +// PurchasedPencil pencil3 = PurchasedPencil.successOf(member, "30개 연필팩", 30L, 3000L, 60L, 0, "transaction3", +// uuid3, +// time3); +// PurchasedPencil pencil4 = PurchasedPencil.successOf(member, "15개 연필팩", 15L, 1500L, 75L, 0, "transaction4", +// uuid4, +// time4); +// PurchasedPencil pencil5 = PurchasedPencil.successOf(member, "25개 연필팩", 25L, 2500L, 90L, 0, "transaction5", +// uuid5, +// time5); +// +// purchasedPencilRepository.saveAll(List.of(pencil1, pencil2, pencil3, pencil4, pencil5)); +// +// // when +// List purchasedPencils = pencilService.getPurchasedPencils(member); +// +// purchasedPencils.forEach(pencil -> { +// log.info("[PENCILS]: CREATED_AT : {} ", pencil.getPurchasedAt()); +// } +// ); +// // then +// assertThat(purchasedPencils).hasSize(5) +// .extracting("title", "purchaseQuantity", "price") +// .containsExactly( +// Tuple.tuple("25개 연필팩", 25L, 2500L), +// Tuple.tuple("15개 연필팩", 15L, 1500L), +// Tuple.tuple("30개 연필팩", 30L, 3000L), +// Tuple.tuple("20개 연필팩", 20L, 2000L), +// Tuple.tuple("10개 연필팩", 10L, 1000L) +// ); +// } +// +// @DisplayName("구매한 연필 목록에서 DeliveryStatus 가 에러인 경우에만 목록에 반환되지 않는다.") +// @Test +// void getPurchasedPencilsWithoutDeliveryStatus() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// LocalDateTime time = LocalDateTime.now(); +// UUID uuid = UUID.randomUUID(); +// +// PurchasedPencil pencil = PurchasedPencil.failedDueToServerError(member, "10개 연필팩", 10L, 1000L, 10, +// "transaction1", uuid, time); +// +// purchasedPencilRepository.saveAll(List.of(pencil)); +// +// // when +// List purchasedPencils = pencilService.getPurchasedPencils(member); +// +// assertThat(purchasedPencils).hasSize(0); +// } +// +// @DisplayName("구매한 연필 목록이 없는 경우에는 빈 배열이 반환된다.") +// @Test +// void getEmptyUsedPencilsList() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// // when +// List list = pencilService.getUsedPencils(member); +// +// // then +// assertThat(list).hasSize(0); +// } +// +// @DisplayName("사용한 연필 목록이 생성 시간(createdAt) 내림차순으로 정렬되어 반환된다.") +// @Test +// void getUsedPencilsOrderedByCreatedAtDesc() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// UsedPencil usedPencil = UsedPencil.create(member, 1L, 10L, Usedtype.OWNED, "빌딩", 10L); +// usedPencilRepository.saveAll(List.of(usedPencil)); +// +// // when +// List usedPencils = pencilService.getUsedPencils(member); +// +// // then +// // TODO: 추후에, 시간이 OrderBy 가 정상적으로 되는 지 테스트가 필요 +// assertThat(usedPencils).hasSize(1) +// .extracting("type", "buildingName", "sharedNoteId") +// .containsExactly( +// Tuple.tuple(Usedtype.OWNED, "빌딩", 1L) +// ); +// } +// +// @DisplayName("ConsumptionRequest가 PurchasedPencil 데이터를 기반으로 올바르게 생성된다.") +// @Test +// void getConsumptionRequestFromPurchasedPencil() { +// // given +// Member member = MemberFixture.createDefaultMember(); +// memberRepository.save(member); +// +// LocalDateTime now = LocalDateTime.now(); +// String transactionId = "test-transaction-id"; +// UUID appAccountToken = UUID.randomUUID(); +// +// PurchasedPencil pencil = PurchasedPencil.successOf( +// member, +// "테스트 연필팩", +// 20L, +// 2000L, +// 10L, +// 10, +// transactionId, +// appAccountToken, +// now +// ); +// +// purchasedPencilRepository.save(pencil); +// +// // AcquiredPencil 데이터를 하나라도 만들어줘야 sampleContentProvided == true +// acquiredPencilRepository.save( +// AcquiredPencil.create(member, "노트 작성", 1L, 10L, 10L, false, AcquiredType.NOTE) +// ); +// +// // when +// ConsumptionRequest request = pencilService.getConsumptionRequest(transactionId); +// +// // then +// assertThat(request).isNotNull(); +// assertThat(request.getAppAccountToken()).isEqualTo(appAccountToken); +// assertThat(request.getDeliveryStatus().getValue()).isEqualTo(pencil.getDeliveryStatus().getAppleCode()); +// assertThat(request.getPlayTime()).isEqualTo(PlayTime.FIVE_TO_SIXTY_MINUTES); +// assertThat(request.getLifetimeDollarsPurchased()).isEqualTo( +// LifetimeDollarsPurchased.ONE_CENT_TO_FORTY_NINE_DOLLARS_AND_NINETY_NINE_CENTS); +// assertThat(request.getLifetimeDollarsRefunded()).isEqualTo(LifetimeDollarsRefunded.ZERO_DOLLARS); +// assertThat(request.getCustomerConsented()).isTrue(); +// assertThat(request.getSampleContentProvided()).isTrue(); +// assertThat(request.getPlatform()).isEqualTo(Platform.APPLE); +// +// assertThat(request.getConsumptionStatus()).isEqualTo(ConsumptionStatus.NOT_CONSUMED); +// } +// +// private AcquiredPencil createAcquiredPencilWithTime(LocalDateTime createdAt, String content, Long sharedNoteId, +// Long acquiredQuantity, boolean isRead, AcquiredType type, Member member) { +// return AcquiredPencil.createWithDate(member, content, sharedNoteId, acquiredQuantity, acquiredQuantity, isRead, +// type, createdAt); +// } +// +// } diff --git a/src/test/java/umc/th/juinjang/api/pencilAccount/controller/PencilAccountControllerTest.java b/src/test/java/umc/th/juinjang/api/pencilAccount/controller/PencilAccountControllerTest.java new file mode 100644 index 00000000..75a5af75 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/pencilAccount/controller/PencilAccountControllerTest.java @@ -0,0 +1,50 @@ +package umc.th.juinjang.api.pencilAccount.controller; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import umc.th.juinjang.api.ControllerTestSupport; +import umc.th.juinjang.domain.member.model.Member; + +public class PencilAccountControllerTest extends ControllerTestSupport { + + @DisplayName("내 연필 개수 API 요청이 정상적으로 작동하는 가?") + @Test + void getTotalPencilAmountByMember() throws Exception { + // given + Member mockMember = Member.createKakaoMember( + "test@naver.com", + 1234568L, + "수필씨", + "1.0.0" + ); + + String mockToken = "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.signature"; + Authentication authentication = new TestingAuthenticationToken(mockMember, null, "ROLE_USER"); + + // 서비스 메서드 모킹 + when(pencilAccountService.getTotalPencilAmountByMember(any(Member.class))) + .thenReturn(100L); + + // when & then + mockMvc.perform( + MockMvcRequestBuilders.get("/api/v2/pencil-account/balance") + .with(SecurityMockMvcRequestPostProcessors.authentication(authentication)) + .header("Authorization", mockToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.result.totalBalance").value(100)); + } + +} diff --git a/src/test/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountServiceTest.java b/src/test/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountServiceTest.java new file mode 100644 index 00000000..956511d3 --- /dev/null +++ b/src/test/java/umc/th/juinjang/api/pencilAccount/service/PencilAccountServiceTest.java @@ -0,0 +1,87 @@ +package umc.th.juinjang.api.pencilAccount.service; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import umc.th.juinjang.api.IntegrationTestSupport; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.PencilAccountHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; +import umc.th.juinjang.domain.member.repository.MemberRepository; + +public class PencilAccountServiceTest extends IntegrationTestSupport { + + private final String DEFAULT_EMAIL = "test@naver.com"; + private final String DEFAULT_IMAGE_URL = "https://image.url.com"; + private final String DEFAULT_NICKNAME = "test"; + private final String DEFAULT_INTRODUCTION = ""; + + @Autowired + private PencilAccountService pencilAccountService; + + @Autowired + private MemberRepository memberRepository; + + @DisplayName("보유한 총 연필의 개수를 정상적으로 불러오는 가?") + @Test + void getTotalPencilAmountByMember() { + // given + Member member = createAndSaveMember(); + + // when + long totalBalance = pencilAccountService.getTotalPencilAmountByMember(member); + + // then + assertThat(totalBalance).isEqualTo(0L); + } + + @DisplayName("연필 계좌가 존재하지 않는 경우 에러가 발생하는 가 ?") + @Test + void getTotalPencilAmountByMemberWithoutPencilAccount() { + // given + Member member = createAndSaveMemberWithoutAccount(); + + // when + assertThatThrownBy(() -> pencilAccountService.getTotalPencilAmountByMember(member)) + .isInstanceOf(PencilAccountHandler.class) + .satisfies(exception -> { + PencilAccountHandler pencilException = (PencilAccountHandler)exception; + assertThat(pencilException.getErrorReasonHttpStatus().getMessage()) + .isEqualTo(ErrorStatus.PENCIL_ACCOUNT_NOT_FOUND.getMessage()); + }); + } + + private Member createAndSaveMember() { + Member member = Member.createKakaoMember( + DEFAULT_EMAIL, + 1234567L, + DEFAULT_NICKNAME, + "1.0" + ); + return memberRepository.save(member); + } + + private Member createDefaultMember() { + return Member.builder() + .email(DEFAULT_EMAIL) + .provider(MemberProvider.KAKAO) + .kakaoTargetId(91681234L) + .nickname(DEFAULT_NICKNAME) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now().plusDays(7L)) + .introduction(DEFAULT_INTRODUCTION) + .imageUrl(DEFAULT_IMAGE_URL) + .build(); + } + + private Member createAndSaveMemberWithoutAccount() { + Member member = createDefaultMember(); + return memberRepository.save(member); + } +} diff --git a/src/test/java/umc/th/juinjang/domain/member/model/MemberTest.java b/src/test/java/umc/th/juinjang/domain/member/model/MemberTest.java new file mode 100644 index 00000000..6e2762b7 --- /dev/null +++ b/src/test/java/umc/th/juinjang/domain/member/model/MemberTest.java @@ -0,0 +1,90 @@ +package umc.th.juinjang.domain.member.model; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import jakarta.transaction.Transactional; +import umc.th.juinjang.domain.member.repository.MemberRepository; +import umc.th.juinjang.domain.pencilaccount.model.PencilAccount; +import umc.th.juinjang.domain.pencilaccount.repository.PencilAccountRepository; + +@ActiveProfiles("test") +@SpringBootTest +@Transactional +public class MemberTest { + // 엔티티 테스트 진행 + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private PencilAccountRepository pencilAccountRepository; + + @AfterEach + public void tearDown() { + memberRepository.deleteAll(); + pencilAccountRepository.deleteAll(); + } + + @DisplayName("카카오 회원가입 시, 회원이 정상적으로 저장이 되는 가?") + @Test + void createKakaoMember() { + // given + String email = "test@example.com"; + Long targetId = 12345678L; + String nickname = "테스트유저"; + String agreeVersion = "1.0"; + + // when + Member member = Member.createKakaoMember(email, targetId, nickname, agreeVersion); + memberRepository.saveAndFlush(member); + + // then + Member savedMember = memberRepository.findById(member.getMemberId()).orElseThrow(); + PencilAccount pencilAccount = savedMember.getAccount(); + + assertThat(savedMember).isNotNull(); + assertThat(savedMember) + .extracting(Member::getKakaoTargetId, Member::getEmail, Member::getNickname, Member::getAgreeVersion) + .contains(targetId, email, nickname, agreeVersion); + assertThat(pencilAccount).isNotNull(); + assertThat(pencilAccount) + .extracting(PencilAccount::getAcquiredBalance, PencilAccount::getTotalBalance, + PencilAccount::getPurchasedBalance, PencilAccount::getTotalRefundAmount) + .contains(0L, 0L, 0L, 0L); + } + + @DisplayName("애플 회원가입 시, 회원이 정상적으로 저장이 되는 가?") + @Test + void createAppleMember() { + // given + String email = "test@example.com"; + String sub = "qweqeqws"; + String nickname = "테스트유저"; + String agreeVersion = "1.0"; + + // when + Member member = Member.createAppleMember(email, sub, nickname, agreeVersion); + memberRepository.saveAndFlush(member); + + // then + Member savedMember = memberRepository.findById(member.getMemberId()).orElseThrow(); + PencilAccount pencilAccount = savedMember.getAccount(); + + assertThat(savedMember).isNotNull(); + assertThat(savedMember) + .extracting(Member::getAppleSub, Member::getEmail, Member::getNickname, Member::getAgreeVersion) + .contains(sub, email, nickname, agreeVersion); + assertThat(pencilAccount).isNotNull(); + assertThat(pencilAccount) + .extracting(PencilAccount::getAcquiredBalance, PencilAccount::getTotalBalance, + PencilAccount::getPurchasedBalance, PencilAccount::getTotalRefundAmount) + .contains(0L, 0L, 0L, 0L); + } +} diff --git a/src/test/java/umc/th/juinjang/repository/limjang/LimjangFixture.java b/src/test/java/umc/th/juinjang/repository/limjang/LimjangFixture.java index d9ccc6f3..8f69ef63 100644 --- a/src/test/java/umc/th/juinjang/repository/limjang/LimjangFixture.java +++ b/src/test/java/umc/th/juinjang/repository/limjang/LimjangFixture.java @@ -1,8 +1,8 @@ package umc.th.juinjang.repository.limjang; import java.time.LocalDateTime; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.enums.MemberProvider; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; public class LimjangFixture { diff --git a/src/test/java/umc/th/juinjang/repository/limjang/LimjangQuerydslTest.java b/src/test/java/umc/th/juinjang/repository/limjang/LimjangQuerydslTest.java index 3d344ddf..631d0cd0 100644 --- a/src/test/java/umc/th/juinjang/repository/limjang/LimjangQuerydslTest.java +++ b/src/test/java/umc/th/juinjang/repository/limjang/LimjangQuerydslTest.java @@ -13,11 +13,13 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import umc.th.juinjang.config.TestConfig; -import umc.th.juinjang.model.entity.Limjang; -import umc.th.juinjang.model.entity.Member; -import umc.th.juinjang.model.entity.enums.LimjangPriceType; -import umc.th.juinjang.model.entity.enums.LimjangPropertyType; -import umc.th.juinjang.model.entity.enums.LimjangPurpose; +import umc.th.juinjang.domain.limjang.model.Limjang; +import umc.th.juinjang.domain.limjang.repository.LimjangRepository; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.limjang.model.LimjangPriceType; +import umc.th.juinjang.domain.limjang.model.LimjangPropertyType; +import umc.th.juinjang.domain.limjang.model.LimjangPurpose; +import umc.th.juinjang.domain.member.repository.MemberRepository; @DataJpaTest @ActiveProfiles("test") @@ -42,27 +44,27 @@ void setUp() { memberRepository.save(member); } - @Test - @DisplayName("키워드를 전달하면 멤버가 소유한 게시글 중 닉네임, 주소, 상세주소 컬럼중 하나라도 키워드를 포함하는 게시글을 리턴한다.") - void testIncludeKeyword() { - - // given - Limjang limjang1 = createLimjang(member, "경기도 구리시 인창동", "삼성아파트", "우리 집"); - Limjang limjang2 = createLimjang(member, "경기도 구리시", "인창", "우리 집"); - Limjang limjang3 = createLimjang(member, "경기도 구리시", "어쩌구", "인창"); - limjangRepository.saveAll(List.of(limjang1, limjang2, limjang3)); - - // when - String keyword = "인창"; - List findLimjangs = limjangRepository.searchLimjangsWhereDeletedIsFalse(member, keyword); - // then - - for (int i = 0; i < findLimjangs.size(); i++) { - System.out.println(findLimjangs.get(i).getLimjangId()); - } - assertThat(findLimjangs) - .hasSize(3); - } + // @Test + // @DisplayName("키워드를 전달하면 멤버가 소유한 게시글 중 닉네임, 주소, 상세주소 컬럼중 하나라도 키워드를 포함하는 게시글을 리턴한다.") + // void testIncludeKeyword() { + // + // // given + // Limjang limjang1 = createLimjang(member, "경기도 구리시 인창동", "삼성아파트", "우리 집"); + // Limjang limjang2 = createLimjang(member, "경기도 구리시", "인창", "우리 집"); + // Limjang limjang3 = createLimjang(member, "경기도 구리시", "어쩌구", "인창"); + // limjangRepository.saveAll(List.of(limjang1, limjang2, limjang3)); + // + // // when + // String keyword = "인창"; + // List findLimjangs = limjangRepository.searchLimjangsWhereDeletedIsFalse(member, keyword); + // // then + // + // for (int i = 0; i < findLimjangs.size(); i++) { + // System.out.println(findLimjangs.get(i).getLimjangId()); + // } + // assertThat(findLimjangs) + // .hasSize(3); + // } @Test @DisplayName("주소, 상세주소, 닉네임 중 하나라도 키워드를 포함하지 않는 게시글은 검색에 걸리지 않음") diff --git a/src/test/java/umc/th/juinjang/testutil/fixture/MemberFixture.java b/src/test/java/umc/th/juinjang/testutil/fixture/MemberFixture.java new file mode 100644 index 00000000..b7ad9d1c --- /dev/null +++ b/src/test/java/umc/th/juinjang/testutil/fixture/MemberFixture.java @@ -0,0 +1,54 @@ +package umc.th.juinjang.testutil.fixture; + +import java.time.LocalDateTime; + +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.member.model.MemberProvider; + +public class MemberFixture { + + public static final String DEFAULT_EMAIL = "test@naver.com"; + public static final String DEFAULT_IMAGE_URL = "https://image.url.com"; + public static final String DEFAULT_NICKNAME = "test"; + public static final String DEFAULT_INTRODUCTION = ""; + public static final Long DEFAULT_KAKAO_ID = 91681234L; + + public static Member createDefaultMember() { + return createDefaultMemberBuilder().build(); + } + + public static Member.MemberBuilder createDefaultMemberBuilder() { + return Member.builder() + .email(DEFAULT_EMAIL) + .provider(MemberProvider.KAKAO) + .kakaoTargetId(DEFAULT_KAKAO_ID) + .nickname(DEFAULT_NICKNAME) + .refreshToken("") + .refreshTokenExpiresAt(LocalDateTime.now().plusDays(7L)) + .introduction(DEFAULT_INTRODUCTION) + .imageUrl(DEFAULT_IMAGE_URL); + } + + public static Member createMemberWithParams( + String email, + Long kakaoTargetId, + String nickname, + String introduction, + String imageUrl) { + + Member.MemberBuilder builder = createDefaultMemberBuilder(); + + if (email != null) + builder.email(email); + if (kakaoTargetId != null) + builder.kakaoTargetId(kakaoTargetId); + if (nickname != null) + builder.nickname(nickname); + if (introduction != null) + builder.introduction(introduction); + if (imageUrl != null) + builder.imageUrl(imageUrl); + + return builder.build(); + } +}