diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b1bc83f2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +# Ignore Git-related files +.git +.gitignore + +# Ignore local IDE files +.idea/ +*.iml +*.log + +# Ignore build directories +build/ +out/ +target/ + +# Ignore Docker-related files +Dockerfile +docker-compose.yml + +# Ignore other unnecessary files/directories +*.md +*.tmp +*.bak + +# Ignore specific directories +cloud/ +config/ +images/ +infra/ +jenkins/ +monitoring/ +nginx/ diff --git a/build.gradle b/build.gradle index 1c46a08c3..081fbf557 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.3.4' + id 'org.springframework.boot' version '3.3.6' id 'io.spring.dependency-management' version '1.1.6' } @@ -15,6 +15,10 @@ bootJar { archivesBaseName = "clab" archiveFileName = "clab.jar" archiveVersion = "1.0.0" + + layered { + enabled = true + } } java { @@ -45,8 +49,8 @@ dependencies { // Monitoring implementation 'org.springframework.boot:spring-boot-starter-actuator' // Spring Boot Actuator implementation 'io.micrometer:micrometer-registry-prometheus' // Prometheus - implementation 'ch.qos.logback:logback-classic:1.5.8' // Logback - implementation 'ch.qos.logback:logback-core:1.5.8' // Logback + implementation 'ch.qos.logback:logback-classic:1.5.12' // Logback + implementation 'ch.qos.logback:logback-core:1.5.12' // Logback // DB implementation 'org.postgresql:postgresql:42.7.4' // PostgreSQL JDBC Driver @@ -67,7 +71,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' // 롬복 implementation 'com.google.code.gson:gson:2.11.0' // JSON 라이브러리 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' // Swagger - implementation 'commons-io:commons-io:2.17.0' // Apache Commons IO + implementation 'commons-io:commons-io:2.18.0' // Apache Commons IO implementation 'com.google.guava:guava:33.3.1-jre' // Google Core Libraries For Java implementation 'org.springframework.boot:spring-boot-starter-mail' // Spring Mail implementation 'com.google.zxing:core:3.4.1' // QR 코드 @@ -75,8 +79,8 @@ dependencies { implementation 'org.apache.commons:commons-lang3:3.17.0' // Apache Commons Lang // MapStruct - implementation 'org.mapstruct:mapstruct:1.6.2' - annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.2' + implementation 'org.mapstruct:mapstruct:1.6.3' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3' annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' // IPInfo @@ -84,9 +88,9 @@ dependencies { implementation 'io.ipinfo:ipinfo-api:3.0.0' // IPInfo API // Slack - implementation 'com.slack.api:slack-api-model:1.43.1' - implementation 'com.slack.api:slack-api-client:1.43.1' - implementation 'com.slack.api:slack-app-backend:1.43.1' + implementation 'com.slack.api:slack-api-model:1.44.2' + implementation 'com.slack.api:slack-api-client:1.44.2' + implementation 'com.slack.api:slack-app-backend:1.44.2' // XSS Filter implementation 'com.navercorp.lucy:lucy-xss-servlet:2.0.1' // Lucy XSS Servlet Filter @@ -94,12 +98,12 @@ dependencies { implementation 'org.apache.commons:commons-text:1.11.0' // Apache Commons Text // Image - implementation 'commons-io:commons-io:2.17.0' + implementation 'commons-io:commons-io:2.18.0' implementation 'com.drewnoakes:metadata-extractor:2.19.0' implementation 'org.imgscalr:imgscalr-lib:4.2' // Emoji - implementation 'com.ibm.icu:icu4j:75.1' + implementation 'com.ibm.icu:icu4j:76.1' // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' // Spring Boot Test @@ -113,7 +117,7 @@ tasks.named('test') { def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile sourceSets { - main.java.srcDirs += [ querydslDir ] + main.java.srcDirs += [querydslDir] } tasks.withType(JavaCompile).configureEach { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded5c..4eaec4670 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jenkins/prod/Dockerfile b/jenkins/prod/Dockerfile index 054f01fbf..aaa3a9dde 100644 --- a/jenkins/prod/Dockerfile +++ b/jenkins/prod/Dockerfile @@ -1,12 +1,32 @@ -# Use the official OpenJDK 21 image from the Docker Hub -FROM openjdk:21-jdk +# 1. Build Stage +FROM gradle:8.11.1-jdk21 AS build +WORKDIR /app -# Expose port 8080 to the outside world -EXPOSE 8080 +# Copy Gradle files and install dependencies +COPY build.gradle settings.gradle /app/ +RUN gradle dependencies --stacktrace -# Copy the JAR file into the container -COPY build/libs/clab.jar /clab.jar +# Copy source code and build +COPY src /app/src +RUN gradle bootJar --no-daemon --stacktrace -# Set the default active profile to 'stage'. Modify the 'spring.profiles.active' property to match your environment. -# For example, use '-Dspring.profiles.active=production' for production environment. -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/clab.jar"] +# Extract layers from JAR file +RUN java -Djarmode=layertools -jar build/libs/*.jar extract \ + && ls -l /app \ + && ls -l /app/dependencies \ + && ls -l /app/spring-boot-loader \ + && ls -l /app/snapshot-dependencies \ + && ls -l /app/application + +# 2. Runtime Stage +FROM eclipse-temurin:21-jre AS runtime +WORKDIR /app + +# Copy each layer +COPY --from=build /app/dependencies/ ./ +COPY --from=build /app/spring-boot-loader/ ./ +COPY --from=build /app/snapshot-dependencies/ ./ +COPY --from=build /app/application/ ./ + +# Run the application +ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "org.springframework.boot.loader.launch.JarLauncher"] diff --git a/jenkins/prod/Jenkinsfile b/jenkins/prod/Jenkinsfile index 49ae770ab..9f0513946 100644 --- a/jenkins/prod/Jenkinsfile +++ b/jenkins/prod/Jenkinsfile @@ -18,7 +18,6 @@ */ def FAILED_STAGE = "" -def BACKUP_FILE = "" pipeline { agent any @@ -35,110 +34,48 @@ pipeline { } withCredentials([file(credentialsId: 'members_prod_config_yml', variable: 'CONFIG_FILE')]) { script { - def config = readYaml(file: env.CONFIG_FILE) - - env.JENKINS_DOMAIN = config.'jenkins-domain' - env.SLACK_WEBHOOK_URL = config.slack.'webhook-url' - env.SLACK_COLOR_SUCCESS = config.slack.'color-success' - env.SLACK_COLOR_FAILURE = config.slack.'color-failure' - - env.PG_USER = config.postgresql.user - env.PG_PASSWORD = config.postgresql.password - env.BACKUP_DIR = config.postgresql.'backup-dir' - - env.STAGING_USER = config.staging.'user' - env.STAGING_HOST = config.staging.'host' - env.STAGING_BACKUP_DIR_PATH = config.staging.'backup-dir-path' - env.STAGING_RESTORE_BACKUP_SCRIPT_PATH = config.staging.'restore-backup-script-path' - env.STAGING_SSH_PORT = config.staging.'ssh-port' - env.STAGING_PG_USER = config.staging.'postgresql-user' - - env.DOCKER_HUB_REPO = config.dockerhub.repo - env.DOCKER_HUB_USER = config.dockerhub.user - env.DOCKER_HUB_PASSWORD = config.dockerhub.password - - env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path' - env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path' - env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path' - - env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path' - env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path' - env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path' - - env.BLUE_CONTAINER = config.containers.blue - env.GREEN_CONTAINER = config.containers.green - env.BLUE_URL = config.containers.'blue-url' - env.GREEN_URL = config.containers.'green-url' - env.IMAGE_NAME = config.containers.'image-name' - - env.APPLICATION_NETWORK = config.networks.application - env.MONITORING_NETWORK = config.networks.monitoring - - env.PROFILE = config.spring.profile - env.PORT_A = config.spring.'port-a'.toString() - env.PORT_B = config.spring.'port-b'.toString() - - env.WHITELIST_ADMIN_USERNAME = config.admin.username - env.WHITELIST_ADMIN_PASSWORD = config.admin.password - - env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}" - env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name' - env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name' + loadEnvironmentVariables(env.CONFIG_FILE) } } } } - stage('Check Java Version') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - } - sh 'java -version' - } - } - - stage('Get Git Change Log') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - env.GIT_CHANGELOG = getChangeLog() - } - } - } - - stage('PostgreSQL Backup') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - BACKUP_FILE = backupPostgres() + stage('Concurrent Pre-Build Steps') { + parallel { + stage('Get Git Change Log') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + env.GIT_CHANGELOG = getChangeLog() + } + } } - } - } - stage('Docker Hub Login') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - dockerLogin() + stage('PostgreSQL Backup') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + backupPostgres() + } + } } - } - } - stage('Determine Containers') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - determineContainers() + stage('Docker Hub Login') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + dockerLogin() + } + } } - } - } - stage('Build Application') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - buildApplication() + stage('Determine Containers') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + determineContainers() + } + } } } } @@ -170,29 +107,33 @@ pipeline { } } - stage('Transfer Backup to Staging') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - transferBackupToStaging(BACKUP_FILE) + stage('Backup Transfer and Deployment') { + parallel { + stage('Transfer Backup to Staging') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + transferBackupToStaging(BACKUP_FILE) + } + } } - } - } - stage('Restore Backup on Staging') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - restoreBackupOnStaging(BACKUP_FILE) + stage('Restore Backup on Staging') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + restoreBackupOnStaging(BACKUP_FILE) + } + } } - } - } - stage('Switch Traffic and Cleanup') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - switchTrafficAndCleanup() + stage('Switch Traffic and Cleanup') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + switchTrafficAndCleanup() + } + } } } } @@ -217,7 +158,14 @@ def sendSlackBuildNotification(String message, String color) { def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}" def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console" - def payload = [ + def payload = createSlackPayload(message, color, jobUrl, consoleOutputUrl) + def payloadJson = groovy.json.JsonOutput.toJson(payload) + + sendHttpPostRequest(env.SLACK_WEBHOOK_URL, payloadJson) +} + +def createSlackPayload(String message, String color, String jobUrl, String consoleOutputUrl) { + return [ blocks: [ [ type: "section", @@ -263,17 +211,74 @@ def sendSlackBuildNotification(String message, String color) { ] ] ] - ].findAll { it != null } + ] ] ] ] +} - withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) { - def payloadJson = groovy.json.JsonOutput.toJson(payload) - sh """ - curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL} - """ - } +def sendHttpPostRequest(String url, String payload) { + def CONTENT_TYPE_JSON = 'application/json' + def HTTP_POST = 'POST' + + sh """ + curl -X ${HTTP_POST} \\ + -H 'Content-type: ${CONTENT_TYPE_JSON}' \\ + --data '${payload}' \\ + ${url} + """ +} + +def loadEnvironmentVariables(String configFile) { + def config = readYaml(file: configFile) + + env.JENKINS_DOMAIN = config.'jenkins-domain' + env.SLACK_WEBHOOK_URL = config.slack.'webhook-url' + env.SLACK_COLOR_SUCCESS = config.slack.'color-success' + env.SLACK_COLOR_FAILURE = config.slack.'color-failure' + + env.PG_USER = config.postgresql.user + env.PG_PASSWORD = config.postgresql.password + env.BACKUP_DIR = config.postgresql.'backup-dir' + + env.STAGING_USER = config.staging.'user' + env.STAGING_HOST = config.staging.'host' + env.STAGING_BACKUP_DIR_PATH = config.staging.'backup-dir-path' + env.STAGING_RESTORE_BACKUP_SCRIPT_PATH = config.staging.'restore-backup-script-path' + env.STAGING_SSH_PORT = config.staging.'ssh-port' + env.STAGING_PG_USER = config.staging.'postgresql-user' + + env.DOCKER_HUB_REPO = config.dockerhub.repo + env.DOCKER_HUB_USER = config.dockerhub.user + env.DOCKER_HUB_PASSWORD = config.dockerhub.password + + env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path' + env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path' + env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path' + + env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path' + env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path' + env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path' + + env.BLUE_CONTAINER = config.containers.blue + env.GREEN_CONTAINER = config.containers.green + env.BLUE_URL = config.containers.'blue-url' + env.GREEN_URL = config.containers.'green-url' + env.IMAGE_NAME = config.containers.'image-name' + + env.APPLICATION_NETWORK = config.networks.application + env.MONITORING_NETWORK = config.networks.monitoring + + env.PROFILE = config.spring.profile + env.PORT_A = config.spring.'port-a'.toString() + env.PORT_B = config.spring.'port-b'.toString() + + env.WHITELIST_ADMIN_USERNAME = config.admin.username + env.WHITELIST_ADMIN_PASSWORD = config.admin.password + + env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}" + env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name' + env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name' } def getChangeLog() { @@ -295,163 +300,106 @@ def getChangeLog() { def backupPostgres() { def BACKUP_FILE = "postgres_backup_${new Date().format('yyyy-MM-dd_HH-mm-ss')}.sql" - withEnv([ - "BACKUP_DIR=${env.BACKUP_DIR}", - "POSTGRESQL_CONTAINER_NAME=${env.POSTGRESQL_CONTAINER_NAME}", - "PG_PASSWORD=${env.PG_PASSWORD}", - "PG_USER=${env.PG_USER}" - ]) { - sh """ - echo "Backing up PostgreSQL database to ${BACKUP_DIR}/${BACKUP_FILE}..." - echo "Executing as user: \$(whoami)" - docker exec -e PGPASSWORD=${PG_PASSWORD} ${POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${PG_USER} > ${BACKUP_DIR}/${BACKUP_FILE}' - """ - } - return BACKUP_FILE + sh """ + echo "Backing up PostgreSQL database to ${env.BACKUP_DIR}/${BACKUP_FILE}..." + docker exec -e PGPASSWORD=${env.PG_PASSWORD} ${env.POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${env.PG_USER} > ${env.BACKUP_DIR}/${BACKUP_FILE}' + """ } def dockerLogin() { - withEnv(["DOCKER_HUB_PASSWORD=${env.DOCKER_HUB_PASSWORD}", "DOCKER_HUB_USER=${env.DOCKER_HUB_USER}"]) { - sh """ - echo "Logging in to Docker Hub..." - echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USER} --password-stdin - """ - } + sh """ + echo "Logging in to Docker Hub..." + echo "${env.DOCKER_HUB_PASSWORD}" | docker login -u "${env.DOCKER_HUB_USER}" --password-stdin + """ } def determineContainers() { script { - withEnv([ - "BLUE_CONTAINER=${env.BLUE_CONTAINER}", - "GREEN_CONTAINER=${env.GREEN_CONTAINER}", - "BLUE_URL=${env.BLUE_URL}", - "GREEN_URL=${env.GREEN_URL}", - "PORT_A=${env.PORT_A}", - "PORT_B=${env.PORT_B}" - ]) { - def blueRunning = sh(script: "docker ps --filter 'name=${BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${BLUE_CONTAINER}'", returnStatus: true) == 0 - if (blueRunning) { - env.CURRENT_CONTAINER = BLUE_CONTAINER - env.DEPLOY_CONTAINER = GREEN_CONTAINER - env.NEW_TARGET = GREEN_URL - env.NEW_PORT = PORT_B - env.OLD_PORT = PORT_A - } else { - env.CURRENT_CONTAINER = GREEN_CONTAINER - env.DEPLOY_CONTAINER = BLUE_CONTAINER - env.NEW_TARGET = BLUE_URL - env.NEW_PORT = PORT_A - env.OLD_PORT = PORT_B - } - echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." + def blueRunning = sh(script: "docker ps --filter 'name=${env.BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${env.BLUE_CONTAINER}'", returnStatus: true) == 0 + if (blueRunning) { + env.CURRENT_CONTAINER = env.BLUE_CONTAINER + env.DEPLOY_CONTAINER = env.GREEN_CONTAINER + env.NEW_TARGET = env.GREEN_URL + env.NEW_PORT = env.PORT_B + env.OLD_PORT = env.PORT_A + } else { + env.CURRENT_CONTAINER = env.GREEN_CONTAINER + env.DEPLOY_CONTAINER = env.BLUE_CONTAINER + env.NEW_TARGET = env.BLUE_URL + env.NEW_PORT = env.PORT_A + env.OLD_PORT = env.PORT_B } - } -} - -def buildApplication() { - withEnv([ - "PROFILE=${env.PROFILE}" - ]) { - sh """ - echo "Building application with profile ${PROFILE}..." - ./gradlew clean build -Penv=${PROFILE} --stacktrace --info - """ + echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." } } def buildAndPushDockerImage() { - withEnv([ - "DOCKER_HUB_REPO=${env.DOCKER_HUB_REPO}", - "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", - "DOCKERFILE_PATH=${env.DOCKERFILE_PATH}", - "IMAGE_NAME=${env.IMAGE_NAME}" - ]) { - sh """ - docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${DEPLOY_CONTAINER} . - docker tag ${IMAGE_NAME}:${DEPLOY_CONTAINER} ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} - docker push ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} - """ - } + sh """ + DOCKER_BUILDKIT=1 docker build -f ${env.DOCKERFILE_PATH} -t ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} . + docker tag ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER} + docker push ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER} + docker logout + """ } def deployNewInstance() { - withEnv([ - "PROFILE=${env.PROFILE}", - "NEW_PORT=${env.NEW_PORT}", - "APPLICATION_NETWORK=${env.APPLICATION_NETWORK}", - "MONITORING_NETWORK=${env.MONITORING_NETWORK}", - "EXTERNAL_SERVER_CONFIG_PATH=${env.EXTERNAL_SERVER_CONFIG_PATH}", - "EXTERNAL_SERVER_CLOUD_PATH=${env.EXTERNAL_SERVER_CLOUD_PATH}", - "EXTERNAL_SERVER_LOGS_PATH=${env.EXTERNAL_SERVER_LOGS_PATH}", - "INTERNAL_SERVER_CONFIG_PATH=${env.INTERNAL_SERVER_CONFIG_PATH}", - "INTERNAL_SERVER_CLOUD_PATH=${env.INTERNAL_SERVER_CLOUD_PATH}", - "INTERNAL_SERVER_LOGS_PATH=${env.INTERNAL_SERVER_LOGS_PATH}", - "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", - "IMAGE_NAME=${env.IMAGE_NAME}" - ]) { - sh """ - echo "Stopping and removing existing container if it exists" - if docker ps | grep -q ${DEPLOY_CONTAINER}; then - docker stop ${DEPLOY_CONTAINER} - docker rm ${DEPLOY_CONTAINER} - fi - - echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}" - docker run -d --name ${DEPLOY_CONTAINER} \\ - -p ${NEW_PORT}:8080 \\ - --network ${APPLICATION_NETWORK} \\ - -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\ - -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\ - -v ${EXTERNAL_SERVER_LOGS_PATH}:${INTERNAL_SERVER_LOGS_PATH} \\ - -e LOG_PATH=${INTERNAL_SERVER_LOGS_PATH} \\ - -e SPRING_PROFILES_ACTIVE=${PROFILE} \\ - ${IMAGE_NAME}:${DEPLOY_CONTAINER} - - echo "Checking if monitoring network ${MONITORING_NETWORK} exists" - if docker network ls --format '{{.Name}}' | grep -q '^${MONITORING_NETWORK}\$'; then - echo "Connecting to monitoring network ${MONITORING_NETWORK}" - docker network connect ${MONITORING_NETWORK} ${DEPLOY_CONTAINER} - else - echo "Monitoring network ${MONITORING_NETWORK} does not exist. Skipping connection." - fi - - echo "Listing all containers" - docker ps -a - """ - } + sh """ + echo "Stopping and removing existing container if it exists" + if docker ps | grep -q ${env.DEPLOY_CONTAINER}; then + docker stop ${env.DEPLOY_CONTAINER} + docker rm ${env.DEPLOY_CONTAINER} + fi + + echo "Running new container ${env.DEPLOY_CONTAINER} with image ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER}" + docker run -d --name ${env.DEPLOY_CONTAINER} \\ + -p ${env.NEW_PORT}:8080 \\ + --network ${env.APPLICATION_NETWORK} \\ + -v ${env.EXTERNAL_SERVER_CONFIG_PATH}:${env.INTERNAL_SERVER_CONFIG_PATH} \\ + -v ${env.EXTERNAL_SERVER_CLOUD_PATH}:${env.INTERNAL_SERVER_CLOUD_PATH} \\ + -v ${env.EXTERNAL_SERVER_LOGS_PATH}:${env.INTERNAL_SERVER_LOGS_PATH} \\ + -e LOG_PATH=${env.INTERNAL_SERVER_LOGS_PATH} \\ + -e SPRING_PROFILES_ACTIVE=${env.PROFILE} \\ + ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} + + echo "Checking if monitoring network ${env.MONITORING_NETWORK} exists" + if docker network ls --format '{{.Name}}' | grep -q '^${env.MONITORING_NETWORK}\$'; then + echo "Connecting to monitoring network ${env.MONITORING_NETWORK}" + docker network connect ${env.MONITORING_NETWORK} ${env.DEPLOY_CONTAINER} + else + echo "Monitoring network ${env.MONITORING_NETWORK} does not exist. Skipping connection." + fi + + echo "Listing all containers" + docker ps -a + """ } def performHealthCheck() { - withEnv([ - "WHITELIST_ADMIN_USERNAME=${env.WHITELIST_ADMIN_USERNAME}", - "WHITELIST_ADMIN_PASSWORD=${env.WHITELIST_ADMIN_PASSWORD}" - ]) { - def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() - echo "Public IP address: ${PUBLIC_IP}" - - def start_time = System.currentTimeMillis() - def timeout = start_time + 150000 // 2.5 minutes - - while (System.currentTimeMillis() < timeout) { - def elapsed = (System.currentTimeMillis() - start_time) / 1000 - echo "Checking health... ${elapsed} seconds elapsed." - def status = sh( - script: """curl -s -u ${WHITELIST_ADMIN_USERNAME}:${WHITELIST_ADMIN_PASSWORD} \ - http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", - returnStatus: true - ) - if (status == 0) { - echo "New application started successfully after ${elapsed} seconds." - return - } - sleep 5 + def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() + echo "Public IP address: ${PUBLIC_IP}" + + def start_time = System.currentTimeMillis() + def timeout = start_time + 150000 // 2.5 minutes + + while (System.currentTimeMillis() < timeout) { + def elapsed = (System.currentTimeMillis() - start_time) / 1000 + echo "Checking health... ${elapsed} seconds elapsed." + def status = sh( + script: """curl -s -u ${env.WHITELIST_ADMIN_USERNAME}:${env.WHITELIST_ADMIN_PASSWORD} \ + http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", + returnStatus: true + ) + if (status == 0) { + echo "New application started successfully after ${elapsed} seconds." + return } + sleep 5 + } - if (System.currentTimeMillis() >= timeout) { - sh "docker stop ${env.DEPLOY_CONTAINER}" - sh "docker rm ${env.DEPLOY_CONTAINER}" - error "Health check failed" - } + if (System.currentTimeMillis() >= timeout) { + sh "docker stop ${env.DEPLOY_CONTAINER}" + sh "docker rm ${env.DEPLOY_CONTAINER}" + error "Health check failed" } } @@ -471,30 +419,21 @@ def restoreBackupOnStaging(String BACKUP_FILE) { } def switchTrafficAndCleanup() { - withEnv([ - "NEW_PORT=${env.NEW_PORT}", - "OLD_PORT=${env.OLD_PORT}", - "NEW_TARGET=${env.NEW_TARGET}", - "CURRENT_CONTAINER=${env.CURRENT_CONTAINER}", - "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", - "NGINX_CONTAINER_NAME=${env.NGINX_CONTAINER_NAME}" - ]) { - sh """ - echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}." - docker exec ${NGINX_CONTAINER_NAME} bash -c ' - export BACKEND_URL=${NEW_TARGET} - envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf - ' - docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/default.conf - docker exec ${NGINX_CONTAINER_NAME} nginx -t - docker exec ${NGINX_CONTAINER_NAME} nginx -s reload - - echo "Checking if current container ${CURRENT_CONTAINER} is running..." - if docker ps | grep -q ${CURRENT_CONTAINER}; then - docker stop ${CURRENT_CONTAINER} - docker rm ${CURRENT_CONTAINER} - echo "Removed old container ${CURRENT_CONTAINER}." - fi - """ - } + sh """ + echo "Switching traffic to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." + docker exec ${env.NGINX_CONTAINER_NAME} bash -c ' + export BACKEND_URL=${env.NEW_TARGET} + envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/members.conf.template > /etc/nginx/conf.d/members.conf + ' + docker exec ${env.NGINX_CONTAINER_NAME} sed -i 's/${env.OLD_PORT}/${env.NEW_PORT}/' /etc/nginx/conf.d/members.conf + docker exec ${env.NGINX_CONTAINER_NAME} nginx -t + docker exec ${env.NGINX_CONTAINER_NAME} nginx -s reload + + echo "Checking if current container ${env.CURRENT_CONTAINER} is running..." + if docker ps | grep -q ${env.CURRENT_CONTAINER}; then + docker stop ${env.CURRENT_CONTAINER} + docker rm ${env.CURRENT_CONTAINER} + echo "Removed old container ${env.CURRENT_CONTAINER}." + fi + """ } diff --git a/jenkins/stage/Dockerfile b/jenkins/stage/Dockerfile index bf0236339..b7f76d4a6 100644 --- a/jenkins/stage/Dockerfile +++ b/jenkins/stage/Dockerfile @@ -1,12 +1,32 @@ -# Use the official OpenJDK 21 image from the Docker Hub -FROM openjdk:21-jdk +# 1. Build Stage +FROM gradle:8.11.1-jdk21 AS build +WORKDIR /app -# Expose port 8080 to the outside world -EXPOSE 8080 +# Copy Gradle files and install dependencies +COPY build.gradle settings.gradle /app/ +RUN gradle dependencies --stacktrace -# Copy the JAR file into the container -COPY build/libs/clab.jar /clab.jar +# Copy source code and build +COPY src /app/src +RUN gradle bootJar --no-daemon --build-cache --stacktrace -# Set the default active profile to 'stage'. Modify the 'spring.profiles.active' property to match your environment. -# For example, use '-Dspring.profiles.active=production' for production environment. -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=stage", "/clab.jar"] +# Extract layers from JAR file +RUN java -Djarmode=layertools -jar build/libs/*.jar extract \ + && ls -l /app \ + && ls -l /app/dependencies \ + && ls -l /app/spring-boot-loader \ + && ls -l /app/snapshot-dependencies \ + && ls -l /app/application + +# 2. Runtime Stage +FROM eclipse-temurin:21-jre AS runtime +WORKDIR /app + +# Copy each layer +COPY --from=build /app/dependencies/ ./ +COPY --from=build /app/spring-boot-loader/ ./ +COPY --from=build /app/snapshot-dependencies/ ./ +COPY --from=build /app/application/ ./ + +# Run the application +ENTRYPOINT ["java", "-Dspring.profiles.active=stage", "org.springframework.boot.loader.launch.JarLauncher"] diff --git a/jenkins/stage/Jenkinsfile b/jenkins/stage/Jenkinsfile index 00d96cce1..d22b26d02 100644 --- a/jenkins/stage/Jenkinsfile +++ b/jenkins/stage/Jenkinsfile @@ -34,103 +34,48 @@ pipeline { } withCredentials([file(credentialsId: 'members_stage_config_yml', variable: 'CONFIG_FILE')]) { script { - def config = readYaml(file: env.CONFIG_FILE) - - env.JENKINS_DOMAIN = config.'jenkins-domain' - env.SLACK_WEBHOOK_URL = config.slack.'webhook-url' - env.SLACK_COLOR_SUCCESS = config.slack.'color-success' - env.SLACK_COLOR_FAILURE = config.slack.'color-failure' - - env.PG_USER = config.postgresql.user - env.PG_PASSWORD = config.postgresql.password - env.BACKUP_DIR = config.postgresql.'backup-dir' - - env.DOCKER_HUB_REPO = config.dockerhub.repo - env.DOCKER_HUB_USER = config.dockerhub.user - env.DOCKER_HUB_PASSWORD = config.dockerhub.password - - env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path' - env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path' - env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path' - - env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path' - env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path' - env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path' - - env.BLUE_CONTAINER = config.containers.blue - env.GREEN_CONTAINER = config.containers.green - env.BLUE_URL = config.containers.'blue-url' - env.GREEN_URL = config.containers.'green-url' - env.IMAGE_NAME = config.containers.'image-name' - - env.APPLICATION_NETWORK = config.networks.application - env.MONITORING_NETWORK = config.networks.monitoring - - env.PROFILE = config.spring.profile - env.PORT_A = config.spring.'port-a'.toString() - env.PORT_B = config.spring.'port-b'.toString() - - env.WHITELIST_ADMIN_USERNAME = config.admin.username - env.WHITELIST_ADMIN_PASSWORD = config.admin.password - - env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}" - env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name' - env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name' + loadEnvironmentVariables(env.CONFIG_FILE) } } } } - stage('Check Java Version') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - } - sh 'java -version' - } - } - - stage('Get Git Change Log') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - env.GIT_CHANGELOG = getChangeLog() - } - } - } - - stage('PostgreSQL Backup') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - backupPostgres() + stage('Concurrent Pre-Build Steps') { + parallel { + stage('Get Git Change Log') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + env.GIT_CHANGELOG = getChangeLog() + } + } } - } - } - stage('Docker Hub Login') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - dockerLogin() + stage('PostgreSQL Backup') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + backupPostgres() + } + } } - } - } - stage('Determine Containers') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - determineContainers() + stage('Docker Hub Login') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + dockerLogin() + } + } } - } - } - stage('Build Application') { - steps { - script { - FAILED_STAGE = env.STAGE_NAME - buildApplication() + stage('Determine Containers') { + steps { + script { + FAILED_STAGE = env.STAGE_NAME + determineContainers() + } + } } } } @@ -191,7 +136,14 @@ def sendSlackBuildNotification(String message, String color) { def jobUrl = "${env.JENKINS_DOMAIN}/job/${env.JOB_NAME}" def consoleOutputUrl = "${jobUrl}/${env.BUILD_NUMBER}/console" - def payload = [ + def payload = createSlackPayload(message, color, jobUrl, consoleOutputUrl) + def payloadJson = groovy.json.JsonOutput.toJson(payload) + + sendHttpPostRequest(env.SLACK_WEBHOOK_URL, payloadJson) +} + +def createSlackPayload(String message, String color, String jobUrl, String consoleOutputUrl) { + return [ blocks: [ [ type: "section", @@ -237,17 +189,67 @@ def sendSlackBuildNotification(String message, String color) { ] ] ] - ].findAll { it != null } + ] ] ] ] +} - withEnv(["SLACK_WEBHOOK_URL=${env.SLACK_WEBHOOK_URL}"]) { - def payloadJson = groovy.json.JsonOutput.toJson(payload) - sh """ - curl -X POST -H 'Content-type: application/json' --data '${payloadJson}' ${SLACK_WEBHOOK_URL} - """ - } +def sendHttpPostRequest(String url, String payload) { + def CONTENT_TYPE_JSON = 'application/json' + def HTTP_POST = 'POST' + + sh """ + curl -X ${HTTP_POST} \\ + -H 'Content-type: ${CONTENT_TYPE_JSON}' \\ + --data '${payload}' \\ + ${url} + """ +} + +def loadEnvironmentVariables(String configFile) { + def config = readYaml(file: configFile) + + env.JENKINS_DOMAIN = config.'jenkins-domain' + env.SLACK_WEBHOOK_URL = config.slack.'webhook-url' + env.SLACK_COLOR_SUCCESS = config.slack.'color-success' + env.SLACK_COLOR_FAILURE = config.slack.'color-failure' + + env.PG_USER = config.postgresql.user + env.PG_PASSWORD = config.postgresql.password + env.BACKUP_DIR = config.postgresql.'backup-dir' + + env.DOCKER_HUB_REPO = config.dockerhub.repo + env.DOCKER_HUB_USER = config.dockerhub.user + env.DOCKER_HUB_PASSWORD = config.dockerhub.password + + env.EXTERNAL_SERVER_CONFIG_PATH = config.'external-server'.'config-path' + env.EXTERNAL_SERVER_CLOUD_PATH = config.'external-server'.'cloud-path' + env.EXTERNAL_SERVER_LOGS_PATH = config.'external-server'.'logs-path' + + env.INTERNAL_SERVER_CONFIG_PATH = config.'internal-server'.'config-path' + env.INTERNAL_SERVER_CLOUD_PATH = config.'internal-server'.'cloud-path' + env.INTERNAL_SERVER_LOGS_PATH = config.'internal-server'.'logs-path' + + env.BLUE_CONTAINER = config.containers.blue + env.GREEN_CONTAINER = config.containers.green + env.BLUE_URL = config.containers.'blue-url' + env.GREEN_URL = config.containers.'green-url' + env.IMAGE_NAME = config.containers.'image-name' + + env.APPLICATION_NETWORK = config.networks.application + env.MONITORING_NETWORK = config.networks.monitoring + + env.PROFILE = config.spring.profile + env.PORT_A = config.spring.'port-a'.toString() + env.PORT_B = config.spring.'port-b'.toString() + + env.WHITELIST_ADMIN_USERNAME = config.admin.username + env.WHITELIST_ADMIN_PASSWORD = config.admin.password + + env.DOCKERFILE_PATH = "${env.WORKSPACE}${config.docker.'dockerfile-path'}" + env.NGINX_CONTAINER_NAME = config.docker.'nginx-container-name' + env.POSTGRESQL_CONTAINER_NAME = config.docker.'postgresql-container-name' } def getChangeLog() { @@ -269,189 +271,125 @@ def getChangeLog() { def backupPostgres() { def BACKUP_FILE = "postgres_backup_${new Date().format('yyyy-MM-dd_HH-mm-ss')}.sql" - withEnv([ - "BACKUP_DIR=${env.BACKUP_DIR}", - "POSTGRESQL_CONTAINER_NAME=${env.POSTGRESQL_CONTAINER_NAME}", - "PG_PASSWORD=${env.PG_PASSWORD}", - "PG_USER=${env.PG_USER}" - ]) { - sh """ - echo "Backing up PostgreSQL database to ${BACKUP_DIR}/${BACKUP_FILE}..." - docker exec -e PGPASSWORD=${PG_PASSWORD} ${POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${PG_USER} > ${BACKUP_DIR}/${BACKUP_FILE}' - """ - } + sh """ + echo "Backing up PostgreSQL database to ${env.BACKUP_DIR}/${BACKUP_FILE}..." + docker exec -e PGPASSWORD=${env.PG_PASSWORD} ${env.POSTGRESQL_CONTAINER_NAME} sh -c 'pg_dumpall -c -U ${env.PG_USER} > ${env.BACKUP_DIR}/${BACKUP_FILE}' + """ } def dockerLogin() { - withEnv(["DOCKER_HUB_PASSWORD=${env.DOCKER_HUB_PASSWORD}", "DOCKER_HUB_USER=${env.DOCKER_HUB_USER}"]) { - sh """ - echo "Logging in to Docker Hub..." - echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USER} --password-stdin - """ - } + sh """ + echo "Logging in to Docker Hub..." + echo "${env.DOCKER_HUB_PASSWORD}" | docker login -u "${env.DOCKER_HUB_USER}" --password-stdin + """ } def determineContainers() { script { - withEnv([ - "BLUE_CONTAINER=${env.BLUE_CONTAINER}", - "GREEN_CONTAINER=${env.GREEN_CONTAINER}", - "BLUE_URL=${env.BLUE_URL}", - "GREEN_URL=${env.GREEN_URL}", - "PORT_A=${env.PORT_A}", - "PORT_B=${env.PORT_B}" - ]) { - def blueRunning = sh(script: "docker ps --filter 'name=${BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${BLUE_CONTAINER}'", returnStatus: true) == 0 - if (blueRunning) { - env.CURRENT_CONTAINER = BLUE_CONTAINER - env.DEPLOY_CONTAINER = GREEN_CONTAINER - env.NEW_TARGET = GREEN_URL - env.NEW_PORT = PORT_B - env.OLD_PORT = PORT_A - } else { - env.CURRENT_CONTAINER = GREEN_CONTAINER - env.DEPLOY_CONTAINER = BLUE_CONTAINER - env.NEW_TARGET = BLUE_URL - env.NEW_PORT = PORT_A - env.OLD_PORT = PORT_B - } - echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." + def blueRunning = sh(script: "docker ps --filter 'name=${env.BLUE_CONTAINER}' --format '{{.Names}}' | grep -q '${env.BLUE_CONTAINER}'", returnStatus: true) == 0 + if (blueRunning) { + env.CURRENT_CONTAINER = env.BLUE_CONTAINER + env.DEPLOY_CONTAINER = env.GREEN_CONTAINER + env.NEW_TARGET = env.GREEN_URL + env.NEW_PORT = env.PORT_B + env.OLD_PORT = env.PORT_A + } else { + env.CURRENT_CONTAINER = env.GREEN_CONTAINER + env.DEPLOY_CONTAINER = env.BLUE_CONTAINER + env.NEW_TARGET = env.BLUE_URL + env.NEW_PORT = env.PORT_A + env.OLD_PORT = env.PORT_B } - } -} - -def buildApplication() { - withEnv([ - "PROFILE=${env.PROFILE}" - ]) { - sh """ - echo "Building application with profile ${PROFILE}..." - ./gradlew clean build -Penv=${PROFILE} --stacktrace --info - """ + echo "Current container is ${env.CURRENT_CONTAINER}, deploying to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." } } def buildAndPushDockerImage() { - withEnv([ - "DOCKER_HUB_REPO=${env.DOCKER_HUB_REPO}", - "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", - "DOCKERFILE_PATH=${env.DOCKERFILE_PATH}", - "IMAGE_NAME=${env.IMAGE_NAME}" - ]) { - sh """ - docker build -f ${DOCKERFILE_PATH} -t ${IMAGE_NAME}:${DEPLOY_CONTAINER} . - docker tag ${IMAGE_NAME}:${DEPLOY_CONTAINER} ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} - docker push ${DOCKER_HUB_REPO}:${DEPLOY_CONTAINER} - """ - } + sh """ + DOCKER_BUILDKIT=1 docker build -f ${env.DOCKERFILE_PATH} -t ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} . + docker tag ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER} + docker push ${env.DOCKER_HUB_REPO}:${env.DEPLOY_CONTAINER} + docker logout + """ } def deployNewInstance() { - withEnv([ - "PROFILE=${env.PROFILE}", - "NEW_PORT=${env.NEW_PORT}", - "APPLICATION_NETWORK=${env.APPLICATION_NETWORK}", - "MONITORING_NETWORK=${env.MONITORING_NETWORK}", - "EXTERNAL_SERVER_CONFIG_PATH=${env.EXTERNAL_SERVER_CONFIG_PATH}", - "EXTERNAL_SERVER_CLOUD_PATH=${env.EXTERNAL_SERVER_CLOUD_PATH}", - "EXTERNAL_SERVER_LOGS_PATH=${env.EXTERNAL_SERVER_LOGS_PATH}", - "INTERNAL_SERVER_CONFIG_PATH=${env.INTERNAL_SERVER_CONFIG_PATH}", - "INTERNAL_SERVER_CLOUD_PATH=${env.INTERNAL_SERVER_CLOUD_PATH}", - "INTERNAL_SERVER_LOGS_PATH=${env.INTERNAL_SERVER_LOGS_PATH}", - "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", - "IMAGE_NAME=${env.IMAGE_NAME}" - ]) { - sh """ - echo "Stopping and removing existing container if it exists" - if docker ps | grep -q ${DEPLOY_CONTAINER}; then - docker stop ${DEPLOY_CONTAINER} - docker rm ${DEPLOY_CONTAINER} - fi - - echo "Running new container ${DEPLOY_CONTAINER} with image ${IMAGE_NAME}:${DEPLOY_CONTAINER}" - docker run -d --name ${DEPLOY_CONTAINER} \\ - -p ${NEW_PORT}:8080 \\ - --network ${APPLICATION_NETWORK} \\ - -v ${EXTERNAL_SERVER_CONFIG_PATH}:${INTERNAL_SERVER_CONFIG_PATH} \\ - -v ${EXTERNAL_SERVER_CLOUD_PATH}:${INTERNAL_SERVER_CLOUD_PATH} \\ - -v ${EXTERNAL_SERVER_LOGS_PATH}:${INTERNAL_SERVER_LOGS_PATH} \\ - -e LOG_PATH=${INTERNAL_SERVER_LOGS_PATH} \\ - -e SPRING_PROFILES_ACTIVE=${PROFILE} \\ - ${IMAGE_NAME}:${DEPLOY_CONTAINER} - - echo "Checking if monitoring network ${MONITORING_NETWORK} exists" - if docker network ls --format '{{.Name}}' | grep -q '^${MONITORING_NETWORK}\$'; then - echo "Connecting to monitoring network ${MONITORING_NETWORK}" - docker network connect ${MONITORING_NETWORK} ${DEPLOY_CONTAINER} - else - echo "Monitoring network ${MONITORING_NETWORK} does not exist. Skipping connection." - fi - - echo "Listing all containers" - docker ps -a - """ - } + sh """ + echo "Stopping and removing existing container if it exists" + if docker ps | grep -q ${env.DEPLOY_CONTAINER}; then + docker stop ${env.DEPLOY_CONTAINER} + docker rm ${env.DEPLOY_CONTAINER} + fi + + echo "Running new container ${env.DEPLOY_CONTAINER} with image ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER}" + docker run -d --name ${env.DEPLOY_CONTAINER} \\ + -p ${env.NEW_PORT}:8080 \\ + --network ${env.APPLICATION_NETWORK} \\ + -v ${env.EXTERNAL_SERVER_CONFIG_PATH}:${env.INTERNAL_SERVER_CONFIG_PATH} \\ + -v ${env.EXTERNAL_SERVER_CLOUD_PATH}:${env.INTERNAL_SERVER_CLOUD_PATH} \\ + -v ${env.EXTERNAL_SERVER_LOGS_PATH}:${env.INTERNAL_SERVER_LOGS_PATH} \\ + -e LOG_PATH=${env.INTERNAL_SERVER_LOGS_PATH} \\ + -e SPRING_PROFILES_ACTIVE=${env.PROFILE} \\ + ${env.IMAGE_NAME}:${env.DEPLOY_CONTAINER} + + echo "Checking if monitoring network ${env.MONITORING_NETWORK} exists" + if docker network ls --format '{{.Name}}' | grep -q '^${env.MONITORING_NETWORK}\$'; then + echo "Connecting to monitoring network ${env.MONITORING_NETWORK}" + docker network connect ${env.MONITORING_NETWORK} ${env.DEPLOY_CONTAINER} + else + echo "Monitoring network ${env.MONITORING_NETWORK} does not exist. Skipping connection." + fi + + echo "Listing all containers" + docker ps -a + """ } def performHealthCheck() { - withEnv([ - "WHITELIST_ADMIN_USERNAME=${env.WHITELIST_ADMIN_USERNAME}", - "WHITELIST_ADMIN_PASSWORD=${env.WHITELIST_ADMIN_PASSWORD}" - ]) { - def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() - echo "Public IP address: ${PUBLIC_IP}" - - def start_time = System.currentTimeMillis() - def timeout = start_time + 150000 // 2.5 minutes - - while (System.currentTimeMillis() < timeout) { - def elapsed = (System.currentTimeMillis() - start_time) / 1000 - echo "Checking health... ${elapsed} seconds elapsed." - def status = sh( - script: """curl -s -u ${WHITELIST_ADMIN_USERNAME}:${WHITELIST_ADMIN_PASSWORD} \ - http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", - returnStatus: true - ) - if (status == 0) { - echo "New application started successfully after ${elapsed} seconds." - return - } - sleep 5 + def PUBLIC_IP = sh(script: "curl -s ifconfig.me", returnStdout: true).trim() + echo "Public IP address: ${PUBLIC_IP}" + + def start_time = System.currentTimeMillis() + def timeout = start_time + 150000 // 2.5 minutes + + while (System.currentTimeMillis() < timeout) { + def elapsed = (System.currentTimeMillis() - start_time) / 1000 + echo "Checking health... ${elapsed} seconds elapsed." + def status = sh( + script: """curl -s -u ${env.WHITELIST_ADMIN_USERNAME}:${env.WHITELIST_ADMIN_PASSWORD} \ + http://${PUBLIC_IP}:${env.NEW_PORT}/actuator/health | grep 'UP'""", + returnStatus: true + ) + if (status == 0) { + echo "New application started successfully after ${elapsed} seconds." + return } + sleep 5 + } - if (System.currentTimeMillis() >= timeout) { - sh "docker stop ${env.DEPLOY_CONTAINER}" - sh "docker rm ${env.DEPLOY_CONTAINER}" - error "Health check failed" - } + if (System.currentTimeMillis() >= timeout) { + sh "docker stop ${env.DEPLOY_CONTAINER}" + sh "docker rm ${env.DEPLOY_CONTAINER}" + error "Health check failed" } } def switchTrafficAndCleanup() { - withEnv([ - "NEW_PORT=${env.NEW_PORT}", - "OLD_PORT=${env.OLD_PORT}", - "NEW_TARGET=${env.NEW_TARGET}", - "CURRENT_CONTAINER=${env.CURRENT_CONTAINER}", - "DEPLOY_CONTAINER=${env.DEPLOY_CONTAINER}", - "NGINX_CONTAINER_NAME=${env.NGINX_CONTAINER_NAME}" - ]) { - sh """ - echo "Switching traffic to ${DEPLOY_CONTAINER} on port ${NEW_PORT}." - docker exec ${NGINX_CONTAINER_NAME} bash -c ' - export BACKEND_URL=${NEW_TARGET} - envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf - ' - docker exec ${NGINX_CONTAINER_NAME} sed -i 's/${OLD_PORT}/${NEW_PORT}/' /etc/nginx/conf.d/default.conf - docker exec ${NGINX_CONTAINER_NAME} nginx -t - docker exec ${NGINX_CONTAINER_NAME} nginx -s reload - - echo "Checking if current container ${CURRENT_CONTAINER} is running..." - if docker ps | grep -q ${CURRENT_CONTAINER}; then - docker stop ${CURRENT_CONTAINER} - docker rm ${CURRENT_CONTAINER} - echo "Removed old container ${CURRENT_CONTAINER}." - fi - """ - } + sh """ + echo "Switching traffic to ${env.DEPLOY_CONTAINER} on port ${env.NEW_PORT}." + docker exec ${env.NGINX_CONTAINER_NAME} bash -c ' + export BACKEND_URL=${env.NEW_TARGET} + envsubst "\\\$BACKEND_URL" < /etc/nginx/conf.d/members.conf.template > /etc/nginx/conf.d/members.conf + ' + docker exec ${env.NGINX_CONTAINER_NAME} sed -i 's/${env.OLD_PORT}/${env.NEW_PORT}/' /etc/nginx/conf.d/members.conf + docker exec ${env.NGINX_CONTAINER_NAME} nginx -t + docker exec ${env.NGINX_CONTAINER_NAME} nginx -s reload + + echo "Checking if current container ${env.CURRENT_CONTAINER} is running..." + if docker ps | grep -q ${env.CURRENT_CONTAINER}; then + docker stop ${env.CURRENT_CONTAINER} + docker rm ${env.CURRENT_CONTAINER} + echo "Removed old container ${env.CURRENT_CONTAINER}." + fi + """ } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/api/ActivityGroupBoardController.java b/src/main/java/page/clab/api/domain/activity/activitygroup/api/ActivityGroupBoardController.java index 6b479b89c..1f2e65903 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/api/ActivityGroupBoardController.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/api/ActivityGroupBoardController.java @@ -80,7 +80,7 @@ public ApiResponse> getActivityG public ApiResponse getActivityGroupBoardById( @RequestParam(name = "activityGroupBoardId") Long activityGroupBoardId ) throws PermissionDeniedException { - ActivityGroupBoardResponseDto board = activityGroupBoardService.getActivityGroupBoardById(activityGroupBoardId); + ActivityGroupBoardResponseDto board = activityGroupBoardService.getActivityGroupBoard(activityGroupBoardId); return ApiResponse.success(board); } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupAdminService.java b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupAdminService.java index 4e3a703e4..c55a0f1e4 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupAdminService.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupAdminService.java @@ -18,6 +18,7 @@ import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.activity.activitygroup.domain.GroupMemberStatus; import page.clab.api.domain.activity.activitygroup.domain.GroupSchedule; +import page.clab.api.domain.activity.activitygroup.dto.mapper.ActivityGroupDtoMapper; import page.clab.api.domain.activity.activitygroup.dto.param.GroupScheduleDto; import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupRequestDto; import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupUpdateRequestDto; @@ -50,11 +51,12 @@ public class ActivityGroupAdminService { private final ApplyFormRepository applyFormRepository; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final ActivityGroupDtoMapper mapper; @Transactional public Long createActivityGroup(ActivityGroupRequestDto requestDto) { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = ActivityGroupRequestDto.toEntity(requestDto); + ActivityGroup activityGroup = mapper.fromDto(requestDto); activityGroup.validateAndSetGithubUrl(activityGroup.getGithubUrl()); activityGroupRepository.save(activityGroup); @@ -68,15 +70,16 @@ public Long createActivityGroup(ActivityGroupRequestDto requestDto) { @Transactional public Long updateActivityGroup(Long activityGroupId, ActivityGroupUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); validateLeaderOrAdminPermission(activityGroup, currentMember, "해당 활동을 수정할 권한이 없습니다."); activityGroup.update(requestDto); return activityGroupRepository.save(activityGroup).getId(); } + // 활동 그룹의 status를 수정합니다. @Transactional public ActivityGroupBoardStatusUpdatedResponseDto manageActivityGroup(Long activityGroupId, ActivityGroupStatus status) { - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); activityGroup.updateStatus(status); activityGroupRepository.save(activityGroup); @@ -84,18 +87,18 @@ public ActivityGroupBoardStatusUpdatedResponseDto manageActivityGroup(Long activ if (!CollectionUtils.isEmpty(groupLeaders)) { groupLeaders.forEach(leader -> externalSendNotificationUseCase.sendNotificationToMember(leader.getMemberId(), "활동 그룹이 [" + status.getDescription() + "] 상태로 변경되었습니다.")); } - return ActivityGroupBoardStatusUpdatedResponseDto.toDto(activityGroupId, status); + return mapper.of(activityGroupId, status); } @Transactional(readOnly = true) public PagedResponseDto getDeletedActivityGroups(Pageable pageable) { Page activityGroups = activityGroupRepository.findAllByIsDeletedTrue(pageable); - return new PagedResponseDto<>(activityGroups.map(ActivityGroupResponseDto::toDto)); + return new PagedResponseDto<>(activityGroups.map(mapper::toDto)); } @Transactional public Long deleteActivityGroup(Long activityGroupId) throws PermissionDeniedException { - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); List groupMembers = activityGroupMemberService.getGroupMemberByActivityGroupId(activityGroupId); List groupSchedules = groupScheduleRepository.findAllByActivityGroupIdOrderByIdDesc(activityGroupId); List groupLeaders = activityGroupMemberService.getGroupMemberByActivityGroupIdAndRole(activityGroupId, ActivityGroupRole.LEADER); @@ -116,7 +119,7 @@ public Long deleteActivityGroup(Long activityGroupId) throws PermissionDeniedExc @Transactional public Long updateProjectProgress(Long activityGroupId, Long progress) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); validateLeaderOrAdminPermission(activityGroup, currentMember, "해당 활동을 수정할 권한이 업습니다."); activityGroup.updateProgress(progress); return activityGroupRepository.save(activityGroup).getId(); @@ -125,10 +128,10 @@ public Long updateProjectProgress(Long activityGroupId, Long progress) throws Pe @Transactional public Long addSchedule(Long activityGroupId, List scheduleDtos) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); validateLeaderOrAdminPermission(activityGroup, currentMember, "해당 일정을 등록할 권한이 없습니다."); List groupSchedules = scheduleDtos.stream() - .map(scheduleDto -> GroupScheduleDto.toEntity(scheduleDto, activityGroup)) + .map(scheduleDto -> mapper.fromDto(scheduleDto, activityGroup)) .toList(); groupScheduleRepository.saveAll(groupSchedules); return activityGroup.getId(); @@ -137,7 +140,7 @@ public Long addSchedule(Long activityGroupId, List scheduleDto @Transactional(readOnly = true) public PagedResponseDto getGroupMembersWithApplyReason(Long activityGroupId, Pageable pageable) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); validateLeaderOrAdminPermission(activityGroup, currentMember, "해당 활동의 멤버를 조회할 권한이 없습니다."); List applyForms = applyFormRepository.findAllByActivityGroup(activityGroup); @@ -151,8 +154,8 @@ public PagedResponseDto getGroupM List groupMembersWithApplyReason = groupMembers.getContent().stream() .map(groupMember -> { String applyReason = memberIdToApplyReasonMap.getOrDefault(groupMember.getMemberId(), ""); - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(groupMember.getMemberId()); - return ActivityGroupMemberWithApplyReasonResponseDto.create(member, groupMember, applyReason); + Member member = externalRetrieveMemberUseCase.getById(groupMember.getMemberId()); + return mapper.toDto(member, groupMember, applyReason); }) .toList(); @@ -160,10 +163,11 @@ public PagedResponseDto getGroupM return new PagedResponseDto<>(paginatedGroupMembersWithApplyReason, groupMembers.getTotalElements(), groupMembersWithApplyReason.size()); } + // 활동 멤버들의 status를 수정합니다. @Transactional public Long manageGroupMemberStatus(Long activityGroupId, List memberIds, GroupMemberStatus status) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); validateLeaderOrAdminPermission(activityGroup, currentMember, "해당 활동의 신청 멤버를 조회할 권한이 없습니다."); memberIds.forEach(memberId -> updateGroupMemberStatus(memberId, status, activityGroup)); return activityGroup.getId(); @@ -171,8 +175,8 @@ public Long manageGroupMemberStatus(Long activityGroupId, List memberIds @Transactional public Long changeGroupMemberPosition(Long activityGroupId, String memberId, ActivityGroupRole position) throws PermissionDeniedException { - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); - GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMemberOrThrow(activityGroup, memberId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); + GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMember(activityGroup, memberId); Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); validateLeaderOrAdminPermission(activityGroup, currentMember, "해당 활동의 멤버 직책을 변경할 권한이 없습니다."); @@ -200,39 +204,43 @@ private void updateActivityGroupStatusEnd(ActivityGroup activityGroup) { } private void updateGroupMemberStatus(String memberId, GroupMemberStatus status, ActivityGroup activityGroup) { - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(memberId); - GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMemberOrThrow(activityGroup, member.getId()); + Member member = externalRetrieveMemberUseCase.getById(memberId); + GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMember(activityGroup, member.getId()); groupMember.validateAccessPermission(); groupMember.updateStatus(status); activityGroupMemberService.save(groupMember); externalSendNotificationUseCase.sendNotificationToMember(member.getId(), "[" + activityGroup.getName() + "]" + " 신청이 [" + status.getDescription() + "] 상태로 변경되었습니다."); } - public ActivityGroup getActivityGroupByIdOrThrow(Long activityGroupId) { + public ActivityGroup getActivityGroupById(Long activityGroupId) { return activityGroupRepository.findById(activityGroupId) .orElseThrow(() -> new NotFoundException("존재하지 않는 활동입니다.")); } + // 해당 멤버가 특정 활동 그룹의 리더 또는 관리자인지 검증합니다. + // 예외가 발생하지 않고 안전하게 처리됩니다. public boolean hasLeaderOrAdminRole(ActivityGroup activityGroup, Member member) { - return activityGroupMemberService.getGroupMemberByActivityGroupAndMember(activityGroup, member.getId()) + return activityGroupMemberService.findGroupMemberByActivityGroupAndMember(activityGroup, member.getId()) .map(GroupMember::isLeader) .orElseGet(member::isAdminRole); } + // 해당 멤버가 특정 활동 그룹의 리더 또는 관리자인지 검증합니다. + // 활동 멤버가 아닌 경우 false를 반환합니다. public boolean isMemberGroupLeaderRole(Long activityGroupId, String memberId) { - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(memberId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); + Member member = externalRetrieveMemberUseCase.getById(memberId); try{ - GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMemberOrThrow(activityGroup, member.getId()); + GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMember(activityGroup, member.getId()); return groupMember.isLeader() || member.isAdminRole(); } catch (NotFoundException e) { - return false; + return false; } } public boolean isMemberHasRoleInActivityGroup(Member member, ActivityGroupRole role, Long activityGroupId) { List groupMemberList = activityGroupMemberService.getGroupMemberByMemberId(member.getId()); - ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupById(activityGroupId); return groupMemberList.stream() .anyMatch(groupMember -> groupMember.isSameRoleAndActivityGroup(role, activityGroup)); } @@ -245,7 +253,7 @@ private void validateLeaderRoleChange(ActivityGroup activityGroup, GroupMember g } public ActivityGroup validateAndGetActivityGroupForReporting(Long activityGroupId, Member member) throws PermissionDeniedException, IllegalAccessException { - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); if (!isMemberHasRoleInActivityGroup(member, ActivityGroupRole.LEADER, activityGroupId)) { throw new PermissionDeniedException("해당 그룹의 리더만 보고서를 작성할 수 있습니다."); } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupBoardService.java b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupBoardService.java index 544175368..cb858d0fb 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupBoardService.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupBoardService.java @@ -15,6 +15,7 @@ import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupRole; import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.activity.activitygroup.domain.GroupMemberStatus; +import page.clab.api.domain.activity.activitygroup.dto.mapper.ActivityGroupDtoMapper; import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupBoardRequestDto; import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupBoardUpdateRequestDto; import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupBoardChildResponseDto; @@ -48,11 +49,29 @@ public class ActivityGroupBoardService { private final UploadedFileService uploadedFileService; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final ActivityGroupDtoMapper mapper; + /** + * 새로운 활동 그룹 게시판을 생성합니다. + * + *

새 게시판을 생성하기 전 다음 검증이 수행됩니다:

+ * - 현재 사용자가 해당 활동 그룹의 멤버인지 확인 + * - 공지, 주차별 활동, 과제, 피드백 카테고리는 관리자 또는 리더 권한 필요 + * - 부모 게시판의 유효성 확인 + * - 이번 주에 이미 과제를 제출했는지 확인 + * + *

부모 게시판이 있을 경우 자식 게시판으로 추가하며, 게시판 생성 후 알림을 전송합니다.

+ * + * @param parentId 부모 게시판의 ID (없을 수 있음) + * @param activityGroupId 활동 그룹의 ID + * @param requestDto 게시판 생성 요청 DTO + * @return ActivityGroupBoardReferenceDto + * @throws PermissionDeniedException 권한이 없는 경우 예외 발생 + */ @Transactional public ActivityGroupBoardReferenceDto createActivityGroupBoard(Long parentId, Long activityGroupId, ActivityGroupBoardRequestDto requestDto) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); validateGroupMember(activityGroup, currentMember); validateCanCreateBoard(activityGroup, requestDto.getCategory(), currentMember); @@ -61,8 +80,8 @@ public ActivityGroupBoardReferenceDto createActivityGroupBoard(Long parentId, Lo List uploadedFiles = uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrls()); - ActivityGroupBoard parentBoard = parentId != null ? getActivityGroupBoardByIdOrThrow(parentId) : null; - ActivityGroupBoard board = ActivityGroupBoardRequestDto.toEntity(requestDto, currentMember, activityGroup, parentBoard, uploadedFiles); + ActivityGroupBoard parentBoard = parentId != null ? getActivityGroupBoardById(parentId) : null; + ActivityGroupBoard board = mapper.fromDto(requestDto, currentMember, activityGroup, parentBoard, uploadedFiles); board.validateEssentialElementByCategory(); if (parentId != null) { @@ -72,7 +91,7 @@ public ActivityGroupBoardReferenceDto createActivityGroupBoard(Long parentId, Lo activityGroupBoardRepository.save(board); notifyMembersAboutNewBoard(activityGroupId, activityGroup, board, currentMember); - return ActivityGroupBoardReferenceDto.toDto(board.getId(), activityGroupId, parentId); + return mapper.of(board.getId(), activityGroupId, parentId); } private void validateGroupMember(ActivityGroup activityGroup, Member currentMember) throws PermissionDeniedException { @@ -130,33 +149,34 @@ public PagedResponseDto getAllActivityGroupBoard( Page boards = activityGroupBoardRepository.findAll(pageable); return new PagedResponseDto<>(boards.map(board -> { MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(board.getMemberId()); - return ActivityGroupBoardResponseDto.toDto(board, memberBasicInfoDto); + return mapper.toBoardDto(board, memberBasicInfoDto); })); } @Transactional(readOnly = true) - public ActivityGroupBoardResponseDto getActivityGroupBoardById(Long activityGroupBoardId) throws PermissionDeniedException { + public ActivityGroupBoardResponseDto getActivityGroupBoard(Long activityGroupBoardId) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); + ActivityGroupBoard board = getActivityGroupBoardById(activityGroupBoardId); validateGroupMember(board.getActivityGroup(), currentMember); if (!hasAccessToBoard(board.getActivityGroup(), board, currentMember)) { throw new PermissionDeniedException("해당 게시물을 조회할 권한이 없습니다."); } MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(board.getMemberId()); - return ActivityGroupBoardResponseDto.toDto(board, memberBasicInfoDto); + return mapper.toBoardDto(board, memberBasicInfoDto); } @Transactional(readOnly = true) public PagedResponseDto getActivityGroupBoardByCategory(Long activityGroupId, ActivityGroupBoardCategory category, Pageable pageable) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); validateGroupMember(activityGroup, currentMember); Page boards = activityGroupBoardRepository.findAllByActivityGroup_IdAndCategory(activityGroupId, category, pageable); + // 사용자 권한에 따라 접근 가능한 게시판만 반환합니다. List filteredBoards = boards.stream() .filter(board -> hasAccessToBoard(board.getActivityGroup(), board, currentMember)) .map(board -> { MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(board.getMemberId()); - return ActivityGroupBoardResponseDto.toDto(board, memberBasicInfoDto); + return mapper.toBoardDto(board, memberBasicInfoDto); }) .toList(); return new PagedResponseDto<>(new PageImpl<>(filteredBoards, pageable, boards.getTotalElements())); @@ -165,7 +185,7 @@ public PagedResponseDto getActivityGroupBoardByCa @Transactional(readOnly = true) public PagedResponseDto getActivityGroupBoardByParent(Long parentId, Pageable pageable) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroupBoard parentBoard = getActivityGroupBoardByIdOrThrow(parentId); + ActivityGroupBoard parentBoard = getActivityGroupBoardById(parentId); Long activityGroupId = parentBoard.getActivityGroup().getId(); validateGroupMember(parentBoard.getActivityGroup(), currentMember); @@ -173,6 +193,7 @@ public PagedResponseDto getActivityGroupBoar parentBoard.validateAccessPermission(currentMember, groupLeaders); List childBoards = getChildBoards(parentId); + // 접근 가능한 자식 게시판만 조회합니다. List filteredBoards = childBoards.stream() .filter(board -> hasAccessToBoard(board.getActivityGroup(), board, currentMember)) .map(this::toActivityGroupBoardChildResponseDtoWithMemberInfo) @@ -186,17 +207,18 @@ public List getMyAssignmentsWithFee Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); List mySubmissions = activityGroupBoardRepository.findMySubmissionsWithFeedbacks(parentId, currentMember.getId()); + // 해당 과제에 대한 피드백 목록을 조회합니다. return mySubmissions.stream() .map(submission -> { List feedbackDtos = submission.getChildren().stream() .filter(ActivityGroupBoard::isFeedback) .map(board -> { MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(board.getMemberId()); - return FeedbackResponseDto.toDto(board, memberBasicInfoDto); + return mapper.toFeedbackDto(board, memberBasicInfoDto); }) .toList(); MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(submission.getMemberId()); - return AssignmentSubmissionWithFeedbackResponseDto.toDto(submission, memberBasicInfoDto, feedbackDtos); + return mapper.toAssignmentDto(submission, memberBasicInfoDto, feedbackDtos); }) .toList(); } @@ -204,22 +226,22 @@ public List getMyAssignmentsWithFee @Transactional public ActivityGroupBoardReferenceDto updateActivityGroupBoard(Long activityGroupBoardId, ActivityGroupBoardUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); + ActivityGroupBoard board = getActivityGroupBoardById(activityGroupBoardId); board.validateAccessPermission(currentMember); board.update(requestDto, uploadedFileService); activityGroupBoardRepository.save(board); Long parentId = (board.getParent() != null) ? board.getParent().getId() : null; - return ActivityGroupBoardReferenceDto.toDto(board.getId(), board.getActivityGroup().getId(), parentId); + return mapper.of(board.getId(), board.getActivityGroup().getId(), parentId); } public ActivityGroupBoardReferenceDto deleteActivityGroupBoard(Long activityGroupBoardId) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroupBoard board = getActivityGroupBoardByIdOrThrow(activityGroupBoardId); + ActivityGroupBoard board = getActivityGroupBoardById(activityGroupBoardId); board.validateAccessPermission(currentMember); activityGroupBoardRepository.delete(board); Long parentId = (board.getParent() != null) ? board.getParent().getId() : null; - return ActivityGroupBoardReferenceDto.toDto(board.getId(), board.getActivityGroup().getId(), parentId); + return mapper.of(board.getId(), board.getActivityGroup().getId(), parentId); } @Transactional(readOnly = true) @@ -227,11 +249,11 @@ public PagedResponseDto getDeletedActivityGroupBo Page boards = activityGroupBoardRepository.findAllByIsDeletedTrue(pageable); return new PagedResponseDto<>(boards.map(board -> { MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(board.getMemberId()); - return ActivityGroupBoardResponseDto.toDto(board, memberBasicInfoDto); + return mapper.toBoardDto(board, memberBasicInfoDto); })); } - public ActivityGroupBoard getActivityGroupBoardByIdOrThrow(Long activityGroupBoardId) { + public ActivityGroupBoard getActivityGroupBoardById(Long activityGroupBoardId) { return activityGroupBoardRepository.findById(activityGroupBoardId) .orElseThrow(() -> new NotFoundException("해당 활동 그룹 게시글을 찾을 수 없습니다.")); } @@ -249,9 +271,23 @@ public ActivityGroupBoardChildResponseDto toActivityGroupBoardChildResponseDtoWi .filter(children -> hasAccessToBoard(children.getActivityGroup(), children, currentMember)) .map(this::toActivityGroupBoardChildResponseDtoWithMemberInfo) .toList(); - return ActivityGroupBoardChildResponseDto.toDto(activityGroupBoard, memberBasicInfo, childrenDtos); + return mapper.toChildDto(activityGroupBoard, memberBasicInfo, childrenDtos); } + /** + * 주어진 카테고리와 부모 게시판의 유효성을 검증합니다. + * + *

다음 규칙을 따릅니다:

+ * - 공지와 주차별 활동 게시판은 부모 게시판을 가질 수 없습니다. + * - 과제, 제출, 피드백 게시판은 부모 게시판이 반드시 필요합니다. + * - 과제의 부모는 주차별 활동 게시판이어야 합니다. + * - 제출의 부모는 과제 게시판이어야 합니다. + * - 피드백의 부모는 제출 게시판이어야 합니다. + * + * @param category 게시판 카테고리 + * @param parentId 부모 게시판의 ID + * @throws InvalidParentBoardException 부모 게시판이 유효하지 않은 경우 예외 발생 + */ private void validateParentBoard(ActivityGroupBoardCategory category, Long parentId) throws InvalidParentBoardException { if ((category.isNotice() || category.isWeeklyActivity())) { if (parentId != null) { @@ -265,7 +301,7 @@ private void validateParentBoard(ActivityGroupBoardCategory category, Long paren throw new InvalidParentBoardException(category.getDescription() + " 게시물은 부모 게시판이 필요합니다."); } - ActivityGroupBoard parentBoard = getActivityGroupBoardByIdOrThrow(parentId); + ActivityGroupBoard parentBoard = getActivityGroupBoardById(parentId); ActivityGroupBoardCategory expectedParentCategory = switch (category) { case ASSIGNMENT -> ActivityGroupBoardCategory.WEEKLY_ACTIVITY; @@ -299,7 +335,7 @@ private void validateParentBoard(ActivityGroupBoardCategory category, Long paren * @param member 게시판을 생성한 멤버 객체 */ private void notifyMembersAboutNewBoard(Long activityGroupId, ActivityGroup activityGroup, ActivityGroupBoard board, Member member) { - GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMemberOrThrow(activityGroup, member.getId()); + GroupMember groupMember = activityGroupMemberService.getGroupMemberByActivityGroupAndMember(activityGroup, member.getId()); if (groupMember.isLeader()) { if (board.isFeedback()) { String submitMemberId = board.getParent().getMemberId(); diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupMemberService.java b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupMemberService.java index a4f0905e6..06fb731a8 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupMemberService.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupMemberService.java @@ -21,6 +21,7 @@ import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.activity.activitygroup.domain.GroupMemberStatus; import page.clab.api.domain.activity.activitygroup.domain.GroupSchedule; +import page.clab.api.domain.activity.activitygroup.dto.mapper.ActivityGroupDtoMapper; import page.clab.api.domain.activity.activitygroup.dto.param.ActivityGroupDetails; import page.clab.api.domain.activity.activitygroup.dto.param.GroupScheduleDto; import page.clab.api.domain.activity.activitygroup.dto.request.ApplyFormRequestDto; @@ -59,6 +60,7 @@ public class ActivityGroupMemberService { private final ActivityGroupDetailsRepository activityGroupDetailsRepository; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final ActivityGroupDtoMapper mapper; @Transactional(readOnly = true) public ActivityGroupDetailResponseDto getActivityGroup(Long activityGroupId) { @@ -68,19 +70,21 @@ public ActivityGroupDetailResponseDto getActivityGroup(Long activityGroupId) { boolean isOwner = details.getGroupMembers().stream() .anyMatch(groupMember -> groupMember.isOwnerAndLeader(currentMember)); + // 활동 그룹 멤버를 조회합니다. List groupMemberResponseDtos = details.getGroupMembers().stream() - .map(groupMember -> GroupMemberResponseDto.toDto(externalRetrieveMemberUseCase.findByIdOrThrow(groupMember.getMemberId()), groupMember)) + .map(groupMember -> mapper.toDto(externalRetrieveMemberUseCase.getById(groupMember.getMemberId()), groupMember)) .toList(); + // 활동 그룹 게시판을 조회합니다. List activityGroupBoardResponseDtos = details.getActivityGroupBoards().stream() .map(board -> { MemberBasicInfoDto memberBasicInfoDto = externalRetrieveMemberUseCase.getMemberBasicInfoById(board.getMemberId()); - return ActivityGroupBoardResponseDto.toDto(board, memberBasicInfoDto); + return mapper.toBoardDto(board, memberBasicInfoDto); }) .toList(); - return ActivityGroupDetailResponseDto.create(details.getActivityGroup(), activityGroupBoardResponseDtos, groupMemberResponseDtos, isOwner); + return mapper.toDto(details.getActivityGroup(), activityGroupBoardResponseDtos, groupMemberResponseDtos, isOwner); } @Transactional(readOnly = true) @@ -88,6 +92,8 @@ public PagedResponseDto getMyActivityGroups(Acti String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); List groupMembers = getGroupMemberByMemberId(currentMemberId); + // 현재 로그인한 멤버가 활동중인 그룹을 조회합니다. + // 상태가 전달되지 않으면 모든 그룹을 조회합니다. List activityGroups = groupMembers.stream() .filter(GroupMember::isAccepted) .map(GroupMember::getActivityGroup) @@ -113,34 +119,34 @@ public PagedResponseDto getActivityGroupsByStatu @Transactional(readOnly = true) public PagedResponseDto getActivityGroupsByCategory(ActivityGroupCategory category, Pageable pageable) { Page activityGroupList = getActivityGroupByCategory(category, pageable); - return new PagedResponseDto<>(activityGroupList.map(ActivityGroupResponseDto::toDto)); + return new PagedResponseDto<>(activityGroupList.map(mapper::toDto)); } @Transactional(readOnly = true) public PagedResponseDto getGroupSchedules(Long activityGroupId, Pageable pageable) { Page groupSchedules = getGroupScheduleByActivityGroupId(activityGroupId, pageable); - return new PagedResponseDto<>(groupSchedules.map(GroupScheduleDto::toDto)); + return new PagedResponseDto<>(groupSchedules.map(mapper::toDto)); } @Transactional(readOnly = true) public PagedResponseDto getActivityGroupMembers(Long activityGroupId, Pageable pageable) { Page groupMembers = getGroupMemberByActivityGroupIdAndStatus(activityGroupId, GroupMemberStatus.ACCEPTED, pageable); return new PagedResponseDto<>(groupMembers.map(groupMember -> { - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(groupMember.getMemberId()); - return GroupMemberResponseDto.toDto(member, groupMember); + Member member = externalRetrieveMemberUseCase.getById(groupMember.getMemberId()); + return mapper.toDto(member, groupMember); })); } @Transactional public Long applyActivityGroup(Long activityGroupId, ApplyFormRequestDto formRequestDto) { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = getActivityGroupById(activityGroupId); activityGroup.validateForApplication(); if (isGroupMember(activityGroup, currentMember.getId())) { throw new AlreadyAppliedException("해당 활동에 신청한 내역이 존재합니다."); } - ApplyForm form = ApplyFormRequestDto.toEntity(formRequestDto, activityGroup, currentMember); + ApplyForm form = mapper.fromDto(formRequestDto, activityGroup, currentMember); applyFormRepository.save(form); GroupMember groupMember = GroupMember.create(currentMember.getId(), activityGroup, ActivityGroupRole.NONE, GroupMemberStatus.WAITING); @@ -185,9 +191,9 @@ private ActivityGroupStatusResponseDto getActivityGroupStatusResponseDto(Activit List leaderMembers = groupMemberRepository.findLeaderByActivityGroupId(activityGroupId) .stream() .map(leader -> { - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(leader.getMemberId()); + Member member = externalRetrieveMemberUseCase.getById(leader.getMemberId()); LocalDateTime createdAt = leader.getCreatedAt(); - return LeaderInfo.create(member, createdAt); + return mapper.create(member, createdAt); }) // LEADER 직책을 가진 사람 중 가장 먼저 활동에 참여한 사람 순으로 정렬 .sorted(Comparator.comparing(LeaderInfo::getCreatedAt)) @@ -195,10 +201,10 @@ private ActivityGroupStatusResponseDto getActivityGroupStatusResponseDto(Activit Long weeklyActivityCount = activityGroupBoardRepository.countByActivityGroupIdAndCategory(activityGroupId, ActivityGroupBoardCategory.WEEKLY_ACTIVITY); - return ActivityGroupStatusResponseDto.toDto(activityGroup, leaderMembers, participantCount, weeklyActivityCount); + return mapper.toDto(activityGroup, leaderMembers, participantCount, weeklyActivityCount); } - public ActivityGroup getActivityGroupByIdOrThrow(Long activityGroupId) { + public ActivityGroup getActivityGroupById(Long activityGroupId) { return activityGroupRepository.findById(activityGroupId) .orElseThrow(() -> new NotFoundException("해당 활동이 존재하지 않습니다.")); } @@ -211,11 +217,11 @@ public Map findActivityGroupOwners(List activityGroupId )); } - public Optional getGroupMemberByActivityGroupAndMember(ActivityGroup activityGroup, String memberId) { + public Optional findGroupMemberByActivityGroupAndMember(ActivityGroup activityGroup, String memberId) { return groupMemberRepository.findByActivityGroupAndMemberId(activityGroup, memberId); } - public GroupMember getGroupMemberByActivityGroupAndMemberOrThrow(ActivityGroup activityGroup, String memberId) { + public GroupMember getGroupMemberByActivityGroupAndMember(ActivityGroup activityGroup, String memberId) { return groupMemberRepository.findByActivityGroupAndMemberId(activityGroup, memberId) .orElseThrow(() -> new NotFoundException("해당 멤버가 활동에 참여하지 않았습니다.")); } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupReportService.java b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupReportService.java index 3974caf77..441beddd8 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupReportService.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/application/ActivityGroupReportService.java @@ -10,6 +10,7 @@ import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupReport; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupRole; +import page.clab.api.domain.activity.activitygroup.dto.mapper.ActivityGroupDtoMapper; import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupReportRequestDto; import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupReportUpdateRequestDto; import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupReportResponseDto; @@ -27,6 +28,7 @@ public class ActivityGroupReportService { private final ActivityGroupAdminService activityGroupAdminService; private final ActivityGroupReportRepository activityGroupReportRepository; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final ActivityGroupDtoMapper mapper; @Transactional public Long writeReport(ActivityGroupReportRequestDto requestDto) throws PermissionDeniedException, IllegalAccessException { @@ -39,24 +41,24 @@ public Long writeReport(ActivityGroupReportRequestDto requestDto) throws Permiss @Transactional(readOnly = true) public PagedResponseDto getReports(Long activityGroupId, Pageable pageable) { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); Page reports = activityGroupReportRepository.findAllByActivityGroup(activityGroup, pageable); - return new PagedResponseDto<>(reports.map(ActivityGroupReportResponseDto::toDto)); + return new PagedResponseDto<>(reports.map(mapper::toDto)); } @Transactional(readOnly = true) public ActivityGroupReportResponseDto searchReport(Long activityGroupId, Long turn) { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); ActivityGroupReport report = activityGroupReportRepository.findByActivityGroupAndTurn(activityGroup, turn); - return ActivityGroupReportResponseDto.toDto(report); + return mapper.toDto(report); } @Transactional public Long updateReport(Long reportId, Long activityGroupId, ActivityGroupReportUpdateRequestDto requestDto) throws PermissionDeniedException, IllegalAccessException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); validateReportUpdatePermission(activityGroupId, currentMember, activityGroup); - ActivityGroupReport report = getReportByIdOrThrow(reportId); + ActivityGroupReport report = getReportById(reportId); report.update(requestDto); return activityGroupReportRepository.save(report).getId(); } @@ -71,10 +73,10 @@ public Long deleteReport(Long reportId) throws PermissionDeniedException { @Transactional(readOnly = true) public PagedResponseDto getDeletedActivityGroupReports(Pageable pageable) { Page activityGroupReports = activityGroupReportRepository.findAllByIsDeletedTrue(pageable); - return new PagedResponseDto<>(activityGroupReports.map(ActivityGroupReportResponseDto::toDto)); + return new PagedResponseDto<>(activityGroupReports.map(mapper::toDto)); } - public ActivityGroupReport getReportByIdOrThrow(Long reportId) { + public ActivityGroupReport getReportById(Long reportId) { return activityGroupReportRepository.findById(reportId) .orElseThrow(() -> new NotFoundException("활동 보고서를 찾을 수 없습니다.")); } @@ -83,7 +85,7 @@ private ActivityGroupReport validateReportCreationPermission(ActivityGroupReport if (activityGroupReportRepository.existsByActivityGroupAndTurn(activityGroup, requestDto.getTurn())) { throw new DuplicateReportException("이미 해당 차시의 보고서가 존재합니다."); } - return ActivityGroupReportRequestDto.toEntity(requestDto, activityGroup); + return mapper.fromDto(requestDto, activityGroup); } private void validateReportUpdatePermission(Long activityGroupId, Member currentMember, ActivityGroup activityGroup) throws PermissionDeniedException, IllegalAccessException { @@ -97,7 +99,7 @@ private void validateReportUpdatePermission(Long activityGroupId, Member current @NotNull private ActivityGroupReport validateReportDeletionPermission(Long reportId, Member member) throws PermissionDeniedException { - ActivityGroupReport report = getReportByIdOrThrow(reportId); + ActivityGroupReport report = getReportById(reportId); if (!activityGroupAdminService.isMemberHasRoleInActivityGroup(member, ActivityGroupRole.LEADER, report.getActivityGroup().getId())) { throw new PermissionDeniedException("해당 그룹의 리더만 보고서를 삭제할 수 있습니다."); } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/application/AttendanceService.java b/src/main/java/page/clab/api/domain/activity/activitygroup/application/AttendanceService.java index 5c5938f52..53e0f8ae7 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/application/AttendanceService.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/application/AttendanceService.java @@ -16,6 +16,7 @@ import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupRole; import page.clab.api.domain.activity.activitygroup.domain.Attendance; import page.clab.api.domain.activity.activitygroup.domain.RedisQRKey; +import page.clab.api.domain.activity.activitygroup.dto.mapper.ActivityGroupDtoMapper; import page.clab.api.domain.activity.activitygroup.dto.request.AbsentRequestDto; import page.clab.api.domain.activity.activitygroup.dto.request.AttendanceRequestDto; import page.clab.api.domain.activity.activitygroup.dto.response.AbsentResponseDto; @@ -48,6 +49,7 @@ public class AttendanceService { private final GoogleAuthenticator googleAuthenticator; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final FileService fileService; + private final ActivityGroupDtoMapper mapper; private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @@ -97,7 +99,7 @@ public PagedResponseDto getMyAttendances(Long activityGro Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); ActivityGroup activityGroup = validateGroupAndMemberForAttendance(activityGroupId, currentMember); Page attendances = getAttendanceByMemberId(activityGroup, currentMember.getId(), pageable); - return new PagedResponseDto<>(attendances.map(AttendanceResponseDto::toDto)); + return new PagedResponseDto<>(attendances.map(mapper::toDto)); } @Transactional(readOnly = true) @@ -105,15 +107,15 @@ public PagedResponseDto getGroupAttendances(Long activity Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); ActivityGroup activityGroup = getActivityGroupWithValidPermissions(activityGroupId, currentMember); Page attendances = getAttendanceByActivityGroup(activityGroup, pageable); - return new PagedResponseDto<>(attendances.map(AttendanceResponseDto::toDto)); + return new PagedResponseDto<>(attendances.map(mapper::toDto)); } @Transactional public Long writeAbsentExcuse(AbsentRequestDto requestDto) throws IllegalAccessException, DuplicateAbsentExcuseException { - Member absentee = externalRetrieveMemberUseCase.findByIdOrThrow(requestDto.getAbsenteeId()); + Member absentee = externalRetrieveMemberUseCase.getById(requestDto.getAbsenteeId()); ActivityGroup activityGroup = getValidActivityGroup(requestDto.getActivityGroupId()); validateAbsentExcuseConditions(absentee, activityGroup, requestDto.getAbsentDate()); - Absent absent = AbsentRequestDto.toEntity(requestDto, absentee, activityGroup); + Absent absent = mapper.fromDto(requestDto, absentee, activityGroup); return absentRepository.save(absent).getId(); } @@ -123,8 +125,8 @@ public PagedResponseDto getActivityGroupAbsentExcuses(Long ac ActivityGroup activityGroup = getActivityGroupWithPermissionCheck(activityGroupId, currentMember); Page absents = absentRepository.findAllByActivityGroup(activityGroup, pageable); return new PagedResponseDto<>(absents.map(absent -> { - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(absent.getMemberId()); - return AbsentResponseDto.toDto(absent, member); + Member member = externalRetrieveMemberUseCase.getById(absent.getMemberId()); + return mapper.toDto(absent, member); })); } @@ -152,7 +154,7 @@ public boolean hasAbsentExcuseHistory(ActivityGroup activityGroup, Member absent @NotNull private ActivityGroup validateAttendanceQRCodeGeneration(Long activityGroupId, Member member) throws PermissionDeniedException, IllegalAccessException { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); if (!activityGroupAdminService.isMemberHasRoleInActivityGroup(member, ActivityGroupRole.LEADER, activityGroupId)) { throw new PermissionDeniedException("해당 그룹의 LEADER만 출석체크 QR을 생성할 수 있습니다."); @@ -164,7 +166,7 @@ private ActivityGroup validateAttendanceQRCodeGeneration(Long activityGroupId, M } private ActivityGroup validateMemberForAttendance(Member member, Long activityGroupId) throws IllegalAccessException, DuplicateAttendanceException { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); if (!activityGroupAdminService.isMemberHasRoleInActivityGroup(member, ActivityGroupRole.MEMBER, activityGroupId)) { throw new IllegalAccessException("해당 그룹의 멤버가 아닙니다. 출석체크 인증을 진행할 수 없습니다."); } @@ -183,7 +185,7 @@ private void validateQRCodeData(String QRCodeData) throws InvalidInformationExce @NotNull private ActivityGroup validateGroupAndMemberForAttendance(Long activityGroupId, Member member) throws IllegalAccessException { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); if (!activityGroupAdminService.isMemberHasRoleInActivityGroup(member, ActivityGroupRole.MEMBER, activityGroupId)) { throw new IllegalAccessException("해당 그룹의 멤버가 아닙니다. 출석체크 인증을 진행할 수 없습니다."); } @@ -194,7 +196,7 @@ private ActivityGroup validateGroupAndMemberForAttendance(Long activityGroupId, } private ActivityGroup getActivityGroupWithValidPermissions(Long activityGroupId, Member member) throws PermissionDeniedException { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); if (!member.isAdminRole() || !activityGroupAdminService.isMemberHasRoleInActivityGroup(member, ActivityGroupRole.LEADER, activityGroupId)) { throw new PermissionDeniedException("그룹의 출석기록을 조회할 권한이 없습니다."); } @@ -202,7 +204,7 @@ private ActivityGroup getActivityGroupWithValidPermissions(Long activityGroupId, } private ActivityGroup getValidActivityGroup(Long activityGroupId) throws IllegalAccessException { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); if (!activityGroup.isProgressing()) { throw new IllegalAccessException("활동이 진행 중인 그룹이 아닙니다. 불참 사유서를 등록할 수 없습니다."); } @@ -225,7 +227,7 @@ private void validateAbsentExcuseConditions(Member absentee, ActivityGroup activ } private ActivityGroup getActivityGroupWithPermissionCheck(Long activityGroupId, Member member) throws PermissionDeniedException { - ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + ActivityGroup activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); if (!member.isAdminRole() && !activityGroupAdminService.isMemberHasRoleInActivityGroup(member, ActivityGroupRole.LEADER, activityGroupId)) { throw new PermissionDeniedException("해당 그룹의 불참사유서를 열람할 권한이 부족합니다."); } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dao/ActivityGroupDetailsRepositoryImpl.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dao/ActivityGroupDetailsRepositoryImpl.java index 76f16a385..33fb3af50 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dao/ActivityGroupDetailsRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dao/ActivityGroupDetailsRepositoryImpl.java @@ -12,6 +12,7 @@ import page.clab.api.domain.activity.activitygroup.domain.QActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.QActivityGroupBoard; import page.clab.api.domain.activity.activitygroup.domain.QGroupMember; +import page.clab.api.domain.activity.activitygroup.dto.mapper.ActivityGroupDtoMapper; import page.clab.api.domain.activity.activitygroup.dto.param.ActivityGroupDetails; import java.util.List; @@ -21,6 +22,7 @@ public class ActivityGroupDetailsRepositoryImpl implements ActivityGroupDetailsRepository { private final JPAQueryFactory queryFactory; + private final ActivityGroupDtoMapper mapper; @Override public ActivityGroupDetails fetchActivityGroupDetails(Long activityGroupId) { @@ -56,6 +58,6 @@ public ActivityGroupDetails fetchActivityGroupDetails(Long activityGroupId) { .where(activityGroupCondition) .fetchOne(); - return ActivityGroupDetails.create(foundActivityGroup, groupMembers, boards); + return mapper.of(foundActivityGroup, groupMembers, boards); } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/mapper/ActivityGroupDtoMapper.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/mapper/ActivityGroupDtoMapper.java new file mode 100644 index 000000000..1c683879b --- /dev/null +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/mapper/ActivityGroupDtoMapper.java @@ -0,0 +1,313 @@ +package page.clab.api.domain.activity.activitygroup.dto.mapper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import page.clab.api.domain.activity.activitygroup.domain.Absent; +import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; +import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; +import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupReport; +import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupStatus; +import page.clab.api.domain.activity.activitygroup.domain.ApplyForm; +import page.clab.api.domain.activity.activitygroup.domain.Attendance; +import page.clab.api.domain.activity.activitygroup.domain.GroupMember; +import page.clab.api.domain.activity.activitygroup.domain.GroupSchedule; +import page.clab.api.domain.activity.activitygroup.dto.param.ActivityGroupDetails; +import page.clab.api.domain.activity.activitygroup.dto.param.GroupScheduleDto; +import page.clab.api.domain.activity.activitygroup.dto.request.AbsentRequestDto; +import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupBoardRequestDto; +import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupReportRequestDto; +import page.clab.api.domain.activity.activitygroup.dto.request.ActivityGroupRequestDto; +import page.clab.api.domain.activity.activitygroup.dto.request.ApplyFormRequestDto; +import page.clab.api.domain.activity.activitygroup.dto.response.AbsentResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupBoardChildResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupBoardReferenceDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupBoardResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupBoardStatusUpdatedResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupDetailResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupMemberWithApplyReasonResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupReportResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.ActivityGroupStatusResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.AssignmentSubmissionWithFeedbackResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.AttendanceResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.FeedbackResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.GroupMemberResponseDto; +import page.clab.api.domain.activity.activitygroup.dto.response.LeaderInfo; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.memberManagement.member.domain.Member; +import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.common.file.dto.mapper.FileDtoMapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class ActivityGroupDtoMapper { + + private final FileDtoMapper mapper; + + public GroupSchedule fromDto(GroupScheduleDto groupScheduleDto, ActivityGroup activityGroup) { + return GroupSchedule.builder() + .activityGroup(activityGroup) + .schedule(groupScheduleDto.getSchedule()) + .content(groupScheduleDto.getContent()) + .build(); + } + + public Absent fromDto(AbsentRequestDto requestDto, Member absentee, ActivityGroup activityGroup) { + return Absent.builder() + .memberId(absentee.getId()) + .activityGroup(activityGroup) + .absentDate(requestDto.getAbsentDate()) + .reason(requestDto.getReason()) + .build(); + } + + public ActivityGroupBoard fromDto(ActivityGroupBoardRequestDto requestDto, Member member, ActivityGroup activityGroup, ActivityGroupBoard parentBoard, List uploadedFiles) { + return ActivityGroupBoard.builder() + .activityGroup(activityGroup) + .memberId(member.getId()) + .category(requestDto.getCategory()) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .parent(parentBoard) + .uploadedFiles(uploadedFiles) + .dueDateTime(requestDto.getDueDateTime()) + .isDeleted(false) + .build(); + } + + public ActivityGroupReport fromDto(ActivityGroupReportRequestDto requestDto, ActivityGroup activityGroup) { + return ActivityGroupReport.builder() + .turn(requestDto.getTurn()) + .activityGroup(activityGroup) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .isDeleted(false) + .build(); + } + + public ActivityGroup fromDto(ActivityGroupRequestDto requestDto) { + return ActivityGroup.builder() + .category(requestDto.getCategory()) + .subject(requestDto.getSubject()) + .name(requestDto.getName()) + .content(requestDto.getContent()) + .status(ActivityGroupStatus.WAITING) + .progress(0L) + .imageUrl(requestDto.getImageUrl()) + .curriculum(requestDto.getCurriculum()) + .startDate(requestDto.getStartDate()) + .endDate(requestDto.getEndDate()) + .techStack(requestDto.getTechStack()) + .githubUrl(requestDto.getGithubUrl()) + .isDeleted(false) + .build(); + } + + public ApplyForm fromDto(ApplyFormRequestDto requestDto, ActivityGroup activityGroup, Member member) { + return ApplyForm.builder() + .activityGroup(activityGroup) + .memberId(member.getId()) + .applyReason(requestDto.getApplyReason()) + .build(); + } + + public GroupScheduleDto toDto(GroupSchedule groupSchedule) { + return GroupScheduleDto.builder() + .schedule(groupSchedule.getSchedule()) + .content(groupSchedule.getContent()) + .build(); + } + + public AbsentResponseDto toDto(Absent absent, Member member) { + return AbsentResponseDto.builder() + .absenteeId(member.getId()) + .absenteeName(member.getName()) + .activityGroupId(absent.getActivityGroup().getId()) + .activityGroupName(absent.getActivityGroup().getName()) + .reason(absent.getReason()) + .absentDate(absent.getAbsentDate()) + .build(); + } + + + public ActivityGroupDetailResponseDto toDto(ActivityGroup activityGroup, List boards, List groupMemberResponseDtos, boolean isOwner) { + return ActivityGroupDetailResponseDto.builder() + .id(activityGroup.getId()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .name(activityGroup.getName()) + .content(activityGroup.getContent()) + .status(activityGroup.getStatus()) + .progress(activityGroup.getProgress()) + .imageUrl(activityGroup.getImageUrl()) + .curriculum(activityGroup.getCurriculum()) + .groupMembers(groupMemberResponseDtos) + .startDate(activityGroup.getStartDate()) + .endDate(activityGroup.getEndDate()) + .techStack(activityGroup.getTechStack()) + .githubUrl(activityGroup.getGithubUrl()) + .activityGroupBoards(boards) + .isOwner(isOwner) + .createdAt(activityGroup.getCreatedAt()) + .build(); + } + + public ActivityGroupMemberWithApplyReasonResponseDto toDto(Member member, GroupMember groupMember, String applyReason) { + return ActivityGroupMemberWithApplyReasonResponseDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .role(groupMember.getRole().toString()) + .status(groupMember.getStatus()) + .applyReason(applyReason) + .build(); + } + + public ActivityGroupReportResponseDto toDto(ActivityGroupReport report) { + return ActivityGroupReportResponseDto.builder() + .activityGroupId(report.getActivityGroup().getId()) + .activityGroupName(report.getActivityGroup().getName()) + .turn(report.getTurn()) + .title(report.getTitle()) + .content(report.getContent()) + .createdAt(report.getCreatedAt()) + .updatedAt(report.getUpdatedAt()) + .build(); + } + + public ActivityGroupResponseDto toDto(ActivityGroup activityGroup) { + return ActivityGroupResponseDto.builder() + .id(activityGroup.getId()) + .name(activityGroup.getName()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .status(activityGroup.getStatus()) + .imageUrl(activityGroup.getImageUrl()) + .createdAt(activityGroup.getCreatedAt()) + .build(); + } + + public ActivityGroupStatusResponseDto toDto(ActivityGroup activityGroup, List leader, Long participantCount, Long weeklyActivityCount) { + return ActivityGroupStatusResponseDto.builder() + .id(activityGroup.getId()) + .name(activityGroup.getName()) + .content(activityGroup.getContent()) + .category(activityGroup.getCategory()) + .subject(activityGroup.getSubject()) + .imageUrl(activityGroup.getImageUrl()) + .leaders(CollectionUtils.isEmpty(leader) ? null : leader) + .participantCount(participantCount) + .weeklyActivityCount(weeklyActivityCount) + .createdAt(activityGroup.getCreatedAt()) + .build(); + } + + public AttendanceResponseDto toDto(Attendance attendance) { + return AttendanceResponseDto.builder() + .activityGroupId(attendance.getActivityGroup().getId()) + .memberId(attendance.getMemberId()) + .attendanceDateTime(attendance.getCreatedAt()) + .build(); + } + + public GroupMemberResponseDto toDto(Member member, GroupMember groupMember) { + return GroupMemberResponseDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .role(groupMember.getRole().getKey()) + .status(groupMember.getStatus()) + .build(); + } + + public ActivityGroupBoardChildResponseDto toChildDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfoDto, List childrenDtos) { + return ActivityGroupBoardChildResponseDto.builder() + .id(board.getId()) + .memberId(memberBasicInfoDto.getMemberId()) + .memberName(memberBasicInfoDto.getMemberName()) + .category(board.getCategory()) + .title(board.getTitle()) + .content(board.getContent()) + .dueDateTime(board.getDueDateTime()) + .updatedAt(board.getUpdatedAt()) + .createdAt(board.getCreatedAt()) + .files(mapper.toDto(board.getUploadedFiles())) + .children(childrenDtos) + .build(); + } + + public AssignmentSubmissionWithFeedbackResponseDto toAssignmentDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfo, List feedbackDtos) { + return AssignmentSubmissionWithFeedbackResponseDto.builder() + .id(board.getId()) + .memberId(memberBasicInfo.getMemberId()) + .memberName(memberBasicInfo.getMemberName()) + .parentId(board.getParent() != null ? board.getParent().getId() : null) + .content(board.getContent()) + .files(mapper.toDto(board.getUploadedFiles())) + .createdAt(board.getCreatedAt()) + .updatedAt(board.getUpdatedAt()) + .feedbacks(feedbackDtos) + .build(); + } + + public ActivityGroupBoardResponseDto toBoardDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfoDto) { + return ActivityGroupBoardResponseDto.builder() + .id(board.getId()) + .memberId(memberBasicInfoDto.getMemberId()) + .memberName(memberBasicInfoDto.getMemberName()) + .parentId(board.getParent() != null ? board.getParent().getId() : null) + .category(board.getCategory()) + .title(board.getTitle()) + .content(board.getContent()) + .files(mapper.toDto(board.getUploadedFiles())) + .dueDateTime(board.getDueDateTime()) + .createdAt(board.getCreatedAt()) + .updatedAt(board.getUpdatedAt()) + .build(); + } + + public FeedbackResponseDto toFeedbackDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfo) { + return FeedbackResponseDto.builder() + .id(board.getId()) + .memberId(memberBasicInfo.getMemberId()) + .memberName(memberBasicInfo.getMemberName()) + .content(board.getContent()) + .files(mapper.toDto(board.getUploadedFiles())) + .createdAt(board.getCreatedAt()) + .updatedAt(board.getUpdatedAt()) + .build(); + } + + public ActivityGroupDetails of(ActivityGroup activityGroup, List groupMembers, List activityGroupBoards) { + return ActivityGroupDetails.builder() + .activityGroup(activityGroup) + .groupMembers(groupMembers) + .activityGroupBoards(activityGroupBoards) + .build(); + } + + public ActivityGroupBoardStatusUpdatedResponseDto of(Long groupId, ActivityGroupStatus activityGroupStatus) { + return ActivityGroupBoardStatusUpdatedResponseDto.builder() + .id(groupId) + .activityGroupStatus(activityGroupStatus) + .build(); + } + + public ActivityGroupBoardReferenceDto of(Long id, Long groupId, Long parentId) { + return ActivityGroupBoardReferenceDto.builder() + .id(id) + .groupId(groupId) + .parentId(parentId) + .build(); + } + + public LeaderInfo create(Member leader, LocalDateTime createdAt) { + return LeaderInfo.builder() + .id(leader.getId()) + .name(leader.getName()) + .createdAt(createdAt) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/ActivityGroupDetails.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/ActivityGroupDetails.java index 8b9cf0d67..a50eed5a6 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/ActivityGroupDetails.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/ActivityGroupDetails.java @@ -1,7 +1,7 @@ package page.clab.api.domain.activity.activitygroup.dto.param; +import lombok.Builder; import lombok.Getter; -import lombok.Setter; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; import page.clab.api.domain.activity.activitygroup.domain.GroupMember; @@ -9,20 +9,10 @@ import java.util.List; @Getter -@Setter +@Builder public class ActivityGroupDetails { private ActivityGroup activityGroup; private List groupMembers; private List activityGroupBoards; - - private ActivityGroupDetails(ActivityGroup activityGroup, List groupMembers, List activityGroupBoards) { - this.activityGroup = activityGroup; - this.groupMembers = groupMembers; - this.activityGroupBoards = activityGroupBoards; - } - - public static ActivityGroupDetails create(ActivityGroup activityGroup, List groupMembers, List activityGroupBoards) { - return new ActivityGroupDetails(activityGroup, groupMembers, activityGroupBoards); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/GroupScheduleDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/GroupScheduleDto.java index 248331a9f..f0c9b41f1 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/GroupScheduleDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/param/GroupScheduleDto.java @@ -3,8 +3,6 @@ import lombok.Builder; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.activity.activitygroup.domain.GroupSchedule; import java.time.LocalDateTime; @@ -15,19 +13,4 @@ public class GroupScheduleDto { private LocalDateTime schedule; private String content; - - public static GroupScheduleDto toDto(GroupSchedule groupSchedule) { - return GroupScheduleDto.builder() - .schedule(groupSchedule.getSchedule()) - .content(groupSchedule.getContent()) - .build(); - } - - public static GroupSchedule toEntity(GroupScheduleDto groupScheduleDto, ActivityGroup activityGroup) { - return GroupSchedule.builder() - .activityGroup(activityGroup) - .schedule(groupScheduleDto.getSchedule()) - .content(groupScheduleDto.getContent()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/AbsentRequestDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/AbsentRequestDto.java index c0b7fc008..933766213 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/AbsentRequestDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/AbsentRequestDto.java @@ -4,9 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.Absent; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.memberManagement.member.domain.Member; import java.time.LocalDate; @@ -29,14 +26,4 @@ public class AbsentRequestDto { @NotNull(message = "{notNull.absent.absentDate}") @Schema(description = "불참 날짜", example = "2023-11-12", required = true) private LocalDate absentDate; - - public static Absent toEntity(AbsentRequestDto requestDto, Member absentee, ActivityGroup activityGroup) { - return Absent.builder() - .memberId(absentee.getId()) - .activityGroup(activityGroup) - .absentDate(requestDto.getAbsentDate()) - .reason(requestDto.getReason()) - .build(); - - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupBoardRequestDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupBoardRequestDto.java index fe75afce5..fc7098303 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupBoardRequestDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupBoardRequestDto.java @@ -4,11 +4,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoardCategory; -import page.clab.api.domain.memberManagement.member.domain.Member; -import page.clab.api.global.common.file.domain.UploadedFile; import java.time.LocalDateTime; import java.util.List; @@ -32,18 +28,4 @@ public class ActivityGroupBoardRequestDto { @Schema(description = "마감일자", example = "2024-11-28 18:00:00.000") private LocalDateTime dueDateTime; - - public static ActivityGroupBoard toEntity(ActivityGroupBoardRequestDto requestDto, Member member, ActivityGroup activityGroup, ActivityGroupBoard parentBoard, List uploadedFiles) { - return ActivityGroupBoard.builder() - .activityGroup(activityGroup) - .memberId(member.getId()) - .category(requestDto.getCategory()) - .title(requestDto.getTitle()) - .content(requestDto.getContent()) - .parent(parentBoard) - .uploadedFiles(uploadedFiles) - .dueDateTime(requestDto.getDueDateTime()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupReportRequestDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupReportRequestDto.java index f09687c5a..ad45f841a 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupReportRequestDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupReportRequestDto.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupReport; @Getter @Setter @@ -26,14 +24,4 @@ public class ActivityGroupReportRequestDto { @NotNull(message = "notNull.report.content") @Schema(description = "내용", example = "변수, 자료형에 대해 공부", required = true) private String content; - - public static ActivityGroupReport toEntity(ActivityGroupReportRequestDto requestDto, ActivityGroup activityGroup) { - return ActivityGroupReport.builder() - .turn(requestDto.getTurn()) - .activityGroup(activityGroup) - .title(requestDto.getTitle()) - .content(requestDto.getContent()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupRequestDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupRequestDto.java index fae9ea85b..93c4934ab 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupRequestDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ActivityGroupRequestDto.java @@ -4,9 +4,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupCategory; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupStatus; import java.time.LocalDate; @@ -46,22 +44,4 @@ public class ActivityGroupRequestDto { @Schema(description = "Github URL", example = "https://github.com/KGU-C-Lab") private String githubUrl; - - public static ActivityGroup toEntity(ActivityGroupRequestDto requestDto) { - return ActivityGroup.builder() - .category(requestDto.getCategory()) - .subject(requestDto.getSubject()) - .name(requestDto.getName()) - .content(requestDto.getContent()) - .status(ActivityGroupStatus.WAITING) - .progress(0L) - .imageUrl(requestDto.getImageUrl()) - .curriculum(requestDto.getCurriculum()) - .startDate(requestDto.getStartDate()) - .endDate(requestDto.getEndDate()) - .techStack(requestDto.getTechStack()) - .githubUrl(requestDto.getGithubUrl()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ApplyFormRequestDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ApplyFormRequestDto.java index 9edfbc2c8..be4618936 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ApplyFormRequestDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/request/ApplyFormRequestDto.java @@ -4,9 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.activity.activitygroup.domain.ApplyForm; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Setter @@ -15,12 +12,4 @@ public class ApplyFormRequestDto { @NotNull(message = "{notnull.applyForm.applyReason}") @Schema(description = "지원 동기", example = "백엔드에 관심이 있어서") private String applyReason; - - public static ApplyForm toEntity(ApplyFormRequestDto requestDto, ActivityGroup activityGroup, Member member) { - return ApplyForm.builder() - .activityGroup(activityGroup) - .memberId(member.getId()) - .applyReason(requestDto.getApplyReason()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AbsentResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AbsentResponseDto.java index 78728e818..522a660a8 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AbsentResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AbsentResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.Absent; -import page.clab.api.domain.memberManagement.member.domain.Member; import java.time.LocalDate; @@ -17,15 +15,4 @@ public class AbsentResponseDto { private String activityGroupName; private String reason; private LocalDate absentDate; - - public static AbsentResponseDto toDto(Absent absent, Member member) { - return AbsentResponseDto.builder() - .absenteeId(member.getId()) - .absenteeName(member.getName()) - .activityGroupId(absent.getActivityGroup().getId()) - .activityGroupName(absent.getActivityGroup().getName()) - .reason(absent.getReason()) - .absentDate(absent.getAbsentDate()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardChildResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardChildResponseDto.java index dd2ecbb84..f817f6fc6 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardChildResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardChildResponseDto.java @@ -2,10 +2,7 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoardCategory; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; @@ -26,20 +23,4 @@ public class ActivityGroupBoardChildResponseDto { private LocalDateTime createdAt; private List files; private List children; - - public static ActivityGroupBoardChildResponseDto toDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfoDto, List childrenDtos) { - return ActivityGroupBoardChildResponseDto.builder() - .id(board.getId()) - .memberId(memberBasicInfoDto.getMemberId()) - .memberName(memberBasicInfoDto.getMemberName()) - .category(board.getCategory()) - .title(board.getTitle()) - .content(board.getContent()) - .dueDateTime(board.getDueDateTime()) - .updatedAt(board.getUpdatedAt()) - .createdAt(board.getCreatedAt()) - .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) - .children(childrenDtos) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardReferenceDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardReferenceDto.java index 536ef9f75..5dd4481f0 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardReferenceDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardReferenceDto.java @@ -10,12 +10,4 @@ public class ActivityGroupBoardReferenceDto { private Long id; private Long groupId; private Long parentId; - - public static ActivityGroupBoardReferenceDto toDto(Long id, Long groupId, Long parentId) { - return ActivityGroupBoardReferenceDto.builder() - .id(id) - .groupId(groupId) - .parentId(parentId) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardResponseDto.java index 200c9b001..b39f8721e 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardResponseDto.java @@ -2,11 +2,7 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoardCategory; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; -import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; @@ -27,20 +23,4 @@ public class ActivityGroupBoardResponseDto { private LocalDateTime dueDateTime; private LocalDateTime createdAt; private LocalDateTime updatedAt; - - public static ActivityGroupBoardResponseDto toDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfoDto) { - return ActivityGroupBoardResponseDto.builder() - .id(board.getId()) - .memberId(memberBasicInfoDto.getMemberId()) - .memberName(memberBasicInfoDto.getMemberName()) - .parentId(board.getParent() != null ? board.getParent().getId() : null) - .category(board.getCategory()) - .title(board.getTitle()) - .content(board.getContent()) - .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) - .dueDateTime(board.getDueDateTime()) - .createdAt(board.getCreatedAt()) - .updatedAt(board.getUpdatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardStatusUpdatedResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardStatusUpdatedResponseDto.java index 2e656022f..896a3609d 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardStatusUpdatedResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardStatusUpdatedResponseDto.java @@ -10,11 +10,4 @@ public class ActivityGroupBoardStatusUpdatedResponseDto { private Long id; private ActivityGroupStatus activityGroupStatus; - - public static ActivityGroupBoardStatusUpdatedResponseDto toDto(Long groupId, ActivityGroupStatus activityGroupStatus) { - return ActivityGroupBoardStatusUpdatedResponseDto.builder() - .id(groupId) - .activityGroupStatus(activityGroupStatus) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardUpdateResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardUpdateResponseDto.java index be85edd46..e16fee33a 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardUpdateResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupBoardUpdateResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; @Getter @Builder @@ -10,11 +9,4 @@ public class ActivityGroupBoardUpdateResponseDto { private Long id; private Long parentId; - - public static ActivityGroupBoardUpdateResponseDto toDto(ActivityGroupBoard board) { - return ActivityGroupBoardUpdateResponseDto.builder() - .id(board.getId()) - .parentId(board.getParent() != null ? board.getParent().getId() : null) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupDetailResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupDetailResponseDto.java index 557c187e3..b78140cd5 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupDetailResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupDetailResponseDto.java @@ -6,10 +6,8 @@ import java.util.List; import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupCategory; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupStatus; -import page.clab.api.domain.activity.activitygroup.domain.GroupMember; @Getter @Builder @@ -34,27 +32,4 @@ public class ActivityGroupDetailResponseDto { @JsonProperty("isOwner") private boolean isOwner; private LocalDateTime createdAt; - - public static ActivityGroupDetailResponseDto create(ActivityGroup activityGroup, - List boards, List groupMemberResponseDtos, boolean isOwner) { - return ActivityGroupDetailResponseDto.builder() - .id(activityGroup.getId()) - .category(activityGroup.getCategory()) - .subject(activityGroup.getSubject()) - .name(activityGroup.getName()) - .content(activityGroup.getContent()) - .status(activityGroup.getStatus()) - .progress(activityGroup.getProgress()) - .imageUrl(activityGroup.getImageUrl()) - .curriculum(activityGroup.getCurriculum()) - .groupMembers(groupMemberResponseDtos) - .startDate(activityGroup.getStartDate()) - .endDate(activityGroup.getEndDate()) - .techStack(activityGroup.getTechStack()) - .githubUrl(activityGroup.getGithubUrl()) - .activityGroupBoards(boards) - .isOwner(isOwner) - .createdAt(activityGroup.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java index c59952c78..dbfb0ae18 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupMemberWithApplyReasonResponseDto.java @@ -2,9 +2,7 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.activity.activitygroup.domain.GroupMemberStatus; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -15,14 +13,4 @@ public class ActivityGroupMemberWithApplyReasonResponseDto { private String role; private GroupMemberStatus status; private String applyReason; - - public static ActivityGroupMemberWithApplyReasonResponseDto create(Member member, GroupMember groupMember, String applyReason) { - return ActivityGroupMemberWithApplyReasonResponseDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .role(groupMember.getRole().toString()) - .status(groupMember.getStatus()) - .applyReason(applyReason) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupReportResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupReportResponseDto.java index f2c0efbfb..3e06ff955 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupReportResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupReportResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupReport; import java.time.LocalDateTime; @@ -17,16 +16,4 @@ public class ActivityGroupReportResponseDto { private String content; private LocalDateTime createdAt; private LocalDateTime updatedAt; - - public static ActivityGroupReportResponseDto toDto(ActivityGroupReport report) { - return ActivityGroupReportResponseDto.builder() - .activityGroupId(report.getActivityGroup().getId()) - .activityGroupName(report.getActivityGroup().getName()) - .turn(report.getTurn()) - .title(report.getTitle()) - .content(report.getContent()) - .createdAt(report.getCreatedAt()) - .updatedAt(report.getUpdatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupResponseDto.java index 7bb4109b2..2709c70f7 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupCategory; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupStatus; @@ -19,16 +18,4 @@ public class ActivityGroupResponseDto { private ActivityGroupStatus status; private String imageUrl; private LocalDateTime createdAt; - - public static ActivityGroupResponseDto toDto(ActivityGroup activityGroup) { - return ActivityGroupResponseDto.builder() - .id(activityGroup.getId()) - .name(activityGroup.getName()) - .category(activityGroup.getCategory()) - .subject(activityGroup.getSubject()) - .status(activityGroup.getStatus()) - .imageUrl(activityGroup.getImageUrl()) - .createdAt(activityGroup.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupStatusResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupStatusResponseDto.java index b800dc654..9033fa48c 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupStatusResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/ActivityGroupStatusResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import org.springframework.util.CollectionUtils; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupCategory; import java.time.LocalDateTime; @@ -23,19 +21,4 @@ public class ActivityGroupStatusResponseDto { private Long participantCount; private Long weeklyActivityCount; private LocalDateTime createdAt; - - public static ActivityGroupStatusResponseDto toDto(ActivityGroup activityGroup, List leader, Long participantCount, Long weeklyActivityCount) { - return ActivityGroupStatusResponseDto.builder() - .id(activityGroup.getId()) - .name(activityGroup.getName()) - .content(activityGroup.getContent()) - .category(activityGroup.getCategory()) - .subject(activityGroup.getSubject()) - .imageUrl(activityGroup.getImageUrl()) - .leaders(CollectionUtils.isEmpty(leader) ? null : leader) - .participantCount(participantCount) - .weeklyActivityCount(weeklyActivityCount) - .createdAt(activityGroup.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java index f8dcbbc67..a03991ba3 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AssignmentSubmissionWithFeedbackResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; @@ -22,18 +20,4 @@ public class AssignmentSubmissionWithFeedbackResponseDto { private LocalDateTime createdAt; private LocalDateTime updatedAt; private List feedbacks; - - public static AssignmentSubmissionWithFeedbackResponseDto toDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfo, List feedbackDtos) { - return AssignmentSubmissionWithFeedbackResponseDto.builder() - .id(board.getId()) - .memberId(memberBasicInfo.getMemberId()) - .memberName(memberBasicInfo.getMemberName()) - .parentId(board.getParent() != null ? board.getParent().getId() : null) - .content(board.getContent()) - .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) - .createdAt(board.getCreatedAt()) - .updatedAt(board.getUpdatedAt()) - .feedbacks(feedbackDtos) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AttendanceResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AttendanceResponseDto.java index 508dff60e..01e43dbb6 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AttendanceResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/AttendanceResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.Attendance; import java.time.LocalDateTime; @@ -13,12 +12,4 @@ public class AttendanceResponseDto { private Long activityGroupId; private String memberId; private LocalDateTime attendanceDateTime; - - public static AttendanceResponseDto toDto(Attendance attendance) { - return AttendanceResponseDto.builder() - .activityGroupId(attendance.getActivityGroup().getId()) - .memberId(attendance.getMemberId()) - .attendanceDateTime(attendance.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/FeedbackResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/FeedbackResponseDto.java index 021b242e2..7da6e03fc 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/FeedbackResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/FeedbackResponseDto.java @@ -2,10 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupBoard; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; -import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; @@ -22,16 +18,4 @@ public class FeedbackResponseDto { private List files; private LocalDateTime createdAt; private LocalDateTime updatedAt; - - public static FeedbackResponseDto toDto(ActivityGroupBoard board, MemberBasicInfoDto memberBasicInfo) { - return FeedbackResponseDto.builder() - .id(board.getId()) - .memberId(memberBasicInfo.getMemberId()) - .memberName(memberBasicInfo.getMemberName()) - .content(board.getContent()) - .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) - .createdAt(board.getCreatedAt()) - .updatedAt(board.getUpdatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/GroupMemberResponseDto.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/GroupMemberResponseDto.java index 74334928e..ee6bad669 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/GroupMemberResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/GroupMemberResponseDto.java @@ -2,9 +2,7 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.activity.activitygroup.domain.GroupMemberStatus; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -14,13 +12,4 @@ public class GroupMemberResponseDto { private String memberName; private String role; private GroupMemberStatus status; - - public static GroupMemberResponseDto toDto(Member member, GroupMember groupMember) { - return GroupMemberResponseDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .role(groupMember.getRole().getKey()) - .status(groupMember.getStatus()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/LeaderInfo.java b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/LeaderInfo.java index 6f612aee2..b96f293a8 100644 --- a/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/LeaderInfo.java +++ b/src/main/java/page/clab/api/domain/activity/activitygroup/dto/response/LeaderInfo.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; import java.time.LocalDateTime; @@ -16,12 +15,4 @@ public class LeaderInfo { @JsonIgnore private LocalDateTime createdAt; - - public static LeaderInfo create(Member leader, LocalDateTime createdAt) { - return LeaderInfo.builder() - .id(leader.getId()) - .name(leader.getName()) - .createdAt(createdAt) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewMapper.java b/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewMapper.java index 7f47e6417..2a28cb973 100644 --- a/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewMapper.java +++ b/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface ReviewMapper { - ReviewJpaEntity toJpaEntity(Review review); + ReviewJpaEntity toEntity(Review review); - Review toDomainEntity(ReviewJpaEntity entity); + Review toDomain(ReviewJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewPersistenceAdapter.java b/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewPersistenceAdapter.java index d6103f682..f55547f76 100644 --- a/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/activity/review/adapter/out/persistence/ReviewPersistenceAdapter.java @@ -21,28 +21,28 @@ public class ReviewPersistenceAdapter implements @Override public Review save(Review review) { - ReviewJpaEntity entity = mapper.toJpaEntity(review); + ReviewJpaEntity entity = mapper.toEntity(review); ReviewJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override - public Review findByIdOrThrow(Long reviewId) { + public Review getById(Long reviewId) { return repository.findById(reviewId) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[Review] id: " + reviewId + "에 해당하는 리뷰가 존재하지 않습니다.")); } @Override public Page findAllByIsDeletedTrue(Pageable pageable) { return repository.findAllByIsDeletedTrue(pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public Page findAllByMemberId(String memberId, Pageable pageable) { return repository.findAllByMemberId(memberId, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override @@ -53,6 +53,6 @@ public boolean existsByMemberIdAndActivityGroup(String memberId, ActivityGroup a @Override public Page findByConditions(String memberId, String memberName, Long activityId, Boolean isPublic, Pageable pageable) { return repository.findByConditions(memberId, memberName, activityId, isPublic, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/activity/review/application/dto/mapper/ReviewDtoMapper.java b/src/main/java/page/clab/api/domain/activity/review/application/dto/mapper/ReviewDtoMapper.java new file mode 100644 index 000000000..dff125ec1 --- /dev/null +++ b/src/main/java/page/clab/api/domain/activity/review/application/dto/mapper/ReviewDtoMapper.java @@ -0,0 +1,39 @@ +package page.clab.api.domain.activity.review.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; +import page.clab.api.domain.activity.review.application.dto.request.ReviewRequestDto; +import page.clab.api.domain.activity.review.application.dto.response.ReviewResponseDto; +import page.clab.api.domain.activity.review.domain.Review; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberReviewInfoDto; + +@Component +public class ReviewDtoMapper { + + public Review fromDto(ReviewRequestDto requestDto, String memberId, ActivityGroup activityGroup) { + return Review.builder() + .activityGroup(activityGroup) + .memberId(memberId) + .content(requestDto.getContent()) + .isPublic(false) + .isDeleted(false) + .build(); + } + + public ReviewResponseDto toDto(Review review, MemberReviewInfoDto reviewer, boolean isOwner) { + ActivityGroup activityGroup = review.getActivityGroup(); + return ReviewResponseDto.builder() + .id(review.getId()) + .activityGroupId(activityGroup.getId()) + .activityGroupName(activityGroup.getName()) + .activityGroupCategory(String.valueOf(activityGroup.getCategory())) + .memberId(reviewer.getMemberId()) + .name(reviewer.getMemberName()) + .department(reviewer.getDepartment()) + .content(review.getContent()) + .isPublic(review.getIsPublic()) + .isOwner(isOwner) + .createdAt(review.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/activity/review/application/dto/request/ReviewRequestDto.java b/src/main/java/page/clab/api/domain/activity/review/application/dto/request/ReviewRequestDto.java index 815142fc6..3d255c500 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/dto/request/ReviewRequestDto.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/dto/request/ReviewRequestDto.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.activity.review.domain.Review; @Getter @Setter @@ -19,14 +17,4 @@ public class ReviewRequestDto { @Schema(description = "후기", example = "C-Lab에는 다양한 분야에 대한 열정있는 사람들이 모이기 때문에 다양하고 활동적인 스터디 그룹과 프로젝트 팀이 있습니다. " + "신입생이라도 자유롭게 스터디 그룹을 만들고 사람들을 모아서 원하는 분야와 관련된 기술을 알아보고 같이 공부하기 좋습니다!", required = true) private String content; - - public static Review toEntity(ReviewRequestDto requestDto, String memberId, ActivityGroup activityGroup) { - return Review.builder() - .activityGroup(activityGroup) - .memberId(memberId) - .content(requestDto.getContent()) - .isPublic(false) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/review/application/dto/response/ReviewResponseDto.java b/src/main/java/page/clab/api/domain/activity/review/application/dto/response/ReviewResponseDto.java index d69e6036f..c5e3e85af 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/dto/response/ReviewResponseDto.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/dto/response/ReviewResponseDto.java @@ -2,9 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.activity.review.domain.Review; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberReviewInfoDto; import java.time.LocalDateTime; @@ -23,21 +20,4 @@ public class ReviewResponseDto { private Boolean isPublic; private Boolean isOwner; private LocalDateTime createdAt; - - public static ReviewResponseDto toDto(Review review, MemberReviewInfoDto reviewer, boolean isOwner) { - ActivityGroup activityGroup = review.getActivityGroup(); - return ReviewResponseDto.builder() - .id(review.getId()) - .activityGroupId(activityGroup.getId()) - .activityGroupName(activityGroup.getName()) - .activityGroupCategory(String.valueOf(activityGroup.getCategory())) - .memberId(reviewer.getMemberId()) - .name(reviewer.getMemberName()) - .department(reviewer.getDepartment()) - .content(review.getContent()) - .isPublic(review.getIsPublic()) - .isOwner(isOwner) - .createdAt(review.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/activity/review/application/port/out/RetrieveReviewPort.java b/src/main/java/page/clab/api/domain/activity/review/application/port/out/RetrieveReviewPort.java index e0156a97e..2b35088fd 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/port/out/RetrieveReviewPort.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/port/out/RetrieveReviewPort.java @@ -7,7 +7,7 @@ public interface RetrieveReviewPort { - Review findByIdOrThrow(Long reviewId); + Review getById(Long reviewId); Page findAllByIsDeletedTrue(Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/activity/review/application/service/DeletedReviewsRetrievalService.java b/src/main/java/page/clab/api/domain/activity/review/application/service/DeletedReviewsRetrievalService.java index dbf1177ae..45c2d8dee 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/service/DeletedReviewsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/service/DeletedReviewsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.activity.review.application.dto.mapper.ReviewDtoMapper; import page.clab.api.domain.activity.review.application.dto.response.ReviewResponseDto; import page.clab.api.domain.activity.review.application.port.in.RetrieveDeletedReviewsUseCase; import page.clab.api.domain.activity.review.application.port.out.RetrieveReviewPort; @@ -19,6 +20,7 @@ public class DeletedReviewsRetrievalService implements RetrieveDeletedReviewsUse private final RetrieveReviewPort retrieveReviewPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final ReviewDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -27,7 +29,7 @@ public PagedResponseDto retrieveDeletedReviews(Pageable pagea Page reviews = retrieveReviewPort.findAllByIsDeletedTrue(pageable); return new PagedResponseDto<>(reviews.map(review -> { MemberReviewInfoDto reviewer = externalRetrieveMemberUseCase.getMemberReviewInfoById(review.getMemberId()); - return ReviewResponseDto.toDto(review, reviewer, review.isOwner(currentMemberId)); + return mapper.toDto(review, reviewer, review.isOwner(currentMemberId)); })); } } diff --git a/src/main/java/page/clab/api/domain/activity/review/application/service/MyReviewsRetrievalService.java b/src/main/java/page/clab/api/domain/activity/review/application/service/MyReviewsRetrievalService.java index 30f56a76b..2f0cebd01 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/service/MyReviewsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/service/MyReviewsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.activity.review.application.dto.mapper.ReviewDtoMapper; import page.clab.api.domain.activity.review.application.dto.response.ReviewResponseDto; import page.clab.api.domain.activity.review.application.port.in.RetrieveMyReviewsUseCase; import page.clab.api.domain.activity.review.application.port.out.RetrieveReviewPort; @@ -19,6 +20,7 @@ public class MyReviewsRetrievalService implements RetrieveMyReviewsUseCase { private final RetrieveReviewPort retrieveReviewPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final ReviewDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -27,7 +29,7 @@ public PagedResponseDto retrieveMyReviews(Pageable pageable) Page reviews = retrieveReviewPort.findAllByMemberId(currentMemberId, pageable); return new PagedResponseDto<>(reviews.map(review -> { MemberReviewInfoDto reviewer = externalRetrieveMemberUseCase.getMemberReviewInfoById(review.getMemberId()); - return ReviewResponseDto.toDto(review, reviewer, review.isOwner(currentMemberId)); + return mapper.toDto(review, reviewer, review.isOwner(currentMemberId)); })); } } diff --git a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRegisterService.java b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRegisterService.java index 9071d453b..ca2413a5f 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRegisterService.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRegisterService.java @@ -9,6 +9,7 @@ import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupRole; import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.activity.activitygroup.exception.ActivityGroupNotFinishedException; +import page.clab.api.domain.activity.review.application.dto.mapper.ReviewDtoMapper; import page.clab.api.domain.activity.review.application.dto.request.ReviewRequestDto; import page.clab.api.domain.activity.review.application.exception.AlreadyReviewedException; import page.clab.api.domain.activity.review.application.port.in.RegisterReviewUseCase; @@ -30,14 +31,15 @@ public class ReviewRegisterService implements RegisterReviewUseCase { private final ActivityGroupMemberService activityGroupMemberService; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final ReviewDtoMapper mapper; @Transactional @Override public Long registerReview(ReviewRequestDto requestDto) { MemberBasicInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo(); - ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupByIdOrThrow(requestDto.getActivityGroupId()); + ActivityGroup activityGroup = activityGroupMemberService.getActivityGroupById(requestDto.getActivityGroupId()); validateReviewCreationPermission(activityGroup, currentMemberInfo.getMemberId()); - Review review = ReviewRequestDto.toEntity(requestDto, currentMemberInfo.getMemberId(), activityGroup); + Review review = mapper.fromDto(requestDto, currentMemberInfo.getMemberId(), activityGroup); notifyGroupLeaderOfNewReview(activityGroup, currentMemberInfo.getMemberName()); return registerReviewPort.save(review).getId(); } diff --git a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRemoveService.java b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRemoveService.java index 63ce2eb3f..130ad7d77 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRemoveService.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewRemoveService.java @@ -23,7 +23,7 @@ public class ReviewRemoveService implements RemoveReviewUseCase { @Override public Long removeReview(Long reviewId) throws PermissionDeniedException { MemberDetailedInfoDto currentMember = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Review review = retrieveReviewPort.findByIdOrThrow(reviewId); + Review review = retrieveReviewPort.getById(reviewId); review.validateAccessPermission(currentMember); review.delete(); return registerReviewPort.save(review).getId(); diff --git a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewUpdateService.java b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewUpdateService.java index e0c41281f..301288f88 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewUpdateService.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewUpdateService.java @@ -24,7 +24,7 @@ public class ReviewUpdateService implements UpdateReviewUseCase { @Override public Long updateReview(Long reviewId, ReviewUpdateRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMember = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Review review = retrieveReviewPort.findByIdOrThrow(reviewId); + Review review = retrieveReviewPort.getById(reviewId); review.validateAccessPermission(currentMember); review.update(requestDto); return registerReviewPort.save(review).getId(); diff --git a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewsByConditionsRetrievalService.java index 80291b926..4d97fc901 100644 --- a/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewsByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/activity/review/application/service/ReviewsByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.activity.review.application.dto.mapper.ReviewDtoMapper; import page.clab.api.domain.activity.review.application.dto.response.ReviewResponseDto; import page.clab.api.domain.activity.review.application.port.in.RetrieveReviewsByConditionsUseCase; import page.clab.api.domain.activity.review.application.port.out.RetrieveReviewPort; @@ -19,6 +20,7 @@ public class ReviewsByConditionsRetrievalService implements RetrieveReviewsByCon private final RetrieveReviewPort retrieveReviewPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final ReviewDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -27,7 +29,7 @@ public PagedResponseDto retrieveReviews(String memberId, Stri Page reviews = retrieveReviewPort.findByConditions(memberId, memberName, activityId, isPublic, pageable); return new PagedResponseDto<>(reviews.map(review -> { MemberReviewInfoDto reviewer = externalRetrieveMemberUseCase.getMemberReviewInfoById(review.getMemberId()); - return ReviewResponseDto.toDto(review, reviewer, review.isOwner(currentMemberId)); + return mapper.toDto(review, reviewer, review.isOwner(currentMemberId)); })); } } diff --git a/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogMapper.java b/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogMapper.java index a39abc90c..fc3a9e682 100644 --- a/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogMapper.java +++ b/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface AccountAccessLogMapper { - AccountAccessLogJpaEntity toJpaEntity(AccountAccessLog accountAccessLog); + AccountAccessLogJpaEntity toEntity(AccountAccessLog accountAccessLog); - AccountAccessLog toDomainEntity(AccountAccessLogJpaEntity jpaEntity); + AccountAccessLog toDomain(AccountAccessLogJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogPersistenceAdapter.java b/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogPersistenceAdapter.java index 3054d507b..4220791df 100644 --- a/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/auth/accountAccessLog/adapter/out/persistence/AccountAccessLogPersistenceAdapter.java @@ -15,8 +15,8 @@ public class AccountAccessLogPersistenceAdapter implements @Override public AccountAccessLog save(AccountAccessLog accountAccessLog) { - AccountAccessLogJpaEntity jpaEntity = accountAccessLogMapper.toJpaEntity(accountAccessLog); + AccountAccessLogJpaEntity jpaEntity = accountAccessLogMapper.toEntity(accountAccessLog); AccountAccessLogJpaEntity savedEntity = accountAccessLogRepository.save(jpaEntity); - return accountAccessLogMapper.toDomainEntity(savedEntity); + return accountAccessLogMapper.toDomain(savedEntity); } } diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoMapper.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoMapper.java index c0f4d6e64..6ef43fca9 100644 --- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoMapper.java +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface AccountLockInfoMapper { - AccountLockInfoJpaEntity toJpaEntity(AccountLockInfo accountLockInfo); + AccountLockInfoJpaEntity toEntity(AccountLockInfo accountLockInfo); - AccountLockInfo toDomainEntity(AccountLockInfoJpaEntity jpaEntity); + AccountLockInfo toDomain(AccountLockInfoJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoPersistenceAdapter.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoPersistenceAdapter.java index fae7ce718..59691005a 100644 --- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/adapter/out/persistence/AccountLockInfoPersistenceAdapter.java @@ -24,24 +24,24 @@ public class AccountLockInfoPersistenceAdapter implements @Override public AccountLockInfo save(AccountLockInfo accountLockInfo) { - AccountLockInfoJpaEntity jpaEntity = accountLockInfoMapper.toJpaEntity(accountLockInfo); + AccountLockInfoJpaEntity jpaEntity = accountLockInfoMapper.toEntity(accountLockInfo); AccountLockInfoJpaEntity savedEntity = accountLockInfoRepository.save(jpaEntity); - return accountLockInfoMapper.toDomainEntity(savedEntity); + return accountLockInfoMapper.toDomain(savedEntity); } @Override public Optional findByMemberId(String memberId) { - return accountLockInfoRepository.findByMemberId(memberId).map(accountLockInfoMapper::toDomainEntity); + return accountLockInfoRepository.findByMemberId(memberId).map(accountLockInfoMapper::toDomain); } @Override public void delete(AccountLockInfo accountLockInfo) { - AccountLockInfoJpaEntity jpaEntity = accountLockInfoMapper.toJpaEntity(accountLockInfo); + AccountLockInfoJpaEntity jpaEntity = accountLockInfoMapper.toEntity(accountLockInfo); accountLockInfoRepository.delete(jpaEntity); } public Page findByLockUntil(LocalDateTime banDate, Pageable pageable) { Page jpaEntities = accountLockInfoRepository.findByLockUntil(banDate, pageable); - return jpaEntities.map(accountLockInfoMapper::toDomainEntity); + return jpaEntities.map(accountLockInfoMapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/mapper/AccountLockInfoDtoMapper.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/mapper/AccountLockInfoDtoMapper.java new file mode 100644 index 000000000..f96f7b5c2 --- /dev/null +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/mapper/AccountLockInfoDtoMapper.java @@ -0,0 +1,16 @@ +package page.clab.api.domain.auth.accountLockInfo.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.auth.accountLockInfo.application.dto.response.AccountLockInfoResponseDto; +import page.clab.api.domain.auth.accountLockInfo.domain.AccountLockInfo; + +@Component +public class AccountLockInfoDtoMapper { + + public AccountLockInfoResponseDto toDto(AccountLockInfo accountLockInfo, String memberName) { + return AccountLockInfoResponseDto.builder() + .id(accountLockInfo.getMemberId()) + .name(memberName) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/response/AccountLockInfoResponseDto.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/response/AccountLockInfoResponseDto.java index 5bf08c01d..929bdefe5 100644 --- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/response/AccountLockInfoResponseDto.java +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/dto/response/AccountLockInfoResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.auth.accountLockInfo.domain.AccountLockInfo; @Getter @Builder @@ -10,11 +9,4 @@ public class AccountLockInfoResponseDto { private String id; private String name; - - public static AccountLockInfoResponseDto toDto(AccountLockInfo accountLockInfo, String memberName) { - return AccountLockInfoResponseDto.builder() - .id(accountLockInfo.getMemberId()) - .name(memberName) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/BanMembersRetrievalService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/BanMembersRetrievalService.java index 0053239c2..cec3d1ca4 100644 --- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/BanMembersRetrievalService.java +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/BanMembersRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.auth.accountLockInfo.application.dto.mapper.AccountLockInfoDtoMapper; import page.clab.api.domain.auth.accountLockInfo.application.dto.response.AccountLockInfoResponseDto; import page.clab.api.domain.auth.accountLockInfo.application.port.in.RetrieveBannedMembersUseCase; import page.clab.api.domain.auth.accountLockInfo.application.port.out.RetrieveAccountLockInfoPort; @@ -20,6 +21,7 @@ public class BanMembersRetrievalService implements RetrieveBannedMembersUseCase private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final AccountLockInfoDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -28,7 +30,7 @@ public PagedResponseDto retrieveBanMembers(Pageable Page banMembers = retrieveAccountLockInfoPort.findByLockUntil(banDate, pageable); return new PagedResponseDto<>(banMembers.map(accountLockInfo -> { String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(accountLockInfo.getMemberId()).getMemberName(); - return AccountLockInfoResponseDto.toDto(accountLockInfo, memberName); + return mapper.toDto(accountLockInfo, memberName); })); } } diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java index 5dfe8f82b..a7ca3a616 100644 --- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.accountLockInfo.application.port.in.BanMemberUseCase; @@ -11,8 +12,8 @@ import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -22,8 +23,18 @@ public class MemberBanService implements BanMemberUseCase { private final RegisterAccountLockInfoPort registerAccountLockInfoPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + /** + * 멤버를 영구적으로 차단합니다. + * + *

해당 멤버의 계정 잠금 정보를 조회하고, 없으면 새로 생성합니다. + * Redis에 저장된 해당 멤버의 인증 토큰을 삭제하며, Slack에 밴 알림을 전송합니다.

+ * + * @param request 현재 요청 객체 + * @param memberId 차단할 멤버의 ID + * @return 저장된 계정 잠금 정보의 ID + */ @Transactional @Override public Long banMember(HttpServletRequest request, String memberId) { @@ -48,6 +59,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) { private void sendSlackBanNotification(HttpServletRequest request, String memberId) { String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName(); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_BANNED, "ID: " + memberId + ", Name: " + memberName); + String memberBannedMessage = "ID: " + memberId + ", Name: " + memberName; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.MEMBER_BANNED, request, memberBannedMessage)); } } diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java index e6c347a20..1aaf67e4d 100644 --- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java +++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.accountLockInfo.application.port.in.UnbanMemberUseCase; @@ -10,8 +11,8 @@ import page.clab.api.domain.auth.accountLockInfo.domain.AccountLockInfo; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -20,8 +21,18 @@ public class MemberUnbanService implements UnbanMemberUseCase { private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort; private final RegisterAccountLockInfoPort registerAccountLockInfoPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + /** + * 차단된 멤버를 해제합니다. + * + *

해당 멤버의 계정 잠금 정보를 조회하고 해제합니다. + * 해제된 정보는 저장되며, Slack에 해제 알림이 전송됩니다.

+ * + * @param request 현재 요청 객체 + * @param memberId 해제할 멤버의 ID + * @return 업데이트된 계정 잠금 정보의 ID + */ @Transactional @Override public Long unbanMember(HttpServletRequest request, String memberId) { @@ -45,6 +56,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) { private void sendSlackUnbanNotification(HttpServletRequest request, String memberId) { String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName(); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_UNBANNED, "ID: " + memberId + ", Name: " + memberName); + String memberUnbannedMessage = "ID: " + memberId + ", Name: " + memberName; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.MEMBER_UNBANNED, request, memberUnbannedMessage)); } } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpMapper.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpMapper.java index 97360ec77..46faa1518 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpMapper.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface BlacklistIpMapper { - BlacklistIpJpaEntity toJpaEntity(BlacklistIp blacklistIp); + BlacklistIpJpaEntity toEntity(BlacklistIp blacklistIp); BlacklistIp toDomain(BlacklistIpJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpPersistenceAdapter.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpPersistenceAdapter.java index defd8e4b7..cbafe18fe 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/adapter/out/persistence/BlacklistIpPersistenceAdapter.java @@ -25,7 +25,7 @@ public class BlacklistIpPersistenceAdapter implements @Override public BlacklistIp save(BlacklistIp blacklistIp) { - BlacklistIpJpaEntity entity = blacklistIpMapper.toJpaEntity(blacklistIp); + BlacklistIpJpaEntity entity = blacklistIpMapper.toEntity(blacklistIp); BlacklistIpJpaEntity savedEntity = blacklistIpRepository.save(entity); return blacklistIpMapper.toDomain(savedEntity); } @@ -37,7 +37,7 @@ public Optional findByIpAddress(String ipAddress) { } @Override - public BlacklistIp findByIpAddressOrThrow(String ipAddress) { + public BlacklistIp getByIpAddress(String ipAddress) { return blacklistIpRepository.findByIpAddress(ipAddress) .map(blacklistIpMapper::toDomain) .orElseThrow(() -> new NotFoundException("[BlacklistIp] IP: " + ipAddress + "에 해당하는 블랙리스트 IP가 존재하지 않습니다.")); @@ -50,7 +50,7 @@ public boolean existsByIpAddress(String clientIpAddress) { @Override public void delete(BlacklistIp blacklistIp) { - BlacklistIpJpaEntity entity = blacklistIpMapper.toJpaEntity(blacklistIp); + BlacklistIpJpaEntity entity = blacklistIpMapper.toEntity(blacklistIp); blacklistIpRepository.delete(entity); } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/mapper/BlacklistIpDtoMapper.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/mapper/BlacklistIpDtoMapper.java new file mode 100644 index 000000000..e82607c2d --- /dev/null +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/mapper/BlacklistIpDtoMapper.java @@ -0,0 +1,26 @@ +package page.clab.api.domain.auth.blacklistIp.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.auth.blacklistIp.application.dto.request.BlacklistIpRequestDto; +import page.clab.api.domain.auth.blacklistIp.application.dto.response.BlacklistIpResponseDto; +import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; + +@Component +public class BlacklistIpDtoMapper { + + public BlacklistIp fromDto(BlacklistIpRequestDto requestDto) { + return BlacklistIp.builder() + .ipAddress(requestDto.getIpAddress()) + .reason(requestDto.getReason()) + .build(); + } + + public BlacklistIpResponseDto toDto(BlacklistIp blacklistIp) { + return BlacklistIpResponseDto.builder() + .id(blacklistIp.getId()) + .ipAddress(blacklistIp.getIpAddress()) + .reason(blacklistIp.getReason()) + .createdAt(blacklistIp.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/request/BlacklistIpRequestDto.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/request/BlacklistIpRequestDto.java index 76311f1e7..a1ff42dcd 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/request/BlacklistIpRequestDto.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/request/BlacklistIpRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; @Getter @Setter @@ -16,11 +15,4 @@ public class BlacklistIpRequestDto { @Schema(description = "블랙리스트 사유", example = "스팸") private String reason; - - public static BlacklistIp toEntity(BlacklistIpRequestDto requestDto) { - return BlacklistIp.builder() - .ipAddress(requestDto.getIpAddress()) - .reason(requestDto.getReason()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/response/BlacklistIpResponseDto.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/response/BlacklistIpResponseDto.java index 1c3792e76..2e712645f 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/response/BlacklistIpResponseDto.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/dto/response/BlacklistIpResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; import java.time.LocalDateTime; @@ -14,13 +13,4 @@ public class BlacklistIpResponseDto { private String ipAddress; private String reason; private LocalDateTime createdAt; - - public static BlacklistIpResponseDto toDto(BlacklistIp blacklistIp) { - return BlacklistIpResponseDto.builder() - .id(blacklistIp.getId()) - .ipAddress(blacklistIp.getIpAddress()) - .reason(blacklistIp.getReason()) - .createdAt(blacklistIp.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/port/out/RetrieveBlacklistIpPort.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/port/out/RetrieveBlacklistIpPort.java index ba46385c6..097ee7fea 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/port/out/RetrieveBlacklistIpPort.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/port/out/RetrieveBlacklistIpPort.java @@ -15,7 +15,7 @@ public interface RetrieveBlacklistIpPort { Optional findByIpAddress(String ipAddress); - BlacklistIp findByIpAddressOrThrow(String ipAddress); + BlacklistIp getByIpAddress(String ipAddress); boolean existsByIpAddress(String clientIpAddress); } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java index 2d507d055..42bc014d8 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java @@ -2,15 +2,17 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.auth.blacklistIp.application.dto.mapper.BlacklistIpDtoMapper; import page.clab.api.domain.auth.blacklistIp.application.dto.request.BlacklistIpRequestDto; import page.clab.api.domain.auth.blacklistIp.application.port.in.RegisterBlacklistIpUseCase; import page.clab.api.domain.auth.blacklistIp.application.port.out.RegisterBlacklistIpPort; import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort; import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -18,8 +20,19 @@ public class BlacklistIpRegisterService implements RegisterBlacklistIpUseCase { private final RegisterBlacklistIpPort registerBlacklistIpPort; private final RetrieveBlacklistIpPort retrieveBlacklistIpPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + private final BlacklistIpDtoMapper mapper; + /** + * 지정된 IP 주소를 블랙리스트에 등록합니다. + * + *

해당 IP 주소가 이미 블랙리스트에 존재하는지 확인하고, + * 존재하지 않을 경우 새롭게 등록합니다. 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.

+ * + * @param request 현재 요청 객체 + * @param requestDto 블랙리스트에 추가할 IP 주소 정보를 담은 DTO + * @return 기존에 존재하거나 새로 추가된 블랙리스트 IP 주소 + */ @Transactional @Override public String registerBlacklistIp(HttpServletRequest request, BlacklistIpRequestDto requestDto) { @@ -27,9 +40,14 @@ public String registerBlacklistIp(HttpServletRequest request, BlacklistIpRequest return retrieveBlacklistIpPort.findByIpAddress(ipAddress) .map(BlacklistIp::getIpAddress) .orElseGet(() -> { - BlacklistIp blacklistIp = BlacklistIpRequestDto.toEntity(requestDto); + BlacklistIp blacklistIp = mapper.fromDto(requestDto); registerBlacklistIpPort.save(blacklistIp); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Added IP: " + ipAddress); + + String blacklistAddedMessage = "Added IP: " + ipAddress; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request, + blacklistAddedMessage)); + return ipAddress; }); } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java index 0a08ee603..26b558adc 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java @@ -2,14 +2,15 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.blacklistIp.application.port.in.RemoveBlacklistIpUseCase; import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort; import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort; import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -17,14 +18,29 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase { private final RetrieveBlacklistIpPort retrieveBlacklistIpPort; private final RemoveBlacklistIpPort removeBlacklistIpPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + /** + * 지정된 IP 주소를 블랙리스트에서 제거합니다. + * + *

블랙리스트에 등록된 IP 주소 정보를 조회하고 해당 정보를 삭제합니다. + * 삭제가 완료되면 Slack을 통해 보안 알림이 전송됩니다.

+ * + * @param request 현재 요청 객체 + * @param ipAddress 제거할 블랙리스트 IP 주소 + * @return 삭제된 블랙리스트 IP 주소 + */ @Transactional @Override public String removeBlacklistIp(HttpServletRequest request, String ipAddress) { - BlacklistIp blacklistIp = retrieveBlacklistIpPort.findByIpAddressOrThrow(ipAddress); + BlacklistIp blacklistIp = retrieveBlacklistIpPort.getByIpAddress(ipAddress); removeBlacklistIpPort.delete(blacklistIp); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: " + ipAddress); + + String blacklistRemovedMessage = "Deleted IP: " + ipAddress; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request, + blacklistRemovedMessage)); + return blacklistIp.getIpAddress(); } } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java index 13828134b..cdcf60021 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java @@ -1,17 +1,17 @@ package page.clab.api.domain.auth.blacklistIp.application.service; import jakarta.servlet.http.HttpServletRequest; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.blacklistIp.application.port.in.ResetBlacklistIpsUseCase; import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort; import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort; import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; - -import java.util.List; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -19,8 +19,17 @@ public class BlacklistIpResetService implements ResetBlacklistIpsUseCase { private final RetrieveBlacklistIpPort retrieveBlacklistIpPort; private final RemoveBlacklistIpPort removeBlacklistIpPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + /** + * 블랙리스트에 등록된 모든 IP 주소를 초기화합니다. + * + *

블랙리스트에 등록된 모든 IP 주소를 조회하고 삭제합니다. + * 삭제 완료 후, Slack을 통해 모든 IP 주소가 제거되었음을 알리는 보안 알림이 전송됩니다.

+ * + * @param request 현재 요청 객체 + * @return 삭제된 블랙리스트 IP 주소 목록 + */ @Transactional @Override public List resetBlacklistIps(HttpServletRequest request) { @@ -29,7 +38,12 @@ public List resetBlacklistIps(HttpServletRequest request) { .map(BlacklistIp::getIpAddress) .toList(); removeBlacklistIpPort.deleteAll(); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: ALL"); + + String blacklistRemovedMessage = "Deleted IP: ALL"; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request, + blacklistRemovedMessage)); + return blacklistedIps; } } diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRetrievalService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRetrievalService.java index 7e8b8c431..d472ddcf6 100644 --- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRetrievalService.java +++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.auth.blacklistIp.application.dto.mapper.BlacklistIpDtoMapper; import page.clab.api.domain.auth.blacklistIp.application.dto.response.BlacklistIpResponseDto; import page.clab.api.domain.auth.blacklistIp.application.port.in.RetrieveBlacklistIpsUseCase; import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort; @@ -16,11 +17,12 @@ public class BlacklistIpRetrievalService implements RetrieveBlacklistIpsUseCase { private final RetrieveBlacklistIpPort retrieveBlacklistIpPort; + private final BlacklistIpDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveBlacklistIps(Pageable pageable) { Page blacklistedIps = retrieveBlacklistIpPort.findAll(pageable); - return new PagedResponseDto<>(blacklistedIps.map(BlacklistIpResponseDto::toDto)); + return new PagedResponseDto<>(blacklistedIps.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorMapper.java b/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorMapper.java index 165f4e8bd..84d55dc80 100644 --- a/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorMapper.java +++ b/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface AuthenticatorMapper { - AuthenticatorJpaEntity toJpaEntity(Authenticator authenticator); + AuthenticatorJpaEntity toEntity(Authenticator authenticator); - Authenticator toDomainEntity(AuthenticatorJpaEntity jpaEntity); + Authenticator toDomain(AuthenticatorJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorPersistenceAdapter.java b/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorPersistenceAdapter.java index 7de487773..3fec2bad2 100644 --- a/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/auth/login/adapter/out/persistence/AuthenticatorPersistenceAdapter.java @@ -22,26 +22,26 @@ public class AuthenticatorPersistenceAdapter implements @Override public Authenticator save(Authenticator authenticator) { - AuthenticatorJpaEntity jpaEntity = authenticatorMapper.toJpaEntity(authenticator); + AuthenticatorJpaEntity jpaEntity = authenticatorMapper.toEntity(authenticator); AuthenticatorJpaEntity savedEntity = authenticatorRepository.save(jpaEntity); - return authenticatorMapper.toDomainEntity(savedEntity); + return authenticatorMapper.toDomain(savedEntity); } @Override public Optional findById(String memberId) { - return authenticatorRepository.findById(memberId).map(authenticatorMapper::toDomainEntity); + return authenticatorRepository.findById(memberId).map(authenticatorMapper::toDomain); } @Override - public Authenticator findByIdOrThrow(String memberId) { + public Authenticator getById(String memberId) { return authenticatorRepository.findById(memberId) - .map(authenticatorMapper::toDomainEntity) + .map(authenticatorMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Authenticator] memberId: " + memberId + "에 해당하는 Authenticator가 존재하지 않습니다.")); } @Override public void delete(Authenticator authenticator) { - AuthenticatorJpaEntity jpaEntity = authenticatorMapper.toJpaEntity(authenticator); + AuthenticatorJpaEntity jpaEntity = authenticatorMapper.toEntity(authenticator); authenticatorRepository.delete(jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/auth/login/application/port/out/RetrieveAuthenticatorPort.java b/src/main/java/page/clab/api/domain/auth/login/application/port/out/RetrieveAuthenticatorPort.java index 0e82a5629..988e37d88 100644 --- a/src/main/java/page/clab/api/domain/auth/login/application/port/out/RetrieveAuthenticatorPort.java +++ b/src/main/java/page/clab/api/domain/auth/login/application/port/out/RetrieveAuthenticatorPort.java @@ -8,7 +8,7 @@ public interface RetrieveAuthenticatorPort { Optional findById(String memberId); - Authenticator findByIdOrThrow(String memberId); + Authenticator getById(String memberId); boolean existsById(String memberId); } diff --git a/src/main/java/page/clab/api/domain/auth/login/application/service/AuthenticatorService.java b/src/main/java/page/clab/api/domain/auth/login/application/service/AuthenticatorService.java index b1bb57811..c6db533a1 100644 --- a/src/main/java/page/clab/api/domain/auth/login/application/service/AuthenticatorService.java +++ b/src/main/java/page/clab/api/domain/auth/login/application/service/AuthenticatorService.java @@ -44,7 +44,7 @@ private boolean validateTotp(Authenticator authenticator, String totp) { @Override public String resetAuthenticator(String memberId) { - Authenticator authenticator = retrieveAuthenticatorPort.findByIdOrThrow(memberId); + Authenticator authenticator = retrieveAuthenticatorPort.getById(memberId); removeAuthenticatorPort.delete(authenticator); return memberId; } diff --git a/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java b/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java index 655a51a06..402cf4ee7 100644 --- a/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java +++ b/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java @@ -1,8 +1,10 @@ package page.clab.api.domain.auth.login.application.service; import jakarta.servlet.http.HttpServletRequest; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.accountAccessLog.domain.AccountAccessResult; @@ -21,11 +23,10 @@ import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.global.auth.jwt.JwtTokenProvider; -import page.clab.api.global.common.slack.application.SlackService; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType; import page.clab.api.global.util.HttpReqResUtil; -import java.util.List; - @Service @RequiredArgsConstructor @Qualifier("twoFactorAuthenticationService") @@ -36,12 +37,14 @@ public class TwoFactorAuthenticationService implements ManageLoginUseCase { private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalRegisterAccountAccessLogUseCase externalRegisterAccountAccessLogUseCase; private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; private final JwtTokenProvider jwtTokenProvider; @Transactional @Override - public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto) throws LoginFailedException, MemberLockedException { + public LoginResult authenticate(HttpServletRequest request, + TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto) + throws LoginFailedException, MemberLockedException { String memberId = twoFactorAuthenticationRequestDto.getMemberId(); MemberLoginInfoDto loginMember = externalRetrieveMemberUseCase.getMemberLoginInfoById(memberId); String totp = twoFactorAuthenticationRequestDto.getTotp(); @@ -55,9 +58,11 @@ public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticat return LoginResult.create(header, true); } - private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request) throws MemberLockedException, LoginFailedException { + private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request) + throws MemberLockedException, LoginFailedException { if (!manageAuthenticatorUseCase.isAuthenticatorValid(memberId, totp)) { - externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId, AccountAccessResult.FAILURE); + externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId, + AccountAccessResult.FAILURE); externalManageAccountLockUseCase.handleLoginFailure(request, memberId); throw new LoginFailedException("잘못된 인증번호입니다."); } @@ -67,18 +72,21 @@ private void verifyTwoFactorAuthentication(String memberId, String totp, HttpSer private TokenInfo generateAndSaveToken(MemberLoginInfoDto memberInfo) { TokenInfo tokenInfo = jwtTokenProvider.generateToken(memberInfo.getMemberId(), memberInfo.getRole()); String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo, clientIpAddress); + externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo, + clientIpAddress); return tokenInfo; } private void sendAdminLoginNotification(HttpServletRequest request, MemberLoginInfoDto loginMember) { if (loginMember.isSuperAdminRole()) { - slackService.sendAdminLoginNotification(request, loginMember); + eventPublisher.publishEvent( + new NotificationEvent(this, GeneralAlertType.ADMIN_LOGIN, request, loginMember)); } } @Override - public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto) throws LoginFailedException, MemberLockedException { + public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto) + throws LoginFailedException, MemberLockedException { throw new UnsupportedOperationException("Method not implemented"); } diff --git a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java index 7789853cb..a31e7ee8a 100644 --- a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java +++ b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java @@ -2,25 +2,29 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.RemoveAbnormalAccessIpUseCase; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RemoveIpAccessMonitorPort; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor public class AbnormalAccessIpRemoveService implements RemoveAbnormalAccessIpUseCase { private final RemoveIpAccessMonitorPort removeIpAccessMonitorPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; @Override @Transactional public String removeAbnormalAccessIp(HttpServletRequest request, String ipAddress) { removeIpAccessMonitorPort.deleteById(ipAddress); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: " + ipAddress); + String abnormalAccessIpDeletedMessage = "Deleted IP: " + ipAddress; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request, + abnormalAccessIpDeletedMessage)); return ipAddress; } } diff --git a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java index 7a5c7dbcb..f347f2644 100644 --- a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java +++ b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java @@ -1,17 +1,17 @@ package page.clab.api.domain.auth.redisIpAccessMonitor.application.service; import jakarta.servlet.http.HttpServletRequest; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.ClearAbnormalAccessIpsUseCase; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.ClearIpAccessMonitorPort; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RetrieveIpAccessMonitorPort; import page.clab.api.domain.auth.redisIpAccessMonitor.domain.RedisIpAccessMonitor; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; - -import java.util.List; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -19,14 +19,19 @@ public class AbnormalAccessIpsClearService implements ClearAbnormalAccessIpsUseC private final ClearIpAccessMonitorPort clearIpAccessMonitorPort; private final RetrieveIpAccessMonitorPort retrieveIpAccessMonitorPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; @Override @Transactional public List clearAbnormalAccessIps(HttpServletRequest request) { List ipAccessMonitors = retrieveIpAccessMonitorPort.findAll(); clearIpAccessMonitorPort.deleteAll(); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: ALL"); + + String abnormalAccessIpClearedMessage = "Deleted IP: ALL"; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request, + abnormalAccessIpClearedMessage)); + return ipAccessMonitors; } } diff --git a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseMapper.java b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseMapper.java index 66c017165..7d58b0a8f 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseMapper.java +++ b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseMapper.java @@ -8,7 +8,7 @@ public interface AccuseMapper { @Mapping(source = "target", target = "target") - AccuseJpaEntity toJpaEntity(Accuse accuse); + AccuseJpaEntity toEntity(Accuse accuse); @Mapping(source = "target", target = "target") Accuse toDomain(AccuseJpaEntity entity); diff --git a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccusePersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccusePersistenceAdapter.java index 64d3ca530..57e17ca20 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccusePersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccusePersistenceAdapter.java @@ -23,7 +23,7 @@ public class AccusePersistenceAdapter implements @Override public Accuse save(Accuse accuse) { - AccuseJpaEntity entity = accuseMapper.toJpaEntity(accuse); + AccuseJpaEntity entity = accuseMapper.toEntity(accuse); AccuseJpaEntity savedEntity = accuseRepository.save(entity); return accuseMapper.toDomain(savedEntity); } @@ -31,7 +31,7 @@ public Accuse save(Accuse accuse) { @Override public void saveAll(List accuses) { List entities = accuses.stream() - .map(accuseMapper::toJpaEntity) + .map(accuseMapper::toEntity) .toList(); accuseRepository.saveAll(entities); } diff --git a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetMapper.java b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetMapper.java index 1464ea4b5..137850d3a 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetMapper.java +++ b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface AccuseTargetMapper { - AccuseTargetJpaEntity toJpaEntity(AccuseTarget target); + AccuseTargetJpaEntity toEntity(AccuseTarget target); AccuseTarget toDomain(AccuseTargetJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetPersistenceAdapter.java index e3c3fde7b..a31d0c96c 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/accuse/adapter/out/persistence/AccuseTargetPersistenceAdapter.java @@ -24,7 +24,7 @@ public class AccuseTargetPersistenceAdapter implements @Override public AccuseTarget save(AccuseTarget accuseTarget) { - AccuseTargetJpaEntity entity = accuseTargetMapper.toJpaEntity(accuseTarget); + AccuseTargetJpaEntity entity = accuseTargetMapper.toEntity(accuseTarget); AccuseTargetJpaEntity savedEntity = accuseTargetRepository.save(entity); return accuseTargetMapper.toDomain(savedEntity); } @@ -36,7 +36,7 @@ public Optional findById(AccuseTargetId accuseTargetId) { } @Override - public AccuseTarget findByIdOrThrow(AccuseTargetId accuseTargetId) { + public AccuseTarget getById(AccuseTargetId accuseTargetId) { return accuseTargetRepository.findById(accuseTargetId) .map(accuseTargetMapper::toDomain) .orElseThrow(() -> new NotFoundException("[AccuseTarget] id: " + accuseTargetId + "에 해당하는 신고 대상이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/dto/mapper/AccuseDtoMapper.java b/src/main/java/page/clab/api/domain/community/accuse/application/dto/mapper/AccuseDtoMapper.java new file mode 100644 index 000000000..92303c4e0 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/accuse/application/dto/mapper/AccuseDtoMapper.java @@ -0,0 +1,56 @@ +package page.clab.api.domain.community.accuse.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.accuse.application.dto.request.AccuseRequestDto; +import page.clab.api.domain.community.accuse.application.dto.response.AccuseMyResponseDto; +import page.clab.api.domain.community.accuse.application.dto.response.AccuseResponseDto; +import page.clab.api.domain.community.accuse.domain.Accuse; +import page.clab.api.domain.community.accuse.domain.AccuseStatus; +import page.clab.api.domain.community.accuse.domain.AccuseTarget; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; + +import java.util.List; + +@Component +public class AccuseDtoMapper { + + public Accuse fromDto(AccuseRequestDto requestDto, String memberId, AccuseTarget target) { + return Accuse.builder() + .memberId(memberId) + .target(target) + .reason(requestDto.getReason()) + .isDeleted(false) + .build(); + } + + public AccuseTarget fromDto(AccuseRequestDto requestDto) { + return AccuseTarget.builder() + .targetType(requestDto.getTargetType()) + .targetReferenceId(requestDto.getTargetId()) + .accuseCount(1L) + .accuseStatus(AccuseStatus.PENDING) + .build(); + } + + public AccuseMyResponseDto toDto(Accuse accuse) { + return AccuseMyResponseDto.builder() + .targetType(accuse.getTarget().getTargetType()) + .targetId(accuse.getTarget().getTargetReferenceId()) + .reason(accuse.getReason()) + .accuseStatus(accuse.getTarget().getAccuseStatus()) + .createdAt(accuse.getTarget().getCreatedAt()) + .build(); + } + + public AccuseResponseDto toDto(Accuse accuse, List members) { + return AccuseResponseDto.builder() + .members(members) + .targetType(accuse.getTarget().getTargetType()) + .targetId(accuse.getTarget().getTargetReferenceId()) + .reason(accuse.getReason()) + .accuseStatus(accuse.getTarget().getAccuseStatus()) + .accuseCount(accuse.getTarget().getAccuseCount()) + .createdAt(accuse.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/dto/request/AccuseRequestDto.java b/src/main/java/page/clab/api/domain/community/accuse/application/dto/request/AccuseRequestDto.java index 3fdacc856..0910ce314 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/dto/request/AccuseRequestDto.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/dto/request/AccuseRequestDto.java @@ -4,9 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.community.accuse.domain.Accuse; -import page.clab.api.domain.community.accuse.domain.AccuseStatus; -import page.clab.api.domain.community.accuse.domain.AccuseTarget; import page.clab.api.domain.community.accuse.domain.TargetType; @Getter @@ -24,22 +21,4 @@ public class AccuseRequestDto { @NotNull(message = "{notNull.accuse.reason}") @Schema(description = "신고 사유", example = "부적절한 게시글입니다.", required = true) private String reason; - - public static Accuse toEntity(AccuseRequestDto requestDto, String memberId, AccuseTarget target) { - return Accuse.builder() - .memberId(memberId) - .target(target) - .reason(requestDto.getReason()) - .isDeleted(false) - .build(); - } - - public static AccuseTarget toTargetEntity(AccuseRequestDto requestDto) { - return AccuseTarget.builder() - .targetType(requestDto.getTargetType()) - .targetReferenceId(requestDto.getTargetId()) - .accuseCount(1L) - .accuseStatus(AccuseStatus.PENDING) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseMyResponseDto.java b/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseMyResponseDto.java index 854265d73..aeef85115 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseMyResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseMyResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.accuse.domain.Accuse; import page.clab.api.domain.community.accuse.domain.AccuseStatus; import page.clab.api.domain.community.accuse.domain.TargetType; @@ -17,14 +16,4 @@ public class AccuseMyResponseDto { private String reason; private AccuseStatus accuseStatus; private LocalDateTime createdAt; - - public static AccuseMyResponseDto toDto(Accuse accuse) { - return AccuseMyResponseDto.builder() - .targetType(accuse.getTarget().getTargetType()) - .targetId(accuse.getTarget().getTargetReferenceId()) - .reason(accuse.getReason()) - .accuseStatus(accuse.getTarget().getAccuseStatus()) - .createdAt(accuse.getTarget().getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseResponseDto.java b/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseResponseDto.java index 6ec425886..8db8d5faf 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/dto/response/AccuseResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.accuse.domain.Accuse; import page.clab.api.domain.community.accuse.domain.AccuseStatus; import page.clab.api.domain.community.accuse.domain.TargetType; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; @@ -21,16 +20,4 @@ public class AccuseResponseDto { private AccuseStatus accuseStatus; private Long accuseCount; private LocalDateTime createdAt; - - public static AccuseResponseDto toDto(Accuse accuse, List members) { - return AccuseResponseDto.builder() - .members(members) - .targetType(accuse.getTarget().getTargetType()) - .targetId(accuse.getTarget().getTargetReferenceId()) - .reason(accuse.getReason()) - .accuseStatus(accuse.getTarget().getAccuseStatus()) - .accuseCount(accuse.getTarget().getAccuseCount()) - .createdAt(accuse.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/port/out/RetrieveAccuseTargetPort.java b/src/main/java/page/clab/api/domain/community/accuse/application/port/out/RetrieveAccuseTargetPort.java index 8245e5456..a9379be78 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/port/out/RetrieveAccuseTargetPort.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/port/out/RetrieveAccuseTargetPort.java @@ -13,7 +13,7 @@ public interface RetrieveAccuseTargetPort { Optional findById(AccuseTargetId accuseTargetId); - AccuseTarget findByIdOrThrow(AccuseTargetId accuseTargetId); + AccuseTarget getById(AccuseTargetId accuseTargetId); Page findByConditions(TargetType type, AccuseStatus status, boolean countOrder, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationReportService.java b/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationReportService.java index 82db1cb80..114bf45ae 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationReportService.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationReportService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.community.accuse.adapter.out.persistence.AccuseTargetId; +import page.clab.api.domain.community.accuse.application.dto.mapper.AccuseDtoMapper; import page.clab.api.domain.community.accuse.application.dto.request.AccuseRequestDto; import page.clab.api.domain.community.accuse.application.exception.AccuseTargetTypeIncorrectException; import page.clab.api.domain.community.accuse.application.port.in.ReportAccusationUseCase; @@ -33,7 +34,17 @@ public class AccusationReportService implements ReportAccusationUseCase { private final ExternalRetrieveCommentUseCase externalRetrieveCommentUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final AccuseDtoMapper mapper; + /** + * 신고 요청을 처리하고 신고 내역을 저장합니다. + * + *

신고 유형과 대상을 검사하고, 기존 신고 대상이 있으면 조회하여 신고 횟수를 증가시킵니다. + * 새로운 신고라면 신고 대상과 내역을 생성하고 저장한 후, 신고자와 관리자를 대상으로 알림을 전송합니다.

+ * + * @param requestDto 신고 요청 정보 + * @return 생성된 신고의 ID + */ @Transactional @Override public Long reportAccusation(AccuseRequestDto requestDto) { @@ -56,13 +67,13 @@ public Long reportAccusation(AccuseRequestDto requestDto) { private void validateAccusationRequest(TargetType type, Long targetId, String currentMemberId) { switch (type) { case BOARD: - Board board = externalRetrieveBoardUseCase.findByIdOrThrow(targetId); + Board board = externalRetrieveBoardUseCase.getById(targetId); if (board.isOwner(currentMemberId)) { throw new AccuseTargetTypeIncorrectException("자신의 게시글은 신고할 수 없습니다."); } break; case COMMENT: - Comment comment = externalRetrieveCommentUseCase.findByIdOrThrow(targetId); + Comment comment = externalRetrieveCommentUseCase.getById(targetId); if (comment.isOwner(currentMemberId)) { throw new AccuseTargetTypeIncorrectException("자신의 댓글은 신고할 수 없습니다."); } @@ -74,7 +85,7 @@ private void validateAccusationRequest(TargetType type, Long targetId, String cu private AccuseTarget getOrCreateAccuseTarget(AccuseRequestDto requestDto, TargetType type, Long targetId) { return retrieveAccuseTargetPort.findById(AccuseTargetId.create(type, targetId)) - .orElseGet(() -> AccuseRequestDto.toTargetEntity(requestDto)); + .orElseGet(() -> mapper.fromDto(requestDto)); } private Accuse findOrCreateAccusation(AccuseRequestDto requestDto, String memberId, AccuseTarget target) { @@ -85,7 +96,7 @@ private Accuse findOrCreateAccusation(AccuseRequestDto requestDto, String member }) .orElseGet(() -> { target.increaseAccuseCount(); - return AccuseRequestDto.toEntity(requestDto, memberId, target); + return mapper.fromDto(requestDto, memberId, target); }); } } diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationRetrievalService.java b/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationRetrievalService.java index e28ce5d65..ef1f7fc50 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.accuse.application.dto.mapper.AccuseDtoMapper; import page.clab.api.domain.community.accuse.application.dto.response.AccuseResponseDto; import page.clab.api.domain.community.accuse.application.port.in.RetrieveAccusationUseCase; import page.clab.api.domain.community.accuse.application.port.out.RetrieveAccusePort; @@ -27,6 +28,7 @@ public class AccusationRetrievalService implements RetrieveAccusationUseCase { private final RetrieveAccusePort retrieveAccusePort; private final RetrieveAccuseTargetPort retrieveAccuseByTargetPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final AccuseDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -46,7 +48,7 @@ private List convertTargetsToResponseDtos(Page List members = accuses.stream() .map(accuse -> externalRetrieveMemberUseCase.getMemberBasicInfoById(accuse.getMemberId())) .toList(); - return AccuseResponseDto.toDto(accuses.getFirst(), members); + return mapper.toDto(accuses.getFirst(), members); }) .filter(Objects::nonNull) .toList(); diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationStatusService.java b/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationStatusService.java index 568882469..5ad496c62 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationStatusService.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/service/AccusationStatusService.java @@ -29,7 +29,7 @@ public class AccusationStatusService implements ChangeAccusationStatusUseCase { @Transactional @Override public Long changeAccusationStatus(TargetType type, Long targetId, AccuseStatus status) { - AccuseTarget target = retrieveAccuseTargetPort.findByIdOrThrow(AccuseTargetId.create(type, targetId)); + AccuseTarget target = retrieveAccuseTargetPort.getById(AccuseTargetId.create(type, targetId)); target.updateStatus(status); sendStatusUpdateNotifications(status, target); return registerAccuseTargetPort.save(target).getTargetReferenceId(); diff --git a/src/main/java/page/clab/api/domain/community/accuse/application/service/MyAccusationsService.java b/src/main/java/page/clab/api/domain/community/accuse/application/service/MyAccusationsService.java index 43cd39d2a..165c1ee9b 100644 --- a/src/main/java/page/clab/api/domain/community/accuse/application/service/MyAccusationsService.java +++ b/src/main/java/page/clab/api/domain/community/accuse/application/service/MyAccusationsService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.accuse.application.dto.mapper.AccuseDtoMapper; import page.clab.api.domain.community.accuse.application.dto.response.AccuseMyResponseDto; import page.clab.api.domain.community.accuse.application.port.in.RetrieveMyAccusationsUseCase; import page.clab.api.domain.community.accuse.application.port.out.RetrieveAccusePort; @@ -18,12 +19,13 @@ public class MyAccusationsService implements RetrieveMyAccusationsUseCase { private final RetrieveAccusePort retrieveAccusePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final AccuseDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveMyAccusations(Pageable pageable) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); Page accuses = retrieveAccusePort.findByMemberId(currentMemberId, pageable); - return new PagedResponseDto<>(accuses.map(AccuseMyResponseDto::toDto)); + return new PagedResponseDto<>(accuses.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiMapper.java b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiMapper.java index faade089d..e48ab16e1 100644 --- a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiMapper.java +++ b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface BoardEmojiMapper { - BoardEmojiJpaEntity toJpaEntity(BoardEmoji boardEmoji); + BoardEmojiJpaEntity toEntity(BoardEmoji boardEmoji); BoardEmoji toDomain(BoardEmojiJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiPersistenceAdapter.java index 55062519d..c55110eee 100644 --- a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardEmojiPersistenceAdapter.java @@ -32,7 +32,7 @@ public Optional findByBoardIdAndMemberIdAndEmoji(Long boardId, Strin @Override public BoardEmoji save(BoardEmoji boardEmoji) { - BoardEmojiJpaEntity entity = boardEmojiMapper.toJpaEntity(boardEmoji); + BoardEmojiJpaEntity entity = boardEmojiMapper.toEntity(boardEmoji); BoardEmojiJpaEntity savedEntity = boardEmojiRepository.save(entity); return boardEmojiMapper.toDomain(savedEntity); } diff --git a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardMapper.java b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardMapper.java index 14629e611..52a9f8ca7 100644 --- a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardMapper.java +++ b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface BoardMapper { - BoardJpaEntity toJpaEntity(Board board); + BoardJpaEntity toEntity(Board board); Board toDomain(BoardJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardPersistenceAdapter.java index 72414d65a..6752552c3 100644 --- a/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/board/adapter/out/persistence/BoardPersistenceAdapter.java @@ -21,13 +21,13 @@ public class BoardPersistenceAdapter implements @Override public Board save(Board board) { - BoardJpaEntity entity = boardMapper.toJpaEntity(board); + BoardJpaEntity entity = boardMapper.toEntity(board); BoardJpaEntity savedEntity = boardRepository.save(entity); return boardMapper.toDomain(savedEntity); } @Override - public Board findByIdOrThrow(Long boardId) { + public Board getById(Long boardId) { return boardRepository.findById(boardId) .map(boardMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Board] id: " + boardId + "에 해당하는 게시글이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/mapper/BoardDtoMapper.java b/src/main/java/page/clab/api/domain/community/board/application/dto/mapper/BoardDtoMapper.java new file mode 100644 index 000000000..8ba304cd7 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/mapper/BoardDtoMapper.java @@ -0,0 +1,127 @@ +package page.clab.api.domain.community.board.application.dto.mapper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.board.application.dto.request.BoardRequestDto; +import page.clab.api.domain.community.board.application.dto.response.BoardCategoryResponseDto; +import page.clab.api.domain.community.board.application.dto.response.BoardDetailsResponseDto; +import page.clab.api.domain.community.board.application.dto.response.BoardEmojiCountResponseDto; +import page.clab.api.domain.community.board.application.dto.response.BoardListResponseDto; +import page.clab.api.domain.community.board.application.dto.response.BoardMyResponseDto; +import page.clab.api.domain.community.board.application.dto.response.WriterInfo; +import page.clab.api.domain.community.board.application.dto.shared.BoardCommentInfoDto; +import page.clab.api.domain.community.board.domain.Board; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; +import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.common.file.dto.mapper.FileDtoMapper; +import page.clab.api.global.util.RandomNicknameUtil; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class BoardDtoMapper { + + private final FileDtoMapper mapper; + + public Board fromDto(BoardRequestDto requestDto, String memberId, List uploadedFiles) { + return Board.builder() + .memberId(memberId) + .nickname(RandomNicknameUtil.makeRandomNickname()) + .category(requestDto.getCategory()) + .title(requestDto.getTitle()) + .content(requestDto.getContent()) + .uploadedFiles(uploadedFiles) + .imageUrl(requestDto.getImageUrl()) + .wantAnonymous(requestDto.isWantAnonymous()) + .isDeleted(false) + .build(); + } + + public BoardDetailsResponseDto toDto(Board board, MemberDetailedInfoDto memberInfo, boolean isOwner, List emojiInfos) { + WriterInfo writerInfo = createDetail(board, memberInfo); + return BoardDetailsResponseDto.builder() + .id(board.getId()) + .writerId(writerInfo.getId()) + .writerName(writerInfo.getName()) + .writerRoleLevel(writerInfo.getRoleLevel()) + .writerImageUrl(writerInfo.getImageUrl()) + .category(board.getCategory().getKey()) + .title(board.getTitle()) + .content(board.getContent()) + .files(mapper.toDto(board.getUploadedFiles())) + .imageUrl(board.getImageUrl()) + .isOwner(isOwner) + .emojiInfos(emojiInfos) + .createdAt(board.getCreatedAt()) + .build(); + } + + public BoardMyResponseDto toDto(Board board, MemberBasicInfoDto memberInfo) { + return BoardMyResponseDto.builder() + .id(board.getId()) + .category(board.getCategory().getKey()) + .writerName(board.isWantAnonymous() ? board.getNickname() : memberInfo.getMemberName()) + .title(board.getTitle()) + .imageUrl(board.getImageUrl()) + .createdAt(board.getCreatedAt()) + .build(); + } + + public BoardCommentInfoDto toDto(Board board) { + return BoardCommentInfoDto.builder() + .boardId(board.getId()) + .memberId(board.getMemberId()) + .title(board.getTitle()) + .category(board.getCategory()) + .build(); + } + + public BoardCategoryResponseDto toCategoryDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount) { + WriterInfo writerInfo = create(board, memberInfo); + return BoardCategoryResponseDto.builder() + .id(board.getId()) + .category(board.getCategory().getKey()) + .writerId(writerInfo.getId()) + .writerName(writerInfo.getName()) + .title(board.getTitle()) + .commentCount(commentCount) + .imageUrl(board.getImageUrl()) + .createdAt(board.getCreatedAt()) + .build(); + } + + public BoardListResponseDto toListDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount) { + WriterInfo writerInfo = create(board, memberInfo); + return BoardListResponseDto.builder() + .id(board.getId()) + .writerId(writerInfo.getId()) + .writerName(writerInfo.getName()) + .category(board.getCategory().getKey()) + .title(board.getTitle()) + .content(board.getContent()) + .commentCount(commentCount) + .imageUrl(board.getImageUrl()) + .createdAt(board.getCreatedAt()) + .build(); + } + + public WriterInfo create(Board board, MemberDetailedInfoDto memberInfo) { + if (memberInfo.isAdminRole() && board.isNotice()) { + return new WriterInfo(null, "운영진"); + } else if (board.isWantAnonymous()) { + return new WriterInfo(null, board.getNickname()); + } + return new WriterInfo(memberInfo.getMemberId(), memberInfo.getMemberName()); + } + + public WriterInfo createDetail(Board board, MemberDetailedInfoDto memberInfo) { + if (memberInfo.isAdminRole() && board.isNotice()) { + return new WriterInfo(null, "운영진", memberInfo.getRoleLevel(), null); + } else if (board.isWantAnonymous()) { + return new WriterInfo(null, board.getNickname(), null, null); + } + return new WriterInfo(memberInfo.getMemberId(), memberInfo.getMemberName(), memberInfo.getRoleLevel(), memberInfo.getImageUrl()); + } +} diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/request/BoardRequestDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/request/BoardRequestDto.java index 963df5337..88481b690 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/request/BoardRequestDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/request/BoardRequestDto.java @@ -4,10 +4,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.community.board.domain.Board; import page.clab.api.domain.community.board.domain.BoardCategory; -import page.clab.api.global.common.file.domain.UploadedFile; -import page.clab.api.global.util.RandomNicknameUtil; import java.util.List; @@ -36,18 +33,4 @@ public class BoardRequestDto { @NotNull(message = "{notNull.board.wantAnonymous}") @Schema(description = "익명 사용 여부", example = "false", required = true) private boolean wantAnonymous; - - public static Board toEntity(BoardRequestDto requestDto, String memberId, List uploadedFiles) { - return Board.builder() - .memberId(memberId) - .nickname(RandomNicknameUtil.makeRandomNickname()) - .category(requestDto.getCategory()) - .title(requestDto.getTitle()) - .content(requestDto.getContent()) - .uploadedFiles(uploadedFiles) - .imageUrl(requestDto.getImageUrl()) - .wantAnonymous(requestDto.isWantAnonymous()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardCategoryResponseDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardCategoryResponseDto.java index d45d1af55..a817a5c52 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardCategoryResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardCategoryResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.board.domain.Board; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import java.time.LocalDateTime; @@ -19,18 +17,4 @@ public class BoardCategoryResponseDto { private Long commentCount; private String imageUrl; private LocalDateTime createdAt; - - public static BoardCategoryResponseDto toDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount) { - WriterInfo writerInfo = WriterInfo.fromBoard(board, memberInfo); - return BoardCategoryResponseDto.builder() - .id(board.getId()) - .category(board.getCategory().getKey()) - .writerId(writerInfo.getId()) - .writerName(writerInfo.getName()) - .title(board.getTitle()) - .commentCount(commentCount) - .imageUrl(board.getImageUrl()) - .createdAt(board.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardDetailsResponseDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardDetailsResponseDto.java index 4bfb5d317..6baa5ee30 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardDetailsResponseDto.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.board.domain.Board; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDateTime; @@ -29,23 +27,4 @@ public class BoardDetailsResponseDto { private Boolean isOwner; private List emojiInfos; private LocalDateTime createdAt; - - public static BoardDetailsResponseDto toDto(Board board, MemberDetailedInfoDto memberInfo, boolean isOwner, List emojiInfos) { - WriterInfo writerInfo = WriterInfo.fromBoardDetails(board, memberInfo); - return BoardDetailsResponseDto.builder() - .id(board.getId()) - .writerId(writerInfo.getId()) - .writerName(writerInfo.getName()) - .writerRoleLevel(writerInfo.getRoleLevel()) - .writerImageUrl(writerInfo.getImageUrl()) - .category(board.getCategory().getKey()) - .title(board.getTitle()) - .content(board.getContent()) - .files(UploadedFileResponseDto.toDto(board.getUploadedFiles())) - .imageUrl(board.getImageUrl()) - .isOwner(isOwner) - .emojiInfos(emojiInfos) - .createdAt(board.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardEmojiCountResponseDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardEmojiCountResponseDto.java index 238eaadaa..0a35f9743 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardEmojiCountResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardEmojiCountResponseDto.java @@ -16,12 +16,4 @@ public BoardEmojiCountResponseDto(String emoji, Long count, Boolean isOwner) { this.count = count; this.isOwner = isOwner; } - - public static BoardEmojiCountResponseDto toDto(String emoji, Long count, Boolean isOwner) { - return BoardEmojiCountResponseDto.builder() - .emoji(emoji) - .count(count) - .isOwner(isOwner) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardListResponseDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardListResponseDto.java index cca921832..da33f5560 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardListResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardListResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.board.domain.Board; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import java.time.LocalDateTime; @@ -20,19 +18,4 @@ public class BoardListResponseDto { private Long commentCount; private String imageUrl; private LocalDateTime createdAt; - - public static BoardListResponseDto toDto(Board board, MemberDetailedInfoDto memberInfo, Long commentCount) { - WriterInfo writerInfo = WriterInfo.fromBoard(board, memberInfo); - return BoardListResponseDto.builder() - .id(board.getId()) - .writerId(writerInfo.getId()) - .writerName(writerInfo.getName()) - .category(board.getCategory().getKey()) - .title(board.getTitle()) - .content(board.getContent()) - .commentCount(commentCount) - .imageUrl(board.getImageUrl()) - .createdAt(board.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardMyResponseDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardMyResponseDto.java index 366c16319..b4b88ac0a 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardMyResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/response/BoardMyResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.board.domain.Board; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; import java.time.LocalDateTime; @@ -17,15 +15,4 @@ public class BoardMyResponseDto { private String title; private String imageUrl; private LocalDateTime createdAt; - - public static BoardMyResponseDto toDto(Board board, MemberBasicInfoDto memberInfo) { - return BoardMyResponseDto.builder() - .id(board.getId()) - .category(board.getCategory().getKey()) - .writerName(board.isWantAnonymous() ? board.getNickname() : memberInfo.getMemberName()) - .title(board.getTitle()) - .imageUrl(board.getImageUrl()) - .createdAt(board.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/response/WriterInfo.java b/src/main/java/page/clab/api/domain/community/board/application/dto/response/WriterInfo.java index fe6914dc7..204e8419c 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/response/WriterInfo.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/response/WriterInfo.java @@ -1,8 +1,6 @@ package page.clab.api.domain.community.board.application.dto.response; import lombok.Getter; -import page.clab.api.domain.community.board.domain.Board; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; @Getter public class WriterInfo { @@ -23,22 +21,4 @@ public WriterInfo(String id, String name, Long roleLevel, String imageUrl) { this.roleLevel = roleLevel; this.imageUrl = imageUrl; } - - public static WriterInfo fromBoard(Board board, MemberDetailedInfoDto memberInfo) { - if (memberInfo.isAdminRole() && board.isNotice()) { - return new WriterInfo(null, "운영진"); - } else if (board.isWantAnonymous()) { - return new WriterInfo(null, board.getNickname()); - } - return new WriterInfo(memberInfo.getMemberId(), memberInfo.getMemberName()); - } - - public static WriterInfo fromBoardDetails(Board board, MemberDetailedInfoDto memberInfo) { - if (memberInfo.isAdminRole() && board.isNotice()) { - return new WriterInfo(null, "운영진", memberInfo.getRoleLevel(), null); - } else if (board.isWantAnonymous()) { - return new WriterInfo(null, board.getNickname(), null, null); - } - return new WriterInfo(memberInfo.getMemberId(), memberInfo.getMemberName(), memberInfo.getRoleLevel(), memberInfo.getImageUrl()); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/dto/shared/BoardCommentInfoDto.java b/src/main/java/page/clab/api/domain/community/board/application/dto/shared/BoardCommentInfoDto.java index d6a796366..9716d11f3 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/dto/shared/BoardCommentInfoDto.java +++ b/src/main/java/page/clab/api/domain/community/board/application/dto/shared/BoardCommentInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.board.domain.Board; import page.clab.api.domain.community.board.domain.BoardCategory; @Getter @@ -13,13 +12,4 @@ public class BoardCommentInfoDto { private final String memberId; private final String title; private final BoardCategory category; - - public static BoardCommentInfoDto create(Board board) { - return BoardCommentInfoDto.builder() - .boardId(board.getId()) - .memberId(board.getMemberId()) - .title(board.getTitle()) - .category(board.getCategory()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/port/in/RetrieveBoardUseCase.java b/src/main/java/page/clab/api/domain/community/board/application/port/in/RetrieveBoardUseCase.java index 24480d574..61b6ea0c4 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/port/in/RetrieveBoardUseCase.java +++ b/src/main/java/page/clab/api/domain/community/board/application/port/in/RetrieveBoardUseCase.java @@ -9,5 +9,5 @@ public interface RetrieveBoardUseCase { PagedResponseDto retrieveBoards(Pageable pageable); - Board findByIdOrThrow(Long boardId); + Board getById(Long boardId); } diff --git a/src/main/java/page/clab/api/domain/community/board/application/port/out/RetrieveBoardPort.java b/src/main/java/page/clab/api/domain/community/board/application/port/out/RetrieveBoardPort.java index 379d1778f..54aabd972 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/port/out/RetrieveBoardPort.java +++ b/src/main/java/page/clab/api/domain/community/board/application/port/out/RetrieveBoardPort.java @@ -7,7 +7,7 @@ public interface RetrieveBoardPort { - Board findByIdOrThrow(Long boardId); + Board getById(Long boardId); Board findByIdRegardlessOfDeletion(Long boardId); diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardDetailsRetrievalService.java index 12104ddb2..fc3831ce7 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardDetailsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardDetailsRetrievalService.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.response.BoardDetailsResponseDto; import page.clab.api.domain.community.board.application.dto.response.BoardEmojiCountResponseDto; import page.clab.api.domain.community.board.application.port.in.RetrieveBoardDetailsUseCase; @@ -23,16 +24,17 @@ public class BoardDetailsRetrievalService implements RetrieveBoardDetailsUseCase private final RetrieveBoardPort retrieveBoardPort; private final RetrieveBoardEmojiPort retrieveBoardEmojiPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BoardDtoMapper mapper; @Transactional @Override public BoardDetailsResponseDto retrieveBoardDetails(Long boardId) { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Board board = retrieveBoardPort.findByIdOrThrow(boardId); + Board board = retrieveBoardPort.getById(boardId); MemberDetailedInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberDetailedInfoById(board.getMemberId()); boolean isOwner = board.isOwner(currentMemberInfo.getMemberId()); List emojiInfos = getBoardEmojiCountResponseDtoList(boardId, currentMemberInfo.getMemberId()); - return BoardDetailsResponseDto.toDto(board, memberInfo, isOwner, emojiInfos); + return mapper.toDto(board, memberInfo, isOwner, emojiInfos); } @Transactional(readOnly = true) diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardEmojiToggleService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardEmojiToggleService.java index 463558d5d..fbd38c661 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardEmojiToggleService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardEmojiToggleService.java @@ -23,6 +23,17 @@ public class BoardEmojiToggleService implements ToggleBoardEmojiUseCase { private final RegisterBoardEmojiPort registerBoardEmojiPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + /** + * 게시글의 이모지 상태를 토글합니다. + * + *

이모지가 유효한지 검증한 후, 해당 이모지가 이미 존재하면 삭제 상태를 토글합니다. + * 이모지가 없으면 새로 생성하여 저장합니다.

+ * + * @param boardId 이모지를 추가하거나 토글할 게시글의 ID + * @param emoji 추가할 이모지 + * @return 게시글의 카테고리 키 + * @throws InvalidEmojiException 지원하지 않는 이모지를 사용할 경우 예외 발생 + */ @Transactional @Override public String toggleEmojiStatus(Long boardId, String emoji) { @@ -31,7 +42,7 @@ public String toggleEmojiStatus(Long boardId, String emoji) { } MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); String memberId = currentMemberInfo.getMemberId(); - Board board = retrieveBoardPort.findByIdOrThrow(boardId); + Board board = retrieveBoardPort.getById(boardId); BoardEmoji boardEmoji = retrieveBoardEmojiPort.findByBoardIdAndMemberIdAndEmoji(boardId, memberId, emoji) .map(existingEmoji -> { existingEmoji.toggleIsDeletedStatus(); diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java index 0c59985da..cdcd2dee8 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java @@ -1,23 +1,25 @@ package page.clab.api.domain.community.board.application.service; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.request.BoardRequestDto; import page.clab.api.domain.community.board.application.port.in.RegisterBoardUseCase; import page.clab.api.domain.community.board.application.port.out.RegisterBoardPort; import page.clab.api.domain.community.board.domain.Board; -import page.clab.api.global.common.slack.domain.SlackBoardInfo; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase; import page.clab.api.global.common.file.application.UploadedFileService; import page.clab.api.global.common.file.domain.UploadedFile; -import page.clab.api.global.common.slack.application.SlackService; +import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType; import page.clab.api.global.exception.PermissionDeniedException; -import java.util.List; - @Service @RequiredArgsConstructor public class BoardRegisterService implements RegisterBoardUseCase { @@ -26,20 +28,35 @@ public class BoardRegisterService implements RegisterBoardUseCase { private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; private final UploadedFileService uploadedFileService; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + private final BoardDtoMapper mapper; + /** + * 새로운 게시글을 등록합니다. + * + *

현재 로그인한 멤버의 정보를 가져와 게시글을 생성하고, 필요한 경우 알림을 전송합니다. + * 게시글 작성 권한을 검증하며, 공지사항일 경우 Slack과 사용자에게 알림을 보냅니다.

+ * + * @param requestDto 게시글 요청 정보 DTO + * @return 등록된 게시글의 카테고리 키 + * @throws PermissionDeniedException 게시글 작성 권한이 없는 경우 예외 발생 + */ @Transactional @Override public String registerBoard(BoardRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); List uploadedFiles = uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrlList()); - Board board = BoardRequestDto.toEntity(requestDto, currentMemberInfo.getMemberId(), uploadedFiles); + Board board = mapper.fromDto(requestDto, currentMemberInfo.getMemberId(), uploadedFiles); board.validateAccessPermissionForCreation(currentMemberInfo); if (board.shouldNotifyForNewBoard(currentMemberInfo)) { - externalSendNotificationUseCase.sendNotificationToMember(currentMemberInfo.getMemberId(), "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다."); + externalSendNotificationUseCase.sendNotificationToMember(currentMemberInfo.getMemberId(), + "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다."); } - SlackBoardInfo boardInfo = SlackBoardInfo.create(board, currentMemberInfo); - slackService.sendNewBoardNotification(boardInfo); + + BoardNotificationInfo boardInfo = BoardNotificationInfo.create(board, currentMemberInfo); + eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_BOARD, null, + boardInfo)); + return registerBoardPort.save(board).getCategory().getKey(); } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRemoveService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRemoveService.java index f064d171d..ee12336e0 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRemoveService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRemoveService.java @@ -26,7 +26,7 @@ public class BoardRemoveService implements RemoveBoardUseCase { @Override public String removeBoard(Long boardId) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Board board = retrieveBoardPort.findByIdOrThrow(boardId); + Board board = retrieveBoardPort.getById(boardId); board.validateAccessPermission(currentMemberInfo); board.delete(); registerBoardPort.save(board); diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRetrievalService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRetrievalService.java index 063e8ad38..f3e7e227e 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.response.BoardListResponseDto; import page.clab.api.domain.community.board.application.port.in.RetrieveBoardUseCase; import page.clab.api.domain.community.board.application.port.out.RetrieveBoardPort; @@ -22,6 +23,7 @@ public class BoardRetrievalService implements RetrieveBoardUseCase { private final RetrieveBoardPort retrieveBoardPort; private final ExternalRetrieveCommentUseCase externalRetrieveCommentUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BoardDtoMapper mapper; @Transactional @Override @@ -32,8 +34,8 @@ public PagedResponseDto retrieveBoards(Pageable pageable) } @Override - public Board findByIdOrThrow(Long boardId) { - return retrieveBoardPort.findByIdOrThrow(boardId); + public Board getById(Long boardId) { + return retrieveBoardPort.getById(boardId); } private MemberDetailedInfoDto getMemberDetailedInfoByBoard(Board board) { @@ -43,6 +45,6 @@ private MemberDetailedInfoDto getMemberDetailedInfoByBoard(Board board) { @NotNull private BoardListResponseDto mapToBoardListResponseDto(Board board, MemberDetailedInfoDto memberInfo) { Long commentCount = externalRetrieveCommentUseCase.countByBoardId(board.getId()); - return BoardListResponseDto.toDto(board, memberInfo, commentCount); + return mapper.toListDto(board, memberInfo, commentCount); } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardUpdateService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardUpdateService.java index 7363a1195..de477a919 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardUpdateService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardUpdateService.java @@ -27,7 +27,7 @@ public class BoardUpdateService implements UpdateBoardUseCase { @Override public String updateBoard(Long boardId, BoardUpdateRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Board board = retrieveBoardPort.findByIdOrThrow(boardId); + Board board = retrieveBoardPort.getById(boardId); board.validateAccessPermission(currentMemberInfo); board.update(requestDto); registerBoardPort.save(board); diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardsByCategoryRetrievalService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardsByCategoryRetrievalService.java index 927f86765..6898c7a91 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardsByCategoryRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardsByCategoryRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.response.BoardCategoryResponseDto; import page.clab.api.domain.community.board.application.port.in.RetrieveBoardsByCategoryUseCase; import page.clab.api.domain.community.board.application.port.out.RetrieveBoardPort; @@ -22,6 +23,7 @@ public class BoardsByCategoryRetrievalService implements RetrieveBoardsByCategor private final RetrieveBoardPort retrieveBoardPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalRetrieveCommentUseCase externalRetrieveCommentUseCase; + private final BoardDtoMapper mapper; @Transactional @Override @@ -29,7 +31,7 @@ public PagedResponseDto retrieveBoardsByCategory(Board Page boards = retrieveBoardPort.findAllByCategory(category, pageable); return new PagedResponseDto<>(boards.map(board -> { long commentCount = externalRetrieveCommentUseCase.countByBoardId(board.getId()); - return BoardCategoryResponseDto.toDto(board, getMemberDetailedInfoByBoard(board), commentCount); + return mapper.toCategoryDto(board, getMemberDetailedInfoByBoard(board), commentCount); })); } diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/DeletedBoardsRetrievalService.java b/src/main/java/page/clab/api/domain/community/board/application/service/DeletedBoardsRetrievalService.java index f3658a45e..0a0671a19 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/DeletedBoardsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/DeletedBoardsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.response.BoardListResponseDto; import page.clab.api.domain.community.board.application.port.in.RetrieveDeletedBoardsUseCase; import page.clab.api.domain.community.board.application.port.out.RetrieveBoardPort; @@ -22,6 +23,7 @@ public class DeletedBoardsRetrievalService implements RetrieveDeletedBoardsUseCa private final RetrieveBoardPort retrieveBoardPort; private final ExternalRetrieveCommentUseCase externalRetrieveCommentUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BoardDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -38,6 +40,6 @@ private MemberDetailedInfoDto getMemberDetailedInfoByBoard(Board board) { @NotNull private BoardListResponseDto mapToBoardListResponseDto(Board board, MemberDetailedInfoDto memberInfo) { Long commentCount = externalRetrieveCommentUseCase.countByBoardId(board.getId()); - return BoardListResponseDto.toDto(board, memberInfo, commentCount); + return mapper.toListDto(board, memberInfo, commentCount); } } diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/MyBoardsRetrievalService.java b/src/main/java/page/clab/api/domain/community/board/application/service/MyBoardsRetrievalService.java index ac55bc05c..f5d63b4e6 100644 --- a/src/main/java/page/clab/api/domain/community/board/application/service/MyBoardsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/board/application/service/MyBoardsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.response.BoardMyResponseDto; import page.clab.api.domain.community.board.application.port.in.RetrieveMyBoardsUseCase; import page.clab.api.domain.community.board.application.port.out.RetrieveBoardPort; @@ -19,12 +20,13 @@ public class MyBoardsRetrievalService implements RetrieveMyBoardsUseCase { private final RetrieveBoardPort retrieveBoardPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BoardDtoMapper mapper; @Transactional @Override public PagedResponseDto retrieveMyBoards(Pageable pageable) { MemberBasicInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo(); Page boards = retrieveBoardPort.findAllByMemberId(currentMemberInfo.getMemberId(), pageable); - return new PagedResponseDto<>(boards.map(board -> BoardMyResponseDto.toDto(board, currentMemberInfo))); + return new PagedResponseDto<>(boards.map(board -> mapper.toDto(board, currentMemberInfo))); } } diff --git a/src/main/java/page/clab/api/domain/community/board/domain/Board.java b/src/main/java/page/clab/api/domain/community/board/domain/Board.java index 9fb9970fc..c7ab80c84 100644 --- a/src/main/java/page/clab/api/domain/community/board/domain/Board.java +++ b/src/main/java/page/clab/api/domain/community/board/domain/Board.java @@ -50,10 +50,6 @@ public boolean isNotice() { return this.category.equals(BoardCategory.NOTICE); } - public boolean isGraduated() { - return this.category.equals(BoardCategory.GRADUATED); - } - public boolean shouldNotifyForNewBoard(MemberDetailedInfoDto memberInfo) { return memberInfo.isAdminRole() && this.category.equals(BoardCategory.NOTICE); // Assuming 2 is Admin role level } @@ -72,8 +68,5 @@ public void validateAccessPermissionForCreation(MemberDetailedInfoDto currentMem if (this.isNotice() && !currentMemberInfo.isAdminRole()) { throw new PermissionDeniedException("공지사항은 관리자만 작성할 수 있습니다."); } - if (this.isGraduated() && !currentMemberInfo.isGraduated()) { - throw new PermissionDeniedException("졸업생 게시판은 졸업생만 작성할 수 있습니다."); - } } } diff --git a/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java b/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java index 2783017ba..831bad896 100644 --- a/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java +++ b/src/main/java/page/clab/api/domain/community/board/domain/BoardCategory.java @@ -9,8 +9,8 @@ public enum BoardCategory { NOTICE("notice", "공지사항"), FREE("free", "자유 게시판"), - QNA("qna", "질문 게시판"), - GRADUATED("graduated", "졸업생 게시판"), + DEVELOPMENT_QNA("development_qna", "개발 질문 게시판"), + INFORMATION_REVIEWS("information_reviews", "정보 및 후기 게시판"), ORGANIZATION("organization", "동아리 소식"); private final String key; diff --git a/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikeMapper.java b/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikeMapper.java index 804c5d4b3..3657a3fab 100644 --- a/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikeMapper.java +++ b/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikeMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface CommentLikeMapper { - CommentLikeJpaEntity toJpaEntity(CommentLike commentLike); + CommentLikeJpaEntity toEntity(CommentLike commentLike); CommentLike toDomain(CommentLikeJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikePersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikePersistenceAdapter.java index a5fb8da81..67e5ace4c 100644 --- a/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikePersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentLikePersistenceAdapter.java @@ -21,7 +21,7 @@ public class CommentLikePersistenceAdapter implements @Override public CommentLike save(CommentLike commentLike) { - CommentLikeJpaEntity entity = commentLikeMapper.toJpaEntity(commentLike); + CommentLikeJpaEntity entity = commentLikeMapper.toEntity(commentLike); CommentLikeJpaEntity savedEntity = commentLikeRepository.save(entity); return commentLikeMapper.toDomain(savedEntity); } diff --git a/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentMapper.java b/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentMapper.java index 4eafc007e..ec26beffa 100644 --- a/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentMapper.java +++ b/src/main/java/page/clab/api/domain/community/comment/adapter/out/persistence/CommentMapper.java @@ -10,18 +10,18 @@ @Component public class CommentMapper { - public CommentJpaEntity toJpaEntity(Comment comment) { + public CommentJpaEntity toEntity(Comment comment) { Map mappedEntities = new HashMap<>(); - return toJpaEntity(comment, mappedEntities); + return toEntity(comment, mappedEntities); } - private CommentJpaEntity toJpaEntity(Comment comment, Map mappedEntities) { + private CommentJpaEntity toEntity(Comment comment, Map mappedEntities) { if (comment == null) return null; if (mappedEntities.containsKey(comment.getId())) { return mappedEntities.get(comment.getId()); } - CommentJpaEntity parentEntity = toJpaEntity(comment.getParent(), mappedEntities); + CommentJpaEntity parentEntity = toEntity(comment.getParent(), mappedEntities); CommentJpaEntity entity = CommentJpaEntity.builder() .id(comment.getId()) .boardId(comment.getBoardId()) @@ -37,7 +37,7 @@ private CommentJpaEntity toJpaEntity(Comment comment, Map comments) { List entities = comments.stream() - .map(commentMapper::toJpaEntity) + .map(commentMapper::toEntity) .toList(); commentRepository.saveAll(entities); } @@ -43,7 +43,7 @@ public Optional findById(Long commentId) { } @Override - public Comment findByIdOrThrow(Long commentId) { + public Comment getById(Long commentId) { return commentRepository.findById(commentId) .map(commentMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Comment] id: " + commentId + "에 해당하는 댓글이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/community/comment/application/dto/mapper/CommentDtoMapper.java b/src/main/java/page/clab/api/domain/community/comment/application/dto/mapper/CommentDtoMapper.java new file mode 100644 index 000000000..6b4912af3 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/comment/application/dto/mapper/CommentDtoMapper.java @@ -0,0 +1,86 @@ +package page.clab.api.domain.community.comment.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.board.application.dto.shared.BoardCommentInfoDto; +import page.clab.api.domain.community.comment.application.dto.request.CommentRequestDto; +import page.clab.api.domain.community.comment.application.dto.response.CommentMyResponseDto; +import page.clab.api.domain.community.comment.application.dto.response.CommentResponseDto; +import page.clab.api.domain.community.comment.application.dto.response.DeletedCommentResponseDto; +import page.clab.api.domain.community.comment.domain.Comment; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; +import page.clab.api.global.util.RandomNicknameUtil; + +import java.util.List; + +@Component +public class CommentDtoMapper { + + public Comment fromDto(CommentRequestDto requestDto, Long boardId, String writerId, Comment parent) { + return Comment.builder() + .boardId(boardId) + .writerId(writerId) + .nickname(RandomNicknameUtil.makeRandomNickname()) + .content(requestDto.getContent()) + .parent(parent) + .wantAnonymous(requestDto.isWantAnonymous()) + .likes(0L) + .isDeleted(false) + .build(); + } + + public CommentMyResponseDto toDto(Comment comment, MemberDetailedInfoDto memberInfo, BoardCommentInfoDto boardInfo, boolean hasLikeByMe) { + if (comment.getBoardId() == null || comment.getIsDeleted()) { + return null; + } + return CommentMyResponseDto.builder() + .id(comment.getId()) + .boardId(boardInfo.getBoardId()) + .boardCategory(boardInfo.getCategory().getKey()) + .writer(comment.isWantAnonymous() ? comment.getNickname() : memberInfo.getMemberName()) + .writerImageUrl(comment.isWantAnonymous() ? null : memberInfo.getImageUrl()) + .content(comment.getContent()) + .likes(comment.getLikes()) + .hasLikeByMe(hasLikeByMe) + .createdAt(comment.getCreatedAt()) + .build(); + } + + public CommentResponseDto toDto(Comment comment, MemberDetailedInfoDto memberInfo, boolean isOwner, List children) { + if (comment.getIsDeleted()) { + return CommentResponseDto.builder() + .id(comment.getId()) + .isDeleted(true) + .children(children) + .likes(comment.getLikes()) + .createdAt(comment.getCreatedAt()) + .build(); + } + return CommentResponseDto.builder() + .id(comment.getId()) + .isDeleted(false) + .writerId(comment.isWantAnonymous() ? null : comment.getWriterId()) + .writerName(comment.isWantAnonymous() ? comment.getNickname() : memberInfo.getMemberName()) + .writerImageUrl(comment.isWantAnonymous() ? null : memberInfo.getImageUrl()) + .writerRoleLevel(comment.isWantAnonymous() ? null : memberInfo.getRoleLevel()) + .content(comment.getContent()) + .children(children) + .likes(comment.getLikes()) + .isOwner(isOwner) + .createdAt(comment.getCreatedAt()) + .build(); + } + + public DeletedCommentResponseDto toDto(Comment comment, MemberDetailedInfoDto memberInfo, boolean isOwner) { + return DeletedCommentResponseDto.builder() + .id(comment.getId()) + .writerId(comment.isWantAnonymous() ? null : memberInfo.getMemberId()) + .writerName(comment.isWantAnonymous() ? comment.getNickname() : memberInfo.getMemberName()) + .writerImageUrl(comment.isWantAnonymous() ? null : memberInfo.getImageUrl()) + .writerRoleLevel(comment.isWantAnonymous() ? null : memberInfo.getRoleLevel()) + .content(comment.getContent()) + .likes(comment.getLikes()) + .isOwner(isOwner) + .createdAt(comment.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/comment/application/dto/request/CommentRequestDto.java b/src/main/java/page/clab/api/domain/community/comment/application/dto/request/CommentRequestDto.java index 04ee3ae52..c49732ba7 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/dto/request/CommentRequestDto.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/dto/request/CommentRequestDto.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.community.comment.domain.Comment; -import page.clab.api.global.util.RandomNicknameUtil; @Getter @Setter @@ -18,17 +16,4 @@ public class CommentRequestDto { @NotNull(message = "{notNull.comment.wantAnonymous}") @Schema(description = "익명 사용 여부", example = "false", required = true) private boolean wantAnonymous; - - public static Comment toEntity(CommentRequestDto requestDto, Long boardId, String writerId, Comment parent) { - return Comment.builder() - .boardId(boardId) - .writerId(writerId) - .nickname(RandomNicknameUtil.makeRandomNickname()) - .content(requestDto.getContent()) - .parent(parent) - .wantAnonymous(requestDto.isWantAnonymous()) - .likes(0L) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentMyResponseDto.java b/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentMyResponseDto.java index 1d2cab850..584f76725 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentMyResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentMyResponseDto.java @@ -2,9 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.board.application.dto.shared.BoardCommentInfoDto; -import page.clab.api.domain.community.comment.domain.Comment; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import java.time.LocalDateTime; @@ -21,21 +18,4 @@ public class CommentMyResponseDto { private Long likes; private boolean hasLikeByMe; private LocalDateTime createdAt; - - public static CommentMyResponseDto toDto(Comment comment, MemberDetailedInfoDto memberInfo, BoardCommentInfoDto boardInfo, boolean hasLikeByMe) { - if (comment.getBoardId() == null || comment.getIsDeleted()) { - return null; - } - return CommentMyResponseDto.builder() - .id(comment.getId()) - .boardId(boardInfo.getBoardId()) - .boardCategory(boardInfo.getCategory().getKey()) - .writer(comment.isWantAnonymous() ? comment.getNickname() : memberInfo.getMemberName()) - .writerImageUrl(comment.isWantAnonymous() ? null : memberInfo.getImageUrl()) - .content(comment.getContent()) - .likes(comment.getLikes()) - .hasLikeByMe(hasLikeByMe) - .createdAt(comment.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentResponseDto.java b/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentResponseDto.java index 5a732f2ed..a2c1b2034 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/dto/response/CommentResponseDto.java @@ -4,8 +4,6 @@ import lombok.Builder; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.community.comment.domain.Comment; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import java.time.LocalDateTime; import java.util.List; @@ -29,29 +27,4 @@ public class CommentResponseDto { @JsonProperty("isOwner") private Boolean isOwner; private LocalDateTime createdAt; - - public static CommentResponseDto toDto(Comment comment, MemberDetailedInfoDto memberInfo, boolean isOwner, List children) { - if (comment.getIsDeleted()) { - return CommentResponseDto.builder() - .id(comment.getId()) - .isDeleted(true) - .children(children) - .likes(comment.getLikes()) - .createdAt(comment.getCreatedAt()) - .build(); - } - return CommentResponseDto.builder() - .id(comment.getId()) - .isDeleted(false) - .writerId(comment.isWantAnonymous() ? null : comment.getWriterId()) - .writerName(comment.isWantAnonymous() ? comment.getNickname() : memberInfo.getMemberName()) - .writerImageUrl(comment.isWantAnonymous() ? null : memberInfo.getImageUrl()) - .writerRoleLevel(comment.isWantAnonymous() ? null : memberInfo.getRoleLevel()) - .content(comment.getContent()) - .children(children) - .likes(comment.getLikes()) - .isOwner(isOwner) - .createdAt(comment.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/dto/response/DeletedCommentResponseDto.java b/src/main/java/page/clab/api/domain/community/comment/application/dto/response/DeletedCommentResponseDto.java index 0faca611f..536c9b021 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/dto/response/DeletedCommentResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/dto/response/DeletedCommentResponseDto.java @@ -4,8 +4,6 @@ import lombok.Builder; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.community.comment.domain.Comment; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import java.time.LocalDateTime; @@ -25,18 +23,4 @@ public class DeletedCommentResponseDto { @JsonProperty("isOwner") private boolean isOwner; private LocalDateTime createdAt; - - public static DeletedCommentResponseDto toDto(Comment comment, MemberDetailedInfoDto memberInfo, boolean isOwner) { - return DeletedCommentResponseDto.builder() - .id(comment.getId()) - .writerId(comment.isWantAnonymous() ? null : memberInfo.getMemberId()) - .writerName(comment.isWantAnonymous() ? comment.getNickname() : memberInfo.getMemberName()) - .writerImageUrl(comment.isWantAnonymous() ? null : memberInfo.getImageUrl()) - .writerRoleLevel(comment.isWantAnonymous() ? null : memberInfo.getRoleLevel()) - .content(comment.getContent()) - .likes(comment.getLikes()) - .isOwner(isOwner) - .createdAt(comment.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/port/in/RetrieveCommentUseCase.java b/src/main/java/page/clab/api/domain/community/comment/application/port/in/RetrieveCommentUseCase.java index 51d8da8e8..ded720361 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/port/in/RetrieveCommentUseCase.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/port/in/RetrieveCommentUseCase.java @@ -9,5 +9,5 @@ public interface RetrieveCommentUseCase { PagedResponseDto retrieveComments(Long boardId, Pageable pageable); - Comment findByIdOrThrow(Long commentId); + Comment getById(Long commentId); } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/port/out/RetrieveCommentPort.java b/src/main/java/page/clab/api/domain/community/comment/application/port/out/RetrieveCommentPort.java index 4627a08b4..c649f55d1 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/port/out/RetrieveCommentPort.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/port/out/RetrieveCommentPort.java @@ -11,7 +11,7 @@ public interface RetrieveCommentPort { Optional findById(Long commentId); - Comment findByIdOrThrow(Long commentId); + Comment getById(Long commentId); Page findAllByIsDeletedTrueAndBoardId(Long boardId, Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentLikeToggleService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentLikeToggleService.java index 8dcb74d04..58f1877ee 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentLikeToggleService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentLikeToggleService.java @@ -24,11 +24,21 @@ public class CommentLikeToggleService implements ToggleCommentLikeUseCase { private final ExternalRegisterCommentUseCase externalRegisterCommentUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + /** + * 댓글의 좋아요 상태를 토글합니다. + * + *

현재 사용자가 해당 댓글에 좋아요를 누른 상태인 경우 좋아요를 취소하고, + * 그렇지 않은 경우 새롭게 좋아요를 추가합니다. + * 댓글의 좋아요 수를 업데이트한 후 해당 수를 반환합니다.

+ * + * @param commentId 좋아요를 토글할 댓글의 ID + * @return 업데이트된 댓글의 좋아요 수 + */ @Transactional @Override public Long toggleLikeStatus(Long commentId) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); - Comment comment = externalRetrieveCommentUseCase.findByIdOrThrow(commentId); + Comment comment = externalRetrieveCommentUseCase.getById(commentId); return retrieveCommentLikePort.findByCommentIdAndMemberId(comment.getId(), currentMemberId) .map(commentLike -> { removeCommentLikePort.delete(commentLike); @@ -41,6 +51,5 @@ public Long toggleLikeStatus(Long commentId) { comment.incrementLikes(); return externalRegisterCommentUseCase.save(comment).getLikes(); }); - } } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRegisterService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRegisterService.java index 7e0b5c65f..7d63afebb 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRegisterService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRegisterService.java @@ -5,6 +5,7 @@ import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.community.board.application.dto.shared.BoardCommentInfoDto; import page.clab.api.domain.community.board.domain.Board; +import page.clab.api.domain.community.comment.application.dto.mapper.CommentDtoMapper; import page.clab.api.domain.community.comment.application.dto.request.CommentRequestDto; import page.clab.api.domain.community.comment.application.port.in.RegisterCommentUseCase; import page.clab.api.domain.community.comment.application.port.out.RegisterCommentPort; @@ -23,6 +24,7 @@ public class CommentRegisterService implements RegisterCommentUseCase { private final ExternalRetrieveBoardUseCase externalRetrieveBoardUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final CommentDtoMapper mapper; @Transactional @Override @@ -34,9 +36,9 @@ public Long registerComment(Long parentId, Long boardId, CommentRequestDto reque private Comment createAndStoreComment(Long parentId, Long boardId, CommentRequestDto requestDto) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); - Board board = externalRetrieveBoardUseCase.findByIdOrThrow(boardId); + Board board = externalRetrieveBoardUseCase.getById(boardId); Comment parent = findParentComment(parentId); - Comment comment = CommentRequestDto.toEntity(requestDto, board.getId(), currentMemberId, parent); + Comment comment = mapper.fromDto(requestDto, board.getId(), currentMemberId, parent); if (parent != null) { parent.addChildComment(comment); } diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRemoveService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRemoveService.java index ded238590..e9d591d26 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRemoveService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRemoveService.java @@ -21,7 +21,7 @@ public class CommentRemoveService implements RemoveCommentUseCase { @Transactional @Override public Long removeComment(Long commentId) throws PermissionDeniedException { - Comment comment = retrieveCommentPort.findByIdOrThrow(commentId); + Comment comment = retrieveCommentPort.getById(commentId); comment.validateAccessPermission(externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo()); comment.delete(); registerCommentPort.save(comment); diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRetrievalService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRetrievalService.java index c1dee9411..1764c07ee 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.comment.application.dto.mapper.CommentDtoMapper; import page.clab.api.domain.community.comment.application.dto.response.CommentResponseDto; import page.clab.api.domain.community.comment.application.port.in.RetrieveCommentUseCase; import page.clab.api.domain.community.comment.application.port.out.RetrieveCommentPort; @@ -22,6 +23,7 @@ public class CommentRetrievalService implements RetrieveCommentUseCase { private final RetrieveCommentPort retrieveCommentPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final CommentDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -30,8 +32,8 @@ public PagedResponseDto retrieveComments(Long boardId, Pagea } @Override - public Comment findByIdOrThrow(Long commentId) { - return retrieveCommentPort.findByIdOrThrow(commentId); + public Comment getById(Long commentId) { + return retrieveCommentPort.getById(commentId); } @Transactional(readOnly = true) @@ -52,7 +54,7 @@ private CommentResponseDto toCommentResponseDtoWithMemberInfo(Comment comment, S .map(child -> toCommentResponseDtoWithMemberInfo(child, currentMemberId)) .toList(); boolean isOwner = comment.isOwner(currentMemberId); - return CommentResponseDto.toDto(comment, memberInfo, isOwner, childrenDtos); + return mapper.toDto(comment, memberInfo, isOwner, childrenDtos); } private int getNumberOfCurrentPageComments(Page comments, List commentDtos) { diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentUpdateService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentUpdateService.java index 093e75a6d..0e7102adb 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/CommentUpdateService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/CommentUpdateService.java @@ -22,7 +22,7 @@ public class CommentUpdateService implements UpdateCommentUseCase { @Transactional @Override public Long updateComment(Long commentId, CommentUpdateRequestDto requestDto) throws PermissionDeniedException { - Comment comment = retrieveCommentPort.findByIdOrThrow(commentId); + Comment comment = retrieveCommentPort.getById(commentId); comment.validateAccessPermission(externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo()); comment.update(requestDto); registerCommentPort.save(comment); diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/DeletedCommentsRetrievalService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/DeletedCommentsRetrievalService.java index ebd4593f2..d2a3f6709 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/DeletedCommentsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/DeletedCommentsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.comment.application.dto.mapper.CommentDtoMapper; import page.clab.api.domain.community.comment.application.dto.response.DeletedCommentResponseDto; import page.clab.api.domain.community.comment.application.port.in.RetrieveDeletedCommentsUseCase; import page.clab.api.domain.community.comment.application.port.out.RetrieveCommentPort; @@ -22,6 +23,7 @@ public class DeletedCommentsRetrievalService implements RetrieveDeletedCommentsU private final RetrieveCommentPort retrieveCommentPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final CommentDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -31,7 +33,7 @@ public PagedResponseDto retrieveDeletedComments(Long List deletedCommentDtos = comments.stream() .map(comment -> { MemberDetailedInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberDetailedInfoById(comment.getWriterId()); - return DeletedCommentResponseDto.toDto(comment, memberInfo, comment.isOwner(currentMemberId)); + return mapper.toDto(comment, memberInfo, comment.isOwner(currentMemberId)); }) .toList(); return new PagedResponseDto<>(new PageImpl<>(deletedCommentDtos, pageable, comments.getTotalElements())); diff --git a/src/main/java/page/clab/api/domain/community/comment/application/service/MyCommentsRetrievalService.java b/src/main/java/page/clab/api/domain/community/comment/application/service/MyCommentsRetrievalService.java index 6a3f4e84d..fd383622e 100644 --- a/src/main/java/page/clab/api/domain/community/comment/application/service/MyCommentsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/comment/application/service/MyCommentsRetrievalService.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.community.board.application.dto.shared.BoardCommentInfoDto; +import page.clab.api.domain.community.comment.application.dto.mapper.CommentDtoMapper; import page.clab.api.domain.community.comment.application.dto.response.CommentMyResponseDto; import page.clab.api.domain.community.comment.application.port.in.RetrieveMyCommentsUseCase; import page.clab.api.domain.community.comment.application.port.out.RetrieveCommentPort; @@ -26,6 +27,7 @@ public class MyCommentsRetrievalService implements RetrieveMyCommentsUseCase { private final RetrieveCommentPort retrieveCommentPort; private final ExternalRetrieveBoardUseCase externalRetrieveBoardUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final CommentDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -42,6 +44,6 @@ public PagedResponseDto retrieveMyComments(Pageable pageab private CommentMyResponseDto toCommentMyResponseDto(Comment comment) { MemberDetailedInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberDetailedInfoById(comment.getWriterId()); BoardCommentInfoDto boardInfo = externalRetrieveBoardUseCase.getBoardCommentInfoById(comment.getBoardId()); - return CommentMyResponseDto.toDto(comment, memberInfo, boardInfo, false); + return mapper.toDto(comment, memberInfo, boardInfo, false); } } diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java index b051ec488..03e1a9c7a 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface JobPostingMapper { - JobPostingJpaEntity toJpaEntity(JobPosting domain); + JobPostingJpaEntity toEntity(JobPosting domain); JobPosting toDomain(JobPostingJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java index 3050c7176..e4702e5fe 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/adapter/out/persistence/JobPostingPersistenceAdapter.java @@ -19,7 +19,7 @@ public class JobPostingPersistenceAdapter implements private final JobPostingMapper jobPostingMapper; @Override - public JobPosting findByIdOrThrow(Long jobPostingId) { + public JobPosting getById(Long jobPostingId) { return repository.findById(jobPostingId) .map(jobPostingMapper::toDomain) .orElseThrow(() -> new NotFoundException("[JobPosting] id: " + jobPostingId + "에 해당하는 채용 공고가 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/mapper/JobPostingDtoMapper.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/mapper/JobPostingDtoMapper.java new file mode 100644 index 000000000..5ac936bb9 --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/mapper/JobPostingDtoMapper.java @@ -0,0 +1,33 @@ +package page.clab.api.domain.community.jobPosting.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingDetailsResponseDto; +import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingResponseDto; +import page.clab.api.domain.community.jobPosting.domain.JobPosting; + +@Component +public class JobPostingDtoMapper { + + public JobPostingResponseDto toDto(JobPosting jobPosting) { + return JobPostingResponseDto.builder() + .id(jobPosting.getId()) + .title(jobPosting.getTitle()) + .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) + .jobPostingUrl(jobPosting.getJobPostingUrl()) + .createdAt(jobPosting.getCreatedAt()) + .build(); + } + + public JobPostingDetailsResponseDto toDetailsDto(JobPosting jobPosting) { + return JobPostingDetailsResponseDto.builder() + .id(jobPosting.getId()) + .title(jobPosting.getTitle()) + .careerLevel(jobPosting.getCareerLevel()) + .employmentType(jobPosting.getEmploymentType()) + .companyName(jobPosting.getCompanyName()) + .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) + .jobPostingUrl(jobPosting.getJobPostingUrl()) + .createdAt(jobPosting.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java index d33104028..4d96d9991 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingDetailsResponseDto.java @@ -4,7 +4,6 @@ import lombok.Getter; import page.clab.api.domain.community.jobPosting.domain.CareerLevel; import page.clab.api.domain.community.jobPosting.domain.EmploymentType; -import page.clab.api.domain.community.jobPosting.domain.JobPosting; import java.time.LocalDateTime; @@ -20,17 +19,4 @@ public class JobPostingDetailsResponseDto { private String recruitmentPeriod; private String jobPostingUrl; private LocalDateTime createdAt; - - public static JobPostingDetailsResponseDto toDto(JobPosting jobPosting) { - return JobPostingDetailsResponseDto.builder() - .id(jobPosting.getId()) - .title(jobPosting.getTitle()) - .careerLevel(jobPosting.getCareerLevel()) - .employmentType(jobPosting.getEmploymentType()) - .companyName(jobPosting.getCompanyName()) - .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) - .jobPostingUrl(jobPosting.getJobPostingUrl()) - .createdAt(jobPosting.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java index c76db0ddd..a2ae73a86 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/dto/response/JobPostingResponseDto.java @@ -2,10 +2,8 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.jobPosting.domain.JobPosting; import java.time.LocalDateTime; -import java.util.List; @Getter @Builder @@ -16,20 +14,4 @@ public class JobPostingResponseDto { private String recruitmentPeriod; private String jobPostingUrl; private LocalDateTime createdAt; - - public static JobPostingResponseDto toDto(JobPosting jobPosting) { - return JobPostingResponseDto.builder() - .id(jobPosting.getId()) - .title(jobPosting.getTitle()) - .recruitmentPeriod(jobPosting.getRecruitmentPeriod()) - .jobPostingUrl(jobPosting.getJobPostingUrl()) - .createdAt(jobPosting.getCreatedAt()) - .build(); - } - - public static List toDto(List jobPostingList) { - return jobPostingList.stream() - .map(JobPostingResponseDto::toDto) - .toList(); - } } diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java index 27081e0b7..d75173d61 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/port/out/RetrieveJobPostingPort.java @@ -8,7 +8,7 @@ public interface RetrieveJobPostingPort { - JobPosting findByIdOrThrow(Long jobPostingId); + JobPosting getById(Long jobPostingId); Page findByConditions(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java index 15b038ccb..fa40d12a0 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingDetailsRetrievalService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.jobPosting.application.dto.mapper.JobPostingDtoMapper; import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingDetailsResponseDto; import page.clab.api.domain.community.jobPosting.application.port.in.RetrieveJobPostingDetailsUseCase; import page.clab.api.domain.community.jobPosting.application.port.out.RetrieveJobPostingPort; @@ -13,11 +14,12 @@ public class JobPostingDetailsRetrievalService implements RetrieveJobPostingDetailsUseCase { private final RetrieveJobPostingPort retrieveJobPostingPort; + private final JobPostingDtoMapper mapper; @Transactional(readOnly = true) @Override public JobPostingDetailsResponseDto retrieveJobPostingDetails(Long jobPostingId) { - JobPosting jobPosting = retrieveJobPostingPort.findByIdOrThrow(jobPostingId); - return JobPostingDetailsResponseDto.toDto(jobPosting); + JobPosting jobPosting = retrieveJobPostingPort.getById(jobPostingId); + return mapper.toDetailsDto(jobPosting); } } diff --git a/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java index 6158611e8..2c92c3072 100644 --- a/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/jobPosting/application/service/JobPostingsByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.jobPosting.application.dto.mapper.JobPostingDtoMapper; import page.clab.api.domain.community.jobPosting.application.dto.response.JobPostingResponseDto; import page.clab.api.domain.community.jobPosting.application.port.in.RetrieveJobPostingsByConditionsUseCase; import page.clab.api.domain.community.jobPosting.application.port.out.RetrieveJobPostingPort; @@ -18,11 +19,12 @@ public class JobPostingsByConditionsRetrievalService implements RetrieveJobPostingsByConditionsUseCase { private final RetrieveJobPostingPort retrieveJobPostingPort; + private final JobPostingDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveJobPostings(String title, String companyName, CareerLevel careerLevel, EmploymentType employmentType, Pageable pageable) { Page jobPostings = retrieveJobPostingPort.findByConditions(title, companyName, careerLevel, employmentType, pageable); - return new PagedResponseDto<>(jobPostings.map(JobPostingResponseDto::toDto)); + return new PagedResponseDto<>(jobPostings.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java index 82aefb6c4..67349a249 100644 --- a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface NewsMapper { - NewsJpaEntity toJpaEntity(News news); + NewsJpaEntity toEntity(News news); - News toDomainEntity(NewsJpaEntity jpaEntity); + News toDomain(NewsJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java index 7c1467774..8e02d0b09 100644 --- a/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/community/news/adapter/out/persistence/NewsPersistenceAdapter.java @@ -17,15 +17,15 @@ public class NewsPersistenceAdapter implements private final NewsMapper mapper; @Override - public News findByIdOrThrow(Long id) { + public News getById(Long id) { return repository.findById(id) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[News] id: " + id + "에 해당하는 뉴스가 존재하지 않습니다.")); } @Override public Page findByConditions(String title, String category, Pageable pageable) { return repository.findByConditions(title, category, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/community/news/application/dto/mapper/NewsDtoMapper.java b/src/main/java/page/clab/api/domain/community/news/application/dto/mapper/NewsDtoMapper.java new file mode 100644 index 000000000..d13414b9c --- /dev/null +++ b/src/main/java/page/clab/api/domain/community/news/application/dto/mapper/NewsDtoMapper.java @@ -0,0 +1,34 @@ +package page.clab.api.domain.community.news.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.community.news.application.dto.response.NewsDetailsResponseDto; +import page.clab.api.domain.community.news.application.dto.response.NewsResponseDto; +import page.clab.api.domain.community.news.domain.News; + +@Component +public class NewsDtoMapper { + + public NewsResponseDto toDto(News news) { + return NewsResponseDto.builder() + .id(news.getId()) + .title(news.getTitle()) + .category(news.getCategory()) + .articleUrl(news.getArticleUrl()) + .date(news.getDate()) + .createdAt(news.getCreatedAt()) + .build(); + } + + public NewsDetailsResponseDto toDetailsDto(News news) { + return NewsDetailsResponseDto.builder() + .id(news.getId()) + .title(news.getTitle()) + .category(news.getCategory()) + .content(news.getContent()) + .articleUrl(news.getArticleUrl()) + .source(news.getSource()) + .date(news.getDate()) + .createdAt(news.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java index 61fb337f6..81caef81d 100644 --- a/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsDetailsResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.news.domain.News; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDate; @@ -22,17 +21,4 @@ public class NewsDetailsResponseDto { private List files; private LocalDate date; private LocalDateTime createdAt; - - public static NewsDetailsResponseDto toDto(News news) { - return NewsDetailsResponseDto.builder() - .id(news.getId()) - .title(news.getTitle()) - .category(news.getCategory()) - .content(news.getContent()) - .articleUrl(news.getArticleUrl()) - .source(news.getSource()) - .date(news.getDate()) - .createdAt(news.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java index 2657c2cc2..11612a5f2 100644 --- a/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java +++ b/src/main/java/page/clab/api/domain/community/news/application/dto/response/NewsResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.community.news.domain.News; import java.time.LocalDate; import java.time.LocalDateTime; @@ -17,15 +16,4 @@ public class NewsResponseDto { private String articleUrl; private LocalDate date; private LocalDateTime createdAt; - - public static NewsResponseDto toDto(News news) { - return NewsResponseDto.builder() - .id(news.getId()) - .title(news.getTitle()) - .category(news.getCategory()) - .articleUrl(news.getArticleUrl()) - .date(news.getDate()) - .createdAt(news.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java b/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java index 5144473c4..001553eb7 100644 --- a/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java +++ b/src/main/java/page/clab/api/domain/community/news/application/port/out/RetrieveNewsPort.java @@ -6,7 +6,7 @@ public interface RetrieveNewsPort { - News findByIdOrThrow(Long id); + News getById(Long id); Page findByConditions(String title, String category, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java index f36c5f288..e9e472a64 100644 --- a/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/news/application/service/NewsByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.news.application.dto.mapper.NewsDtoMapper; import page.clab.api.domain.community.news.application.dto.response.NewsResponseDto; import page.clab.api.domain.community.news.application.port.in.RetrieveNewsByConditionsUseCase; import page.clab.api.domain.community.news.application.port.out.RetrieveNewsPort; @@ -16,11 +17,12 @@ public class NewsByConditionsRetrievalService implements RetrieveNewsByConditionsUseCase { private final RetrieveNewsPort retrieveNewsPort; + private final NewsDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveNews(String title, String category, Pageable pageable) { Page newsPage = retrieveNewsPort.findByConditions(title, category, pageable); - return new PagedResponseDto<>(newsPage.map(NewsResponseDto::toDto)); + return new PagedResponseDto<>(newsPage.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java index c98ea2140..65cc13848 100644 --- a/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/community/news/application/service/NewsDetailsRetrievalService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.news.application.dto.mapper.NewsDtoMapper; import page.clab.api.domain.community.news.application.dto.response.NewsDetailsResponseDto; import page.clab.api.domain.community.news.application.port.in.RetrieveNewsDetailsUseCase; import page.clab.api.domain.community.news.application.port.out.RetrieveNewsPort; @@ -13,11 +14,12 @@ public class NewsDetailsRetrievalService implements RetrieveNewsDetailsUseCase { private final RetrieveNewsPort retrieveNewsPort; + private final NewsDtoMapper mapper; @Transactional(readOnly = true) @Override public NewsDetailsResponseDto retrieveNewsDetails(Long newsId) { - News news = retrieveNewsPort.findByIdOrThrow(newsId); - return NewsDetailsResponseDto.toDto(news); + News news = retrieveNewsPort.getById(newsId); + return mapper.toDetailsDto(news); } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationMapper.java b/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationMapper.java index ee748ef82..152072ac4 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationMapper.java +++ b/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface ApplicationMapper { - ApplicationJpaEntity toJpaEntity(Application application); + ApplicationJpaEntity toEntity(Application application); Application toDomain(ApplicationJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationPersistenceAdapter.java b/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationPersistenceAdapter.java index b28f1dd08..679532cdf 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/hiring/application/adapter/out/persistence/ApplicationPersistenceAdapter.java @@ -23,7 +23,7 @@ public class ApplicationPersistenceAdapter implements @Override public Application save(Application application) { - ApplicationJpaEntity entity = applicationMapper.toJpaEntity(application); + ApplicationJpaEntity entity = applicationMapper.toEntity(application); ApplicationJpaEntity savedEntity = applicationRepository.save(entity); return applicationMapper.toDomain(savedEntity); } @@ -35,7 +35,7 @@ public Optional findById(ApplicationId applicationId) { } @Override - public Application findByIdOrThrow(ApplicationId applicationId) { + public Application getById(ApplicationId applicationId) { return applicationRepository.findById(applicationId) .map(applicationMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Application] id: " + applicationId + "에 해당하는 지원서가 존재하지 않습니다.")); @@ -55,7 +55,7 @@ public List findByRecruitmentIdAndIsPass(Long recruitmentId, boolea } @Override - public Application findByRecruitmentIdAndStudentIdOrThrow(Long recruitmentId, String studentId) { + public Application getByRecruitmentIdAndStudentId(Long recruitmentId, String studentId) { return applicationRepository.findByRecruitmentIdAndStudentId(recruitmentId, studentId) .map(applicationMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Application] recruitmentId: " + recruitmentId + ", studentId: " + studentId + "에 해당하는 지원서가 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/dto/mapper/ApplicationDtoMapper.java b/src/main/java/page/clab/api/domain/hiring/application/application/dto/mapper/ApplicationDtoMapper.java new file mode 100644 index 000000000..3ebd8f89d --- /dev/null +++ b/src/main/java/page/clab/api/domain/hiring/application/application/dto/mapper/ApplicationDtoMapper.java @@ -0,0 +1,78 @@ +package page.clab.api.domain.hiring.application.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.hiring.application.application.dto.request.ApplicationMemberCreationDto; +import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto; +import page.clab.api.domain.hiring.application.application.dto.response.ApplicationPassResponseDto; +import page.clab.api.domain.hiring.application.application.dto.response.ApplicationResponseDto; +import page.clab.api.domain.hiring.application.domain.Application; +import page.clab.api.global.common.domain.Contact; + +@Component +public class ApplicationDtoMapper { + + public Application fromDto(ApplicationRequestDto requestDto) { + return Application.builder() + .studentId(requestDto.getStudentId()) + .recruitmentId(requestDto.getRecruitmentId()) + .name(requestDto.getName()) + .contact(Contact.of(requestDto.getContact()).getValue()) + .email(requestDto.getEmail()) + .department(requestDto.getDepartment()) + .grade(requestDto.getGrade()) + .birth(requestDto.getBirth()) + .address(requestDto.getAddress()) + .interests(requestDto.getInterests()) + .otherActivities(requestDto.getOtherActivities()) + .githubUrl(requestDto.getGithubUrl()) + .applicationType(requestDto.getApplicationType()) + .isPass(false) + .isDeleted(false) + .build(); + } + + public ApplicationResponseDto toDto(Application application) { + return ApplicationResponseDto.builder() + .studentId(application.getStudentId()) + .recruitmentId(application.getRecruitmentId()) + .name(application.getName()) + .contact(application.getContact()) + .email(application.getEmail()) + .department(application.getDepartment()) + .grade(application.getGrade()) + .birth(application.getBirth()) + .address(application.getAddress()) + .interests(application.getInterests()) + .otherActivities(application.getOtherActivities()) + .githubUrl(application.getGithubUrl()) + .applicationType(application.getApplicationType()) + .isPass(application.getIsPass()) + .updatedAt(application.getUpdatedAt()) + .createdAt(application.getCreatedAt()) + .build(); + } + + public ApplicationMemberCreationDto toCreationDto(Application application) { + return ApplicationMemberCreationDto.builder() + .studentId(application.getStudentId()) + .name(application.getName()) + .contact(application.getContact()) + .email(application.getEmail()) + .department(application.getDepartment()) + .grade(application.getGrade()) + .birth(application.getBirth()) + .address(application.getAddress()) + .interests(application.getInterests()) + .githubUrl(application.getGithubUrl()) + .build(); + } + + public ApplicationPassResponseDto toPassDto(Application application) { + return ApplicationPassResponseDto.builder() + .recruitmentId(application.getRecruitmentId()) + .name(application.getName()) + .applicationType(application.getApplicationType()) + .isPass(application.getIsPass()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationMemberCreationDto.java b/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationMemberCreationDto.java index 90b6bd3d6..44a44ec4d 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationMemberCreationDto.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationMemberCreationDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.hiring.application.domain.Application; import java.time.LocalDate; @@ -20,19 +19,4 @@ public class ApplicationMemberCreationDto { private String address; private String interests; private String githubUrl; - - public static ApplicationMemberCreationDto toDto(Application application) { - return ApplicationMemberCreationDto.builder() - .studentId(application.getStudentId()) - .name(application.getName()) - .contact(application.getContact()) - .email(application.getEmail()) - .department(application.getDepartment()) - .grade(application.getGrade()) - .birth(application.getBirth()) - .address(application.getAddress()) - .interests(application.getInterests()) - .githubUrl(application.getGithubUrl()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationRequestDto.java b/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationRequestDto.java index 279d95608..ec9f61197 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationRequestDto.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/dto/request/ApplicationRequestDto.java @@ -4,9 +4,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.hiring.application.domain.Application; import page.clab.api.domain.hiring.application.domain.ApplicationType; -import page.clab.api.global.common.domain.Contact; import java.time.LocalDate; @@ -64,24 +62,4 @@ public class ApplicationRequestDto { @NotNull(message = "{notNull.application.applicationType}") @Schema(description = "구분", example = "NORMAL", required = true) private ApplicationType applicationType; - - public static Application toEntity(ApplicationRequestDto requestDto) { - return Application.builder() - .studentId(requestDto.getStudentId()) - .recruitmentId(requestDto.getRecruitmentId()) - .name(requestDto.getName()) - .contact(Contact.of(requestDto.getContact()).getValue()) - .email(requestDto.getEmail()) - .department(requestDto.getDepartment()) - .grade(requestDto.getGrade()) - .birth(requestDto.getBirth()) - .address(requestDto.getAddress()) - .interests(requestDto.getInterests()) - .otherActivities(requestDto.getOtherActivities()) - .githubUrl(requestDto.getGithubUrl()) - .applicationType(requestDto.getApplicationType()) - .isPass(false) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java b/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java index f779b7324..c5d264021 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationPassResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.hiring.application.domain.Application; import page.clab.api.domain.hiring.application.domain.ApplicationType; @Getter @@ -14,15 +13,6 @@ public class ApplicationPassResponseDto { private ApplicationType applicationType; private Boolean isPass; - public static ApplicationPassResponseDto toDto(Application application) { - return ApplicationPassResponseDto.builder() - .recruitmentId(application.getRecruitmentId()) - .name(application.getName()) - .applicationType(application.getApplicationType()) - .isPass(application.getIsPass()) - .build(); - } - public static ApplicationPassResponseDto defaultResponse() { return ApplicationPassResponseDto.builder() .isPass(false) diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationResponseDto.java b/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationResponseDto.java index a642fbcf5..9a0693de3 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationResponseDto.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/dto/response/ApplicationResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.hiring.application.domain.Application; import page.clab.api.domain.hiring.application.domain.ApplicationType; import java.time.LocalDate; @@ -28,25 +27,4 @@ public class ApplicationResponseDto { private Boolean isPass; private LocalDateTime updatedAt; private LocalDateTime createdAt; - - public static ApplicationResponseDto toDto(Application application) { - return ApplicationResponseDto.builder() - .studentId(application.getStudentId()) - .recruitmentId(application.getRecruitmentId()) - .name(application.getName()) - .contact(application.getContact()) - .email(application.getEmail()) - .department(application.getDepartment()) - .grade(application.getGrade()) - .birth(application.getBirth()) - .address(application.getAddress()) - .interests(application.getInterests()) - .otherActivities(application.getOtherActivities()) - .githubUrl(application.getGithubUrl()) - .applicationType(application.getApplicationType()) - .isPass(application.getIsPass()) - .updatedAt(application.getUpdatedAt()) - .createdAt(application.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/port/out/RetrieveApplicationPort.java b/src/main/java/page/clab/api/domain/hiring/application/application/port/out/RetrieveApplicationPort.java index 9f0a8e67d..130e0e034 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/port/out/RetrieveApplicationPort.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/port/out/RetrieveApplicationPort.java @@ -12,11 +12,11 @@ public interface RetrieveApplicationPort { Optional findById(ApplicationId applicationId); - Application findByIdOrThrow(ApplicationId applicationId); + Application getById(ApplicationId applicationId); Page findByConditions(Long recruitmentId, String studentId, Boolean isPass, Pageable pageable); List findByRecruitmentIdAndIsPass(Long recruitmentId, boolean isPass); - Application findByRecruitmentIdAndStudentIdOrThrow(Long recruitmentId, String studentId); + Application getByRecruitmentIdAndStudentId(Long recruitmentId, String studentId); } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java index e6adb7a6b..357a59cde 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java @@ -1,15 +1,18 @@ package page.clab.api.domain.hiring.application.application.service; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.hiring.application.application.dto.mapper.ApplicationDtoMapper; import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto; import page.clab.api.domain.hiring.application.application.port.in.ApplyForApplicationUseCase; import page.clab.api.domain.hiring.application.application.port.out.RegisterApplicationPort; import page.clab.api.domain.hiring.application.domain.Application; import page.clab.api.external.hiring.application.application.port.ExternalRetrieveRecruitmentUseCase; import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase; -import page.clab.api.global.common.slack.application.SlackService; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType; @Service @RequiredArgsConstructor @@ -18,17 +21,21 @@ public class ApplicationApplyService implements ApplyForApplicationUseCase { private final RegisterApplicationPort registerApplicationPort; private final ExternalRetrieveRecruitmentUseCase externalRetrieveRecruitmentUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + private final ApplicationDtoMapper mapper; @Transactional @Override public String applyForClub(ApplicationRequestDto requestDto) { externalRetrieveRecruitmentUseCase.validateRecruitmentForApplication(requestDto.getRecruitmentId()); - Application application = ApplicationRequestDto.toEntity(requestDto); + Application application = mapper.fromDto(requestDto); String applicationType = application.getApplicationTypeForNotificationPrefix(); externalSendNotificationUseCase.sendNotificationToAdmins(applicationType + requestDto.getStudentId() + " " + requestDto.getName() + "님이 지원하였습니다."); - slackService.sendNewApplicationNotification(requestDto); + + eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_APPLICATION, null, + requestDto)); + return registerApplicationPort.save(application).getStudentId(); } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationMemberRegisterService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationMemberRegisterService.java index b66ff229a..5051d6b0f 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationMemberRegisterService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationMemberRegisterService.java @@ -5,6 +5,7 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.hiring.application.application.dto.mapper.ApplicationDtoMapper; import page.clab.api.domain.hiring.application.application.dto.request.ApplicationMemberCreationDto; import page.clab.api.domain.hiring.application.application.event.ApplicationMemberCreatedEvent; import page.clab.api.domain.hiring.application.application.event.PositionCreatedByApplicationEvent; @@ -29,7 +30,9 @@ public class ApplicationMemberRegisterService implements RegisterMembersByRecrui private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalRetrievePositionUseCase externalRetrievePositionUseCase; private final ApplicationEventPublisher eventPublisher; + private final ApplicationDtoMapper mapper; + // 해당 모집의 합격자들의 계정을 일괄 생성합니다. @Transactional @Override public List registerMembersByRecruitment(Long recruitmentId) { @@ -39,10 +42,11 @@ public List registerMembersByRecruitment(Long recruitmentId) { .toList(); } + // 해당 모집에서 합격한 특정 지원자의 계정을 생성합니다. @Transactional @Override public String registerMembersByRecruitment(Long recruitmentId, String studentId) { - Application application = retrieveApplicationPort.findByRecruitmentIdAndStudentIdOrThrow(recruitmentId, studentId); + Application application = retrieveApplicationPort.getByRecruitmentIdAndStudentId(recruitmentId, studentId); validateApplicationIsPass(application); return createMemberFromApplication(application); } @@ -62,9 +66,9 @@ private String createMemberFromApplication(Application application) { private Member createMemberByApplication(Application application) { return externalRetrieveMemberUseCase.findById(application.getStudentId()) .orElseGet(() -> { - ApplicationMemberCreationDto dto = ApplicationMemberCreationDto.toDto(application); + ApplicationMemberCreationDto dto = mapper.toCreationDto(application); eventPublisher.publishEvent(new ApplicationMemberCreatedEvent(this, dto)); - return externalRetrieveMemberUseCase.findByIdOrThrow(application.getStudentId()); + return externalRetrieveMemberUseCase.getById(application.getStudentId()); }); } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java index 348aa6df4..168c5007e 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationPassCheckService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.hiring.application.adapter.out.persistence.ApplicationId; +import page.clab.api.domain.hiring.application.application.dto.mapper.ApplicationDtoMapper; import page.clab.api.domain.hiring.application.application.dto.response.ApplicationPassResponseDto; import page.clab.api.domain.hiring.application.application.port.in.CheckApplicationPassStatusUseCase; import page.clab.api.domain.hiring.application.application.port.out.RetrieveApplicationPort; @@ -16,17 +17,18 @@ public class ApplicationPassCheckService implements CheckApplicationPassStatusUs private final RetrieveApplicationPort retrieveApplicationPort; private final RetrieveRecruitmentPort retrieveRecruitmentPort; + private final ApplicationDtoMapper mapper; @Transactional(readOnly = true) @Override public ApplicationPassResponseDto checkPassStatus(Long recruitmentId, String studentId) { ApplicationId id = ApplicationId.create(studentId, recruitmentId); - Recruitment recruitment = retrieveRecruitmentPort.findByIdOrThrow(recruitmentId); + Recruitment recruitment = retrieveRecruitmentPort.getById(recruitmentId); recruitment.validateEndDateWithin7Days(); return retrieveApplicationPort.findById(id) - .map(ApplicationPassResponseDto::toDto) + .map(mapper::toPassDto) .orElseGet(ApplicationPassResponseDto::defaultResponse); } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationRetrievalService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationRetrievalService.java index 2b4b6c38a..a6ea0306b 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationRetrievalService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.hiring.application.application.dto.mapper.ApplicationDtoMapper; import page.clab.api.domain.hiring.application.application.dto.response.ApplicationResponseDto; import page.clab.api.domain.hiring.application.application.port.in.RetrieveApplicationsUseCase; import page.clab.api.domain.hiring.application.application.port.out.RetrieveApplicationPort; @@ -16,11 +17,12 @@ public class ApplicationRetrievalService implements RetrieveApplicationsUseCase { private final RetrieveApplicationPort retrieveApplicationPort; + private final ApplicationDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveApplications(Long recruitmentId, String studentId, Boolean isPass, Pageable pageable) { Page applications = retrieveApplicationPort.findByConditions(recruitmentId, studentId, isPass, pageable); - return new PagedResponseDto<>(applications.map(ApplicationResponseDto::toDto)); + return new PagedResponseDto<>(applications.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApproveApplicationService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApproveApplicationService.java index b02f8c782..230c7d224 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApproveApplicationService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApproveApplicationService.java @@ -17,7 +17,7 @@ public class ApproveApplicationService implements ApproveApplicationUseCase { @Override public Long approveApplication(Long recruitmentId, String studentId) { - Application application = retrieveApplicationPort.findByIdOrThrow(ApplicationId.create(studentId, recruitmentId)); + Application application = retrieveApplicationPort.getById(ApplicationId.create(studentId, recruitmentId)); application.approve(); registerApplicationPort.save(application); return recruitmentId; diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/RejectApplicationService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/RejectApplicationService.java index 91bbfc19b..82b0a82c1 100644 --- a/src/main/java/page/clab/api/domain/hiring/application/application/service/RejectApplicationService.java +++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/RejectApplicationService.java @@ -17,7 +17,7 @@ public class RejectApplicationService implements RejectApplicationUseCase { @Override public Long rejectApplication(Long recruitmentId, String studentId) { - Application application = retrieveApplicationPort.findByIdOrThrow(ApplicationId.create(studentId, recruitmentId)); + Application application = retrieveApplicationPort.getById(ApplicationId.create(studentId, recruitmentId)); application.reject(); registerApplicationPort.save(application); return recruitmentId; diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentMapper.java b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentMapper.java index 7d7ea9a21..9a6ec3d09 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentMapper.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface RecruitmentMapper { - RecruitmentJpaEntity toJpaEntity(Recruitment recruitment); + RecruitmentJpaEntity toEntity(Recruitment recruitment); - Recruitment toDomainEntity(RecruitmentJpaEntity entity); + Recruitment toDomain(RecruitmentJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java index d2025bd4e..8d32312db 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/adapter/out/persistence/RecruitmentPersistenceAdapter.java @@ -23,43 +23,43 @@ public class RecruitmentPersistenceAdapter implements @Override public Recruitment save(Recruitment recruitment) { - RecruitmentJpaEntity entity = mapper.toJpaEntity(recruitment); + RecruitmentJpaEntity entity = mapper.toEntity(recruitment); RecruitmentJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override public Recruitment update(Recruitment recruitment) { - RecruitmentJpaEntity entity = mapper.toJpaEntity(recruitment); + RecruitmentJpaEntity entity = mapper.toEntity(recruitment); RecruitmentJpaEntity updatedEntity = repository.save(entity); - return mapper.toDomainEntity(updatedEntity); + return mapper.toDomain(updatedEntity); } @Override - public Recruitment findByIdOrThrow(Long recruitmentId) { + public Recruitment getById(Long recruitmentId) { return repository.findById(recruitmentId) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[Recruitment] id: " + recruitmentId + "에 해당하는 모집 공고가 존재하지 않습니다.")); } @Override public List findAll() { return repository.findAll().stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } @Override public List findTop5ByOrderByCreatedAtDesc() { return repository.findTop5ByOrderByCreatedAtDesc().stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } @Override public List findByEndDateBetween(LocalDateTime weekAgo, LocalDateTime now) { return repository.findByEndDateBetween(weekAgo, now).stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/mapper/RecruitmentDtoMapper.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/mapper/RecruitmentDtoMapper.java new file mode 100644 index 000000000..3e6627583 --- /dev/null +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/mapper/RecruitmentDtoMapper.java @@ -0,0 +1,40 @@ +package page.clab.api.domain.hiring.recruitment.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.hiring.recruitment.application.dto.request.RecruitmentRequestDto; +import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentEndDateResponseDto; +import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentResponseDto; +import page.clab.api.domain.hiring.recruitment.domain.Recruitment; + +@Component +public class RecruitmentDtoMapper { + + public Recruitment fromDto(RecruitmentRequestDto requestDto) { + return Recruitment.builder() + .startDate(requestDto.getStartDate()) + .endDate(requestDto.getEndDate()) + .applicationType(requestDto.getApplicationType()) + .target(requestDto.getTarget()) + .isDeleted(false) + .build(); + } + + public RecruitmentResponseDto toDto(Recruitment recruitment) { + return RecruitmentResponseDto.builder() + .id(recruitment.getId()) + .startDate(recruitment.getStartDate()) + .endDate(recruitment.getEndDate()) + .applicationType(recruitment.getApplicationType()) + .target(recruitment.getTarget()) + .status(recruitment.getStatus().getDescription()) + .updatedAt(recruitment.getUpdatedAt()) + .build(); + } + + public RecruitmentEndDateResponseDto toEndDateDto(Recruitment recruitment) { + return RecruitmentEndDateResponseDto.builder() + .id(recruitment.getId()) + .applicationType(recruitment.getApplicationType()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/request/RecruitmentRequestDto.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/request/RecruitmentRequestDto.java index 8033ad1d7..87b32e0b4 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/request/RecruitmentRequestDto.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/request/RecruitmentRequestDto.java @@ -5,7 +5,6 @@ import lombok.Getter; import lombok.Setter; import page.clab.api.domain.hiring.application.domain.ApplicationType; -import page.clab.api.domain.hiring.recruitment.domain.Recruitment; import java.time.LocalDateTime; @@ -28,14 +27,4 @@ public class RecruitmentRequestDto { @NotNull(message = "{notNull.recruitment.target}") @Schema(description = "대상", example = "2~3학년", required = true) private String target; - - public static Recruitment toEntity(RecruitmentRequestDto requestDto) { - return Recruitment.builder() - .startDate(requestDto.getStartDate()) - .endDate(requestDto.getEndDate()) - .applicationType(requestDto.getApplicationType()) - .target(requestDto.getTarget()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java index c01f86d37..717b291b9 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentEndDateResponseDto.java @@ -3,9 +3,6 @@ import lombok.Builder; import lombok.Getter; import page.clab.api.domain.hiring.application.domain.ApplicationType; -import page.clab.api.domain.hiring.recruitment.domain.Recruitment; - -import java.util.List; @Getter @Builder @@ -13,17 +10,4 @@ public class RecruitmentEndDateResponseDto { private Long id; private ApplicationType applicationType; - - public static List toDto(List recruitments) { - return recruitments.stream() - .map(RecruitmentEndDateResponseDto::toDto) - .toList(); - } - - public static RecruitmentEndDateResponseDto toDto(Recruitment recruitment) { - return RecruitmentEndDateResponseDto.builder() - .id(recruitment.getId()) - .applicationType(recruitment.getApplicationType()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentResponseDto.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentResponseDto.java index a365b51b2..a7234bd8f 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentResponseDto.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/dto/response/RecruitmentResponseDto.java @@ -3,7 +3,6 @@ import lombok.Builder; import lombok.Getter; import page.clab.api.domain.hiring.application.domain.ApplicationType; -import page.clab.api.domain.hiring.recruitment.domain.Recruitment; import java.time.LocalDateTime; @@ -18,16 +17,4 @@ public class RecruitmentResponseDto { private String target; private String status; private LocalDateTime updatedAt; - - public static RecruitmentResponseDto toDto(Recruitment recruitment) { - return RecruitmentResponseDto.builder() - .id(recruitment.getId()) - .startDate(recruitment.getStartDate()) - .endDate(recruitment.getEndDate()) - .applicationType(recruitment.getApplicationType()) - .target(recruitment.getTarget()) - .status(recruitment.getStatus().getDescription()) - .updatedAt(recruitment.getUpdatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecruitmentUseCase.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecruitmentUseCase.java index fcce1cb9e..fdfaa40c9 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecruitmentUseCase.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/in/RetrieveRecruitmentUseCase.java @@ -4,5 +4,5 @@ import page.clab.api.domain.hiring.recruitment.domain.Recruitment; public interface RetrieveRecruitmentUseCase { - Recruitment findByIdOrThrow(Long recruitmentId); + Recruitment getById(Long recruitmentId); } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java index 67627efac..36d5057e5 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/port/out/RetrieveRecruitmentPort.java @@ -7,7 +7,7 @@ public interface RetrieveRecruitmentPort { - Recruitment findByIdOrThrow(Long recruitmentId); + Recruitment getById(Long recruitmentId); List findAll(); diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java index 038f74be8..3f9d86892 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecentRecruitmentsRetrievalService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.hiring.recruitment.application.dto.mapper.RecruitmentDtoMapper; import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentEndDateResponseDto; import page.clab.api.domain.hiring.recruitment.application.dto.response.RecruitmentResponseDto; import page.clab.api.domain.hiring.recruitment.application.port.in.RetrieveRecentRecruitmentsUseCase; @@ -17,12 +18,13 @@ public class RecentRecruitmentsRetrievalService implements RetrieveRecentRecruitmentsUseCase { private final RetrieveRecruitmentPort retrieveRecruitmentPort; + private final RecruitmentDtoMapper mapper; @Transactional(readOnly = true) @Override public List retrieveRecentRecruitments() { return retrieveRecruitmentPort.findTop5ByOrderByCreatedAtDesc().stream() - .map(RecruitmentResponseDto::toDto) + .map(mapper::toDto) .toList(); } @@ -34,7 +36,7 @@ public List retrieveRecruitmentsByEndDate() { LocalDateTime endOfDay = now.with(LocalTime.MAX); return retrieveRecruitmentPort.findByEndDateBetween(weekAgo, endOfDay).stream() - .map(RecruitmentEndDateResponseDto::toDto) + .map(mapper::toEndDateDto) .toList(); } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRegisterService.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRegisterService.java index 0ef956379..b08d2ae8d 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRegisterService.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.hiring.recruitment.application.dto.mapper.RecruitmentDtoMapper; import page.clab.api.domain.hiring.recruitment.application.dto.request.RecruitmentRequestDto; import page.clab.api.domain.hiring.recruitment.application.port.in.RegisterRecruitmentUseCase; import page.clab.api.domain.hiring.recruitment.application.port.out.RegisterRecruitmentPort; @@ -16,11 +17,12 @@ public class RecruitmentRegisterService implements RegisterRecruitmentUseCase { private final RegisterRecruitmentPort registerRecruitmentPort; private final RecruitmentStatusUpdater recruitmentStatusUpdater; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + private final RecruitmentDtoMapper mapper; @Transactional @Override public Long registerRecruitment(RecruitmentRequestDto requestDto) { - Recruitment recruitment = RecruitmentRequestDto.toEntity(requestDto); + Recruitment recruitment = mapper.fromDto(requestDto); recruitmentStatusUpdater.updateRecruitmentStatus(recruitment); recruitment.validateDateRange(); externalSendNotificationUseCase.sendNotificationToAllMembers("새로운 모집 공고가 등록되었습니다."); diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRetrievalService.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRetrievalService.java index d1bab5e13..af8e2b3da 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRetrievalService.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentRetrievalService.java @@ -13,7 +13,7 @@ public class RecruitmentRetrievalService implements RetrieveRecruitmentUseCase { private final RetrieveRecruitmentPort retrieveRecruitmentPort; @Override - public Recruitment findByIdOrThrow(Long recruitmentId) { - return retrieveRecruitmentPort.findByIdOrThrow(recruitmentId); + public Recruitment getById(Long recruitmentId) { + return retrieveRecruitmentPort.getById(recruitmentId); } } diff --git a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentUpdateService.java b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentUpdateService.java index e71c63715..e3fe62db9 100644 --- a/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentUpdateService.java +++ b/src/main/java/page/clab/api/domain/hiring/recruitment/application/service/RecruitmentUpdateService.java @@ -20,7 +20,7 @@ public class RecruitmentUpdateService implements UpdateRecruitmentUseCase { @Transactional @Override public Long updateRecruitment(Long recruitmentId, RecruitmentUpdateRequestDto requestDto) { - Recruitment recruitment = retrieveRecruitmentUseCase.findByIdOrThrow(recruitmentId); + Recruitment recruitment = retrieveRecruitmentUseCase.getById(recruitmentId); recruitment.update(requestDto); recruitmentStatusUpdater.updateRecruitmentStatus(recruitment); recruitment.validateDateRange(); diff --git a/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookMapper.java b/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookMapper.java index 66b15e254..9ba8d3389 100644 --- a/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookMapper.java +++ b/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface BookMapper { - BookJpaEntity toJpaEntity(Book book); + BookJpaEntity toEntity(Book book); Book toDomain(BookJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookPersistenceAdapter.java b/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookPersistenceAdapter.java index dfa60ca92..197c219fe 100644 --- a/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/library/book/adapter/out/persistence/BookPersistenceAdapter.java @@ -22,7 +22,7 @@ public class BookPersistenceAdapter implements @Override public Book save(Book book) { - BookJpaEntity entity = bookMapper.toJpaEntity(book); + BookJpaEntity entity = bookMapper.toEntity(book); BookJpaEntity savedEntity = bookRepository.save(entity); return bookMapper.toDomain(savedEntity); } @@ -33,7 +33,7 @@ public void delete(Book book) { } @Override - public Book findByIdOrThrow(Long bookId) { + public Book getById(Long bookId) { return bookRepository.findById(bookId) .map(bookMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Book] id: " + bookId + "에 해당하는 책이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/library/book/application/dto/mapper/BookDtoMapper.java b/src/main/java/page/clab/api/domain/library/book/application/dto/mapper/BookDtoMapper.java new file mode 100644 index 000000000..220ca1b06 --- /dev/null +++ b/src/main/java/page/clab/api/domain/library/book/application/dto/mapper/BookDtoMapper.java @@ -0,0 +1,60 @@ +package page.clab.api.domain.library.book.application.dto.mapper; + +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import page.clab.api.domain.library.book.application.dto.request.BookRequestDto; +import page.clab.api.domain.library.book.application.dto.response.BookDetailsResponseDto; +import page.clab.api.domain.library.book.application.dto.response.BookResponseDto; +import page.clab.api.domain.library.book.domain.Book; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +public class BookDtoMapper { + + public Book fromDto(BookRequestDto requestDto) { + return Book.builder() + .category(requestDto.getCategory()) + .title(requestDto.getTitle()) + .author(requestDto.getAuthor()) + .publisher(requestDto.getPublisher()) + .imageUrl(requestDto.getImageUrl()) + .reviewLinks(CollectionUtils.isEmpty(requestDto.getReviewLinks()) ? List.of() : requestDto.getReviewLinks()) + .isDeleted(false) + .build(); + } + + public BookResponseDto toDto(Book book, String borrowerName, LocalDateTime dueDate) { + return BookResponseDto.builder() + .id(book.getId()) + .borrowerId(book.getBorrowerId() == null ? null : book.getBorrowerId()) + .borrowerName(book.getBorrowerId() == null ? null : borrowerName) + .category(book.getCategory()) + .title(book.getTitle()) + .author(book.getAuthor()) + .publisher(book.getPublisher()) + .imageUrl(book.getImageUrl()) + .dueDate(dueDate) + .createdAt(book.getCreatedAt()) + .updatedAt(book.getUpdatedAt()) + .build(); + } + + public BookDetailsResponseDto toDetailsDto(Book book, String borrowerName, LocalDateTime dueDate) { + return BookDetailsResponseDto.builder() + .id(book.getId()) + .borrowerId(book.getBorrowerId() == null ? null : book.getBorrowerId()) + .borrowerName(book.getBorrowerId() == null ? null : borrowerName) + .category(book.getCategory()) + .title(book.getTitle()) + .author(book.getAuthor()) + .publisher(book.getPublisher()) + .imageUrl(book.getImageUrl()) + .reviewLinks(book.getReviewLinks()) + .dueDate(dueDate) + .createdAt(book.getCreatedAt()) + .updatedAt(book.getUpdatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/library/book/application/dto/request/BookRequestDto.java b/src/main/java/page/clab/api/domain/library/book/application/dto/request/BookRequestDto.java index b41245403..a8bc9237d 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/dto/request/BookRequestDto.java +++ b/src/main/java/page/clab/api/domain/library/book/application/dto/request/BookRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.library.book.domain.Book; import java.util.List; @@ -33,16 +32,4 @@ public class BookRequestDto { @Schema(description = "리뷰 링크", example = "[\"https://www.yes24.com/Product/Goods/7516911\",\"https://www.aladin.co.kr/shop/wproduct.aspx?ISBN=8960773433&start=pnaver_02\"]") private List reviewLinks; - - public static Book toEntity(BookRequestDto requestDto) { - return Book.builder() - .category(requestDto.getCategory()) - .title(requestDto.getTitle()) - .author(requestDto.getAuthor()) - .publisher(requestDto.getPublisher()) - .imageUrl(requestDto.getImageUrl()) - .reviewLinks(requestDto.reviewLinks == null ? List.of() : requestDto.reviewLinks) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookDetailsResponseDto.java b/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookDetailsResponseDto.java index 6e3f0b552..172474ae9 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookDetailsResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.library.book.domain.Book; import java.time.LocalDateTime; import java.util.List; @@ -23,21 +22,4 @@ public class BookDetailsResponseDto { private LocalDateTime dueDate; private LocalDateTime createdAt; private LocalDateTime updatedAt; - - public static BookDetailsResponseDto toDto(Book book, String borrowerName, LocalDateTime dueDate) { - return BookDetailsResponseDto.builder() - .id(book.getId()) - .borrowerId(book.getBorrowerId() == null ? null : book.getBorrowerId()) - .borrowerName(book.getBorrowerId() == null ? null : borrowerName) - .category(book.getCategory()) - .title(book.getTitle()) - .author(book.getAuthor()) - .publisher(book.getPublisher()) - .imageUrl(book.getImageUrl()) - .reviewLinks(book.getReviewLinks()) - .dueDate(dueDate) - .createdAt(book.getCreatedAt()) - .updatedAt(book.getUpdatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookResponseDto.java b/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookResponseDto.java index 298c655d4..b4e0b494f 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookResponseDto.java +++ b/src/main/java/page/clab/api/domain/library/book/application/dto/response/BookResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.library.book.domain.Book; import java.time.LocalDateTime; @@ -21,20 +20,4 @@ public class BookResponseDto { private LocalDateTime dueDate; private LocalDateTime createdAt; private LocalDateTime updatedAt; - - public static BookResponseDto toDto(Book book, String borrowerName, LocalDateTime dueDate) { - return BookResponseDto.builder() - .id(book.getId()) - .borrowerId(book.getBorrowerId() == null ? null : book.getBorrowerId()) - .borrowerName(book.getBorrowerId() == null ? null : borrowerName) - .category(book.getCategory()) - .title(book.getTitle()) - .author(book.getAuthor()) - .publisher(book.getPublisher()) - .imageUrl(book.getImageUrl()) - .dueDate(dueDate) - .createdAt(book.getCreatedAt()) - .updatedAt(book.getUpdatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/library/book/application/port/out/RetrieveBookPort.java b/src/main/java/page/clab/api/domain/library/book/application/port/out/RetrieveBookPort.java index 198680e16..e2ed89e83 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/port/out/RetrieveBookPort.java +++ b/src/main/java/page/clab/api/domain/library/book/application/port/out/RetrieveBookPort.java @@ -6,7 +6,7 @@ public interface RetrieveBookPort { - Book findByIdOrThrow(Long bookId); + Book getById(Long bookId); Page findByConditions(String title, String category, String publisher, String borrowerId, String borrowerName, Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/library/book/application/service/BookDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/library/book/application/service/BookDetailsRetrievalService.java index 316ca868c..03856920b 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/service/BookDetailsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/library/book/application/service/BookDetailsRetrievalService.java @@ -4,11 +4,11 @@ import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.library.book.application.dto.mapper.BookDtoMapper; import page.clab.api.domain.library.book.application.dto.response.BookDetailsResponseDto; import page.clab.api.domain.library.book.application.port.in.RetrieveBookDetailsUseCase; import page.clab.api.domain.library.book.application.port.out.RetrieveBookPort; import page.clab.api.domain.library.book.domain.Book; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; import page.clab.api.external.library.bookLoanRecord.application.port.ExternalRetrieveBookLoanRecordUseCase; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; @@ -21,11 +21,12 @@ public class BookDetailsRetrievalService implements RetrieveBookDetailsUseCase { private final RetrieveBookPort retrieveBookPort; private final ExternalRetrieveBookLoanRecordUseCase externalRetrieveBookLoanRecordUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BookDtoMapper mapper; @Transactional(readOnly = true) @Override public BookDetailsResponseDto retrieveBookDetails(Long bookId) { - Book book = retrieveBookPort.findByIdOrThrow(bookId); + Book book = retrieveBookPort.getById(bookId); String borrowerName = getBorrowerName(book); return mapToBookDetailsResponseDto(book, borrowerName); } @@ -41,6 +42,6 @@ private String getBorrowerName(Book book) { @NotNull private BookDetailsResponseDto mapToBookDetailsResponseDto(Book book, String borrowerName) { LocalDateTime dueDate = externalRetrieveBookLoanRecordUseCase.getDueDateForBook(book.getId()); - return BookDetailsResponseDto.toDto(book, borrowerName, dueDate); + return mapper.toDetailsDto(book, borrowerName, dueDate); } } \ No newline at end of file diff --git a/src/main/java/page/clab/api/domain/library/book/application/service/BookRegisterService.java b/src/main/java/page/clab/api/domain/library/book/application/service/BookRegisterService.java index 40ab994c2..0d2719b87 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/service/BookRegisterService.java +++ b/src/main/java/page/clab/api/domain/library/book/application/service/BookRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.library.book.application.dto.mapper.BookDtoMapper; import page.clab.api.domain.library.book.application.dto.request.BookRequestDto; import page.clab.api.domain.library.book.application.port.in.RegisterBookUseCase; import page.clab.api.domain.library.book.application.port.out.RegisterBookPort; @@ -13,11 +14,12 @@ public class BookRegisterService implements RegisterBookUseCase { private final RegisterBookPort registerBookPort; + private final BookDtoMapper mapper; @Transactional @Override public Long registerBook(BookRequestDto requestDto) { - Book book = BookRequestDto.toEntity(requestDto); + Book book = mapper.fromDto(requestDto); return registerBookPort.save(book).getId(); } } diff --git a/src/main/java/page/clab/api/domain/library/book/application/service/BookRemoveService.java b/src/main/java/page/clab/api/domain/library/book/application/service/BookRemoveService.java index ec495608c..51cbc5863 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/service/BookRemoveService.java +++ b/src/main/java/page/clab/api/domain/library/book/application/service/BookRemoveService.java @@ -18,7 +18,7 @@ public class BookRemoveService implements RemoveBookUseCase { @Transactional @Override public Long removeBook(Long bookId) { - Book book = retrieveBookPort.findByIdOrThrow(bookId); + Book book = retrieveBookPort.getById(bookId); book.delete(); return registerBookPort.save(book).getId(); } diff --git a/src/main/java/page/clab/api/domain/library/book/application/service/BookUpdateService.java b/src/main/java/page/clab/api/domain/library/book/application/service/BookUpdateService.java index 61e3e1e42..0df9b23fe 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/service/BookUpdateService.java +++ b/src/main/java/page/clab/api/domain/library/book/application/service/BookUpdateService.java @@ -19,7 +19,7 @@ public class BookUpdateService implements UpdateBookUseCase { @Transactional @Override public Long updateBookInfo(Long bookId, BookUpdateRequestDto requestDto) { - Book book = retrieveBookPort.findByIdOrThrow(bookId); + Book book = retrieveBookPort.getById(bookId); book.update(requestDto); return registerBookPort.save(book).getId(); } diff --git a/src/main/java/page/clab/api/domain/library/book/application/service/BooksByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/library/book/application/service/BooksByConditionsRetrievalService.java index 31cef51b2..6f9884bdc 100644 --- a/src/main/java/page/clab/api/domain/library/book/application/service/BooksByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/library/book/application/service/BooksByConditionsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.library.book.application.dto.mapper.BookDtoMapper; import page.clab.api.domain.library.book.application.dto.response.BookResponseDto; import page.clab.api.domain.library.book.application.port.in.RetrieveBooksByConditionsUseCase; import page.clab.api.domain.library.book.application.port.out.RetrieveBookPort; @@ -23,6 +24,7 @@ public class BooksByConditionsRetrievalService implements RetrieveBooksByConditi private final RetrieveBookPort retrieveBookPort; private final ExternalRetrieveBookLoanRecordUseCase externalRetrieveBookLoanRecordUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BookDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -35,7 +37,7 @@ public PagedResponseDto retrieveBooks(String title, String cate private BookResponseDto mapToBookResponseDto(Book book) { LocalDateTime dueDate = externalRetrieveBookLoanRecordUseCase.getDueDateForBook(book.getId()); String borrowerName = getBorrowerName(book); - return BookResponseDto.toDto(book, borrowerName, dueDate); + return mapper.toDto(book, borrowerName, dueDate); } private String getBorrowerName(Book book) { diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordMapper.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordMapper.java index 283597a3d..049601418 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordMapper.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface BookLoanRecordMapper { - BookLoanRecordJpaEntity toJpaEntity(BookLoanRecord bookLoanRecord); + BookLoanRecordJpaEntity toEntity(BookLoanRecord bookLoanRecord); BookLoanRecord toDomain(BookLoanRecordJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordPersistenceAdapter.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordPersistenceAdapter.java index 9843d5129..8ad059b80 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/adapter/out/persistence/BookLoanRecordPersistenceAdapter.java @@ -25,13 +25,13 @@ public class BookLoanRecordPersistenceAdapter implements @Override public BookLoanRecord save(BookLoanRecord bookLoanRecord) { - BookLoanRecordJpaEntity entity = bookLoanRecordMapper.toJpaEntity(bookLoanRecord); + BookLoanRecordJpaEntity entity = bookLoanRecordMapper.toEntity(bookLoanRecord); BookLoanRecordJpaEntity savedEntity = bookLoanRecordRepository.save(entity); return bookLoanRecordMapper.toDomain(savedEntity); } @Override - public BookLoanRecord findByIdOrThrow(Long bookLoanRecordId) { + public BookLoanRecord getById(Long bookLoanRecordId) { return bookLoanRecordRepository.findById(bookLoanRecordId) .map(bookLoanRecordMapper::toDomain) .orElseThrow(() -> new NotFoundException("[BookLoanRecord] id: " + bookLoanRecordId + "에 해당하는 대출 기록이 존재하지 않습니다.")); @@ -54,7 +54,7 @@ public Optional findByBookIdAndReturnedAtIsNullAndStatus(Long bo } @Override - public BookLoanRecord findByBookIdAndReturnedAtIsNullAndStatusOrThrow(Long bookId, BookLoanStatus bookLoanStatus) { + public BookLoanRecord getByBookIdAndReturnedAtIsNullAndStatus(Long bookId, BookLoanStatus bookLoanStatus) { return bookLoanRecordRepository.findByBookIdAndReturnedAtIsNullAndStatus(bookId, bookLoanStatus) .map(bookLoanRecordMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Book] id: " + bookId + "에 해당하는 대출 기록이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/port/out/RetrieveBookLoanRecordPort.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/port/out/RetrieveBookLoanRecordPort.java index c1497a9d4..dcbbe6fbb 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/port/out/RetrieveBookLoanRecordPort.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/port/out/RetrieveBookLoanRecordPort.java @@ -11,7 +11,7 @@ public interface RetrieveBookLoanRecordPort { - BookLoanRecord findByIdOrThrow(Long bookLoanRecordId); + BookLoanRecord getById(Long bookLoanRecordId); Page findByConditions(Long bookId, String borrowerId, BookLoanStatus status, Pageable pageable); @@ -19,7 +19,7 @@ public interface RetrieveBookLoanRecordPort { Optional findByBookIdAndReturnedAtIsNullAndStatus(Long bookId, BookLoanStatus bookLoanStatus); - BookLoanRecord findByBookIdAndReturnedAtIsNullAndStatusOrThrow(Long bookId, BookLoanStatus bookLoanStatus); + BookLoanRecord getByBookIdAndReturnedAtIsNullAndStatus(Long bookId, BookLoanStatus bookLoanStatus); Optional findByBookIdAndBorrowerIdAndStatus(Long bookId, String borrowerId, BookLoanStatus bookLoanStatus); } diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanApprovalService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanApprovalService.java index 76bb6a00c..bc373105a 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanApprovalService.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanApprovalService.java @@ -21,12 +21,22 @@ public class BookLoanApprovalService implements ApproveBookLoanUseCase { private final ExternalRetrieveBookUseCase externalRetrieveBookUseCase; private final ExternalRegisterBookUseCase externalRegisterBookUseCase; + /** + * 도서 대출을 승인합니다. + * + *

대출 기록을 조회하고, 도서와 대출자의 상태를 검증합니다. + * 도서가 이미 대출 중인지 확인하고, 대출자의 대출 한도를 검증합니다. + * 승인된 대출 기록과 도서 정보를 저장한 후 대출 기록 ID를 반환합니다.

+ * + * @param bookLoanRecordId 대출 기록의 ID + * @return 승인된 대출 기록의 ID + */ @Transactional @Override public Long approveBookLoan(Long bookLoanRecordId) { - BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.findByIdOrThrow(bookLoanRecordId); + BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.getById(bookLoanRecordId); String borrowerId = bookLoanRecord.getBorrowerId(); - Book book = externalRetrieveBookUseCase.findByIdOrThrow(bookLoanRecord.getBookId()); + Book book = externalRetrieveBookUseCase.getById(bookLoanRecord.getBookId()); book.validateBookIsNotBorrowed(); validateBorrowLimit(borrowerId); diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanExtensionService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanExtensionService.java index adfe7fe8b..28fa23137 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanExtensionService.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanExtensionService.java @@ -25,15 +25,26 @@ public class BookLoanExtensionService implements ExtendBookLoanUseCase { private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + /** + * 도서 대출을 연장합니다. + * + *

현재 로그인한 멤버의 정보를 기반으로 대출 기록을 조회하고, + * 해당 사용자가 도서의 현재 대출자인지 검증합니다. + * 대출을 연장한 후 사용자에게 알림을 전송하고, + * 연장된 대출 기록을 저장합니다.

+ * + * @param requestDto 도서 대출 연장 요청 DTO + * @return 연장된 대출 기록의 ID + */ @Transactional @Override public Long extendBookLoan(BookLoanRecordRequestDto requestDto) { MemberBorrowerInfoDto borrowerInfo = externalRetrieveMemberUseCase.getCurrentMemberBorrowerInfo(); String currentMemberId = borrowerInfo.getMemberId(); - Book book = externalRetrieveBookUseCase.findByIdOrThrow(requestDto.getBookId()); + Book book = externalRetrieveBookUseCase.getById(requestDto.getBookId()); book.validateCurrentBorrower(currentMemberId); - BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.findByBookIdAndReturnedAtIsNullAndStatusOrThrow(book.getId(), BookLoanStatus.APPROVED); + BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.getByBookIdAndReturnedAtIsNullAndStatus(book.getId(), BookLoanStatus.APPROVED); bookLoanRecord.extendLoan(borrowerInfo); externalSendNotificationUseCase.sendNotificationToMember(currentMemberId, "[" + book.getTitle() + "] 도서 대출 연장이 완료되었습니다."); diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRejectionService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRejectionService.java index 845763dc4..adc192b80 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRejectionService.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRejectionService.java @@ -18,7 +18,7 @@ public class BookLoanRejectionService implements RejectBookLoanUseCase { @Transactional @Override public Long rejectBookLoan(Long bookLoanRecordId) { - BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.findByIdOrThrow(bookLoanRecordId); + BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.getById(bookLoanRecordId); bookLoanRecord.reject(); return registerBookLoanRecordPort.save(bookLoanRecord).getId(); } diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java index a210a4384..9f32d5adb 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java @@ -1,6 +1,7 @@ package page.clab.api.domain.library.bookLoanRecord.application.service; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,8 +18,9 @@ import page.clab.api.external.library.book.application.port.ExternalRetrieveBookUseCase; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SlackBookLoanRecordInfo; +import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType; import page.clab.api.global.exception.CustomOptimisticLockingFailureException; @Service @@ -30,8 +32,20 @@ public class BookLoanRequestService implements RequestBookLoanUseCase { private final ExternalRetrieveBookUseCase externalRetrieveBookUseCase; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + /** + * 도서 대출 신청을 처리합니다. + * + *

현재 로그인한 멤버의 대출 상태와 한도를 검증한 후, + * 도서의 대출 신청이 이미 존재하는지 확인합니다. 대출 신청이 성공적으로 완료되면 멤버와 Slack에 알림을 전송하고, 대출 기록을 저장한 후 그 ID를 반환합니다.

+ * + * @param requestDto 도서 대출 신청 요청 정보 DTO + * @return 저장된 대출 기록의 ID + * @throws CustomOptimisticLockingFailureException 동시에 다른 사용자가 대출을 신청하여 충돌이 발생한 경우 예외 발생 + * @throws MaxBorrowLimitExceededException 대출 한도를 초과한 경우 예외 발생 + * @throws BookAlreadyAppliedForLoanException 이미 신청된 도서일 경우 예외 발생 + */ @Transactional @Override public Long requestBookLoan(BookLoanRecordRequestDto requestDto) throws CustomOptimisticLockingFailureException { @@ -41,15 +55,18 @@ public Long requestBookLoan(BookLoanRecordRequestDto requestDto) throws CustomOp borrowerInfo.checkLoanSuspension(); validateBorrowLimit(borrowerInfo.getMemberId()); - Book book = externalRetrieveBookUseCase.findByIdOrThrow(requestDto.getBookId()); + Book book = externalRetrieveBookUseCase.getById(requestDto.getBookId()); checkIfLoanAlreadyApplied(book.getId(), borrowerInfo.getMemberId()); BookLoanRecord bookLoanRecord = BookLoanRecord.create(book.getId(), borrowerInfo); - externalSendNotificationUseCase.sendNotificationToMember(borrowerInfo.getMemberId(), "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다."); + externalSendNotificationUseCase.sendNotificationToMember(borrowerInfo.getMemberId(), + "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다."); - SlackBookLoanRecordInfo bookLoanRecordInfo = SlackBookLoanRecordInfo.create(book, borrowerInfo); - slackService.sendNewBookLoanRequestNotification(bookLoanRecordInfo); + BookLoanRecordNotificationInfo bookLoanRecordInfo = BookLoanRecordNotificationInfo.create(book, + borrowerInfo); + eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_BOOK_LOAN_REQUEST, null, + bookLoanRecordInfo)); return registerBookLoanRecordPort.save(bookLoanRecord).getId(); } catch (ObjectOptimisticLockingFailureException e) { diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookReturnService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookReturnService.java index 539e22640..ca35e647f 100644 --- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookReturnService.java +++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookReturnService.java @@ -29,16 +29,27 @@ public class BookReturnService implements ReturnBookUseCase { private final ExternalUpdateMemberUseCase externalUpdateMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; + /** + * 도서 반납을 처리합니다. + * + *

현재 로그인한 멤버가 대출한 도서를 반납 처리하고, + * 대출 기록을 "RETURNED"로 표시합니다. + * 반납 이후 회원의 대출 정지 날짜를 업데이트하고, + * 반납 완료 알림을 사용자에게 전송합니다.

+ * + * @param requestDto 도서 반납 요청 정보 DTO + * @return 반납된 대출 기록의 ID + */ @Transactional @Override public Long returnBook(BookLoanRecordRequestDto requestDto) { MemberBorrowerInfoDto borrowerInfo = externalRetrieveMemberUseCase.getCurrentMemberBorrowerInfo(); String currentMemberId = borrowerInfo.getMemberId(); - Book book = externalRetrieveBookUseCase.findByIdOrThrow(requestDto.getBookId()); + Book book = externalRetrieveBookUseCase.getById(requestDto.getBookId()); book.returnBook(currentMemberId); externalRegisterBookUseCase.save(book); - BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.findByBookIdAndReturnedAtIsNullAndStatusOrThrow(book.getId(), BookLoanStatus.APPROVED); + BookLoanRecord bookLoanRecord = retrieveBookLoanRecordPort.getByBookIdAndReturnedAtIsNullAndStatus(book.getId(), BookLoanStatus.APPROVED); bookLoanRecord.markAsReturned(borrowerInfo); externalUpdateMemberUseCase.updateLoanSuspensionDate(borrowerInfo.getMemberId(), borrowerInfo.getLoanSuspensionDate()); diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardMapper.java b/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardMapper.java index 69e6b2b7b..a13d1a806 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardMapper.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface AwardMapper { - AwardJpaEntity toJpaEntity(Award award); + AwardJpaEntity toEntity(Award award); Award toDomain(AwardJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardPersistenceAdapter.java b/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardPersistenceAdapter.java index 0390c1d99..bb3b9acf9 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/adapter/out/persistence/AwardPersistenceAdapter.java @@ -24,7 +24,7 @@ public class AwardPersistenceAdapter implements @Override public Award save(Award award) { - AwardJpaEntity entity = awardMapper.toJpaEntity(award); + AwardJpaEntity entity = awardMapper.toEntity(award); AwardJpaEntity savedEntity = awardRepository.save(entity); return awardMapper.toDomain(savedEntity); } @@ -32,19 +32,19 @@ public Award save(Award award) { @Override public void saveAll(List awards) { List entities = awards.stream() - .map(awardMapper::toJpaEntity) + .map(awardMapper::toEntity) .toList(); awardRepository.saveAll(entities); } @Override public void delete(Award award) { - AwardJpaEntity entity = awardMapper.toJpaEntity(award); + AwardJpaEntity entity = awardMapper.toEntity(award); awardRepository.delete(entity); } @Override - public Award findByIdOrThrow(Long awardId) { + public Award getById(Long awardId) { return awardRepository.findById(awardId) .map(awardMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Award] id: " + awardId + "에 해당하는 수상 이력이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/mapper/AwardDtoMapper.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/mapper/AwardDtoMapper.java new file mode 100644 index 000000000..aa9630637 --- /dev/null +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/mapper/AwardDtoMapper.java @@ -0,0 +1,31 @@ +package page.clab.api.domain.memberManagement.award.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.award.application.dto.request.AwardRequestDto; +import page.clab.api.domain.memberManagement.award.application.dto.response.AwardResponseDto; +import page.clab.api.domain.memberManagement.award.domain.Award; + +@Component +public class AwardDtoMapper { + + public Award fromDto(AwardRequestDto requestDto, String memberId) { + return Award.builder() + .competitionName(requestDto.getCompetitionName()) + .organizer(requestDto.getOrganizer()) + .awardName(requestDto.getAwardName()) + .awardDate(requestDto.getAwardDate()) + .memberId(memberId) + .isDeleted(false) + .build(); + } + + public AwardResponseDto toDto(Award award) { + return AwardResponseDto.builder() + .id(award.getId()) + .competitionName(award.getCompetitionName()) + .organizer(award.getOrganizer()) + .awardName(award.getAwardName()) + .awardDate(award.getAwardDate()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/request/AwardRequestDto.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/request/AwardRequestDto.java index 0bb7b5bfe..6fbb69a0d 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/request/AwardRequestDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/request/AwardRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.memberManagement.award.domain.Award; import java.time.LocalDate; @@ -27,15 +26,4 @@ public class AwardRequestDto { @NotNull(message = "{notNull.award.awardDate}") @Schema(description = "수상일", example = "2023-08-18", required = true) private LocalDate awardDate; - - public static Award toEntity(AwardRequestDto requestDto, String memberId) { - return Award.builder() - .competitionName(requestDto.getCompetitionName()) - .organizer(requestDto.getOrganizer()) - .awardName(requestDto.getAwardName()) - .awardDate(requestDto.getAwardDate()) - .memberId(memberId) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/response/AwardResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/response/AwardResponseDto.java index 1743a61b5..8ffd49672 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/response/AwardResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/dto/response/AwardResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.award.domain.Award; import java.time.LocalDate; @@ -15,14 +14,4 @@ public class AwardResponseDto { private String organizer; private String awardName; private LocalDate awardDate; - - public static AwardResponseDto toDto(Award award) { - return AwardResponseDto.builder() - .id(award.getId()) - .competitionName(award.getCompetitionName()) - .organizer(award.getOrganizer()) - .awardName(award.getAwardName()) - .awardDate(award.getAwardDate()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/port/out/RetrieveAwardPort.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/port/out/RetrieveAwardPort.java index f95317873..e1f65a345 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/port/out/RetrieveAwardPort.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/port/out/RetrieveAwardPort.java @@ -8,7 +8,7 @@ public interface RetrieveAwardPort { - Award findByIdOrThrow(Long awardId); + Award getById(Long awardId); Page findByConditions(String memberId, Long year, Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRegisterService.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRegisterService.java index e9897cfac..5b7ab75a0 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRegisterService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.award.application.dto.mapper.AwardDtoMapper; import page.clab.api.domain.memberManagement.award.application.dto.request.AwardRequestDto; import page.clab.api.domain.memberManagement.award.application.port.in.RegisterAwardUseCase; import page.clab.api.domain.memberManagement.award.application.port.out.RegisterAwardPort; @@ -15,12 +16,13 @@ public class AwardRegisterService implements RegisterAwardUseCase { private final RegisterAwardPort registerAwardPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final AwardDtoMapper mapper; @Transactional @Override public Long registerAward(AwardRequestDto requestDto) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); - Award award = AwardRequestDto.toEntity(requestDto, currentMemberId); + Award award = mapper.fromDto(requestDto, currentMemberId); return registerAwardPort.save(award).getId(); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRemoveService.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRemoveService.java index b02b1eadf..8a5f6ca6a 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRemoveService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRemoveService.java @@ -23,7 +23,7 @@ public class AwardRemoveService implements RemoveAwardUseCase { @Override public Long removeAward(Long awardId) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Award award = retrieveAwardPort.findByIdOrThrow(awardId); + Award award = retrieveAwardPort.getById(awardId); award.validateAccessPermission(currentMemberInfo); award.delete(); return registerAwardPort.save(award).getId(); diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRetrievalService.java index 4228a5e16..aafbd27d2 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.award.application.dto.mapper.AwardDtoMapper; import page.clab.api.domain.memberManagement.award.application.dto.response.AwardResponseDto; import page.clab.api.domain.memberManagement.award.application.port.in.RetrieveAwardsUseCase; import page.clab.api.domain.memberManagement.award.application.port.out.RetrieveAwardPort; @@ -16,11 +17,12 @@ public class AwardRetrievalService implements RetrieveAwardsUseCase { private final RetrieveAwardPort retrieveAwardPort; + private final AwardDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveAwards(String memberId, Long year, Pageable pageable) { Page awards = retrieveAwardPort.findByConditions(memberId, year, pageable); - return new PagedResponseDto<>(awards.map(AwardResponseDto::toDto)); + return new PagedResponseDto<>(awards.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardUpdateService.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardUpdateService.java index a875e1734..74b86179e 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardUpdateService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/AwardUpdateService.java @@ -24,7 +24,7 @@ public class AwardUpdateService implements UpdateAwardUseCase { @Override public Long updateAward(Long awardId, AwardUpdateRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Award award = retrieveAwardPort.findByIdOrThrow(awardId); + Award award = retrieveAwardPort.getById(awardId); award.validateAccessPermission(currentMemberInfo); award.update(requestDto); return registerAwardPort.save(award).getId(); diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/DeletedAwardRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/DeletedAwardRetrievalService.java index f5b58ee2a..00bbdebfa 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/DeletedAwardRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/DeletedAwardRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.award.application.dto.mapper.AwardDtoMapper; import page.clab.api.domain.memberManagement.award.application.dto.response.AwardResponseDto; import page.clab.api.domain.memberManagement.award.application.port.in.RetrieveDeletedAwardsUseCase; import page.clab.api.domain.memberManagement.award.application.port.out.RetrieveAwardPort; @@ -16,11 +17,12 @@ public class DeletedAwardRetrievalService implements RetrieveDeletedAwardsUseCase { private final RetrieveAwardPort retrieveAwardPort; + private final AwardDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveDeletedAwards(Pageable pageable) { Page awards = retrieveAwardPort.findAllByIsDeletedTrue(pageable); - return new PagedResponseDto<>(awards.map(AwardResponseDto::toDto)); + return new PagedResponseDto<>(awards.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/MyAwardRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/MyAwardRetrievalService.java index 06d26eab2..b66b4f4ce 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/award/application/service/MyAwardRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/award/application/service/MyAwardRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.award.application.dto.mapper.AwardDtoMapper; import page.clab.api.domain.memberManagement.award.application.dto.response.AwardResponseDto; import page.clab.api.domain.memberManagement.award.application.port.in.RetrieveMyAwardsUseCase; import page.clab.api.domain.memberManagement.award.application.port.out.RetrieveAwardPort; @@ -18,12 +19,13 @@ public class MyAwardRetrievalService implements RetrieveMyAwardsUseCase { private final RetrieveAwardPort retrieveAwardPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final AwardDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveMyAwards(Pageable pageable) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); Page awards = retrieveAwardPort.findByMemberId(currentMemberId, pageable); - return new PagedResponseDto<>(awards.map(AwardResponseDto::toDto)); + return new PagedResponseDto<>(awards.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/mapper/CloudDtoMapper.java b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/mapper/CloudDtoMapper.java new file mode 100644 index 000000000..4b38181a7 --- /dev/null +++ b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/mapper/CloudDtoMapper.java @@ -0,0 +1,15 @@ +package page.clab.api.domain.memberManagement.cloud.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.cloud.application.dto.response.CloudUsageInfo; + +@Component +public class CloudDtoMapper { + + public CloudUsageInfo of(String memberId, Long usage) { + return CloudUsageInfo.builder() + .memberId(memberId) + .usage(usage) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/response/CloudUsageInfo.java b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/response/CloudUsageInfo.java index 61767ff2a..1c85f3688 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/response/CloudUsageInfo.java +++ b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/dto/response/CloudUsageInfo.java @@ -9,11 +9,4 @@ public class CloudUsageInfo { private String memberId; private Long usage; - - public static CloudUsageInfo create(String memberId, Long usage) { - return CloudUsageInfo.builder() - .memberId(memberId) - .usage(usage) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalAllService.java b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalAllService.java index 6ce28bbd7..3c5ca2e36 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalAllService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalAllService.java @@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.cloud.application.dto.mapper.CloudDtoMapper; import page.clab.api.domain.memberManagement.cloud.application.dto.response.CloudUsageInfo; import page.clab.api.domain.memberManagement.cloud.application.port.in.RetrieveAllCloudUsageUseCase; import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort; @@ -20,6 +21,7 @@ public class CloudUsageRetrievalAllService implements RetrieveAllCloudUsageUseCase { private final RetrieveMemberPort retrieveMemberPort; + private final CloudDtoMapper mapper; @Value("${resource.file.path}") private String filePath; @@ -34,7 +36,7 @@ public PagedResponseDto retrieveAllCloudUsages(Pageable pageable private CloudUsageInfo getCloudUsageForMember(Member member) { File directory = getMemberDirectory(member.getId()); long usage = FileSystemUtil.calculateDirectorySize(directory); - return CloudUsageInfo.create(member.getId(), usage); + return mapper.of(member.getId(), usage); } private File getMemberDirectory(String memberId) { diff --git a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalByMemberIdService.java b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalByMemberIdService.java index b5a0dcf4b..c7722cc8a 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalByMemberIdService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/CloudUsageRetrievalByMemberIdService.java @@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.cloud.application.dto.mapper.CloudDtoMapper; import page.clab.api.domain.memberManagement.cloud.application.dto.response.CloudUsageInfo; import page.clab.api.domain.memberManagement.cloud.application.port.in.RetrieveCloudUsageByMemberIdUseCase; import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMemberUseCase; @@ -20,6 +21,7 @@ public class CloudUsageRetrievalByMemberIdService implements RetrieveCloudUsageB private final RetrieveMemberUseCase retrieveMemberUseCase; private final RetrieveMemberPort retrieveMemberPort; + private final CloudDtoMapper mapper; @Value("${resource.file.path}") private String filePath; @@ -28,11 +30,11 @@ public class CloudUsageRetrievalByMemberIdService implements RetrieveCloudUsageB @Transactional(readOnly = true) public CloudUsageInfo retrieveCloudUsage(String memberId) throws PermissionDeniedException { Member currentMember = retrieveMemberUseCase.getCurrentMember(); - Member targetMember = retrieveMemberPort.findByIdOrThrow(memberId); + Member targetMember = retrieveMemberPort.getById(memberId); targetMember.validateAccessPermissionForCloud(currentMember); File directory = getMemberDirectory(targetMember.getId()); long usage = FileSystemUtil.calculateDirectorySize(directory); - return CloudUsageInfo.create(targetMember.getId(), usage); + return mapper.of(targetMember.getId(), usage); } private File getMemberDirectory(String memberId) { diff --git a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/FilesInMemberDirectoryRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/FilesInMemberDirectoryRetrievalService.java index c8f7b73da..df985ce6d 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/FilesInMemberDirectoryRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/cloud/application/service/FilesInMemberDirectoryRetrievalService.java @@ -9,6 +9,7 @@ import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort; import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.global.common.dto.PagedResponseDto; +import page.clab.api.global.common.file.dto.mapper.FileDtoMapper; import page.clab.api.global.common.file.dto.response.FileInfo; import page.clab.api.global.util.FileSystemUtil; @@ -20,6 +21,7 @@ public class FilesInMemberDirectoryRetrievalService implements RetrieveFilesInMemberDirectoryUseCase { private final RetrieveMemberPort retrieveMemberPort; + private final FileDtoMapper mapper; @Value("${resource.file.path}") private String filePath; @@ -27,10 +29,10 @@ public class FilesInMemberDirectoryRetrievalService implements RetrieveFilesInMe @Override @Transactional(readOnly = true) public PagedResponseDto retrieveFilesInMemberDirectory(String memberId, Pageable pageable) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + Member member = retrieveMemberPort.getById(memberId); File directory = getMemberDirectory(member.getId()); List files = FileSystemUtil.getFilesInDirectory(directory); - return new PagedResponseDto<>(files.stream().map(FileInfo::toDto).toList(), pageable, files.size()); + return new PagedResponseDto<>(files.stream().map(mapper::create).toList(), pageable, files.size()); } private File getMemberDirectory(String memberId) { diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberMapper.java b/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberMapper.java index c02f163c5..2415d72c7 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberMapper.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface MemberMapper { - MemberJpaEntity toJpaEntity(Member member); + MemberJpaEntity toEntity(Member member); - Member toDomainEntity(MemberJpaEntity jpaEntity); + Member toDomain(MemberJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberPersistenceAdapter.java b/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberPersistenceAdapter.java index a75a447ec..ba503140f 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/adapter/out/persistence/MemberPersistenceAdapter.java @@ -43,72 +43,72 @@ public boolean existsByEmail(String email) { @Override public Member save(Member member) { - MemberJpaEntity jpaEntity = memberMapper.toJpaEntity(member); + MemberJpaEntity jpaEntity = memberMapper.toEntity(member); MemberJpaEntity savedEntity = memberRepository.save(jpaEntity); - return memberMapper.toDomainEntity(savedEntity); + return memberMapper.toDomain(savedEntity); } @Override public Member update(Member member) { - MemberJpaEntity jpaEntity = memberMapper.toJpaEntity(member); + MemberJpaEntity jpaEntity = memberMapper.toEntity(member); MemberJpaEntity updatedEntity = memberRepository.save(jpaEntity); - return memberMapper.toDomainEntity(updatedEntity); + return memberMapper.toDomain(updatedEntity); } @Override public Page findByConditions(String id, String name, Pageable pageable) { Page jpaEntities = memberRepository.findByConditions(id, name, pageable); - return jpaEntities.map(memberMapper::toDomainEntity); + return jpaEntities.map(memberMapper::toDomain); } @Override - public Member findFirstByRoleOrThrow(Role role) { + public Member getFirstByRole(Role role) { MemberJpaEntity jpaEntity = memberRepository.findFirstByRole(role) .orElseThrow(() -> new NotFoundException("[Member] role: " + role + "에 해당하는 회원이 존재하지 않습니다.")); - return memberMapper.toDomainEntity(jpaEntity); + return memberMapper.toDomain(jpaEntity); } @Override public Optional findById(String memberId) { - return memberRepository.findById(memberId).map(memberMapper::toDomainEntity); + return memberRepository.findById(memberId).map(memberMapper::toDomain); } @Override - public Member findByIdOrThrow(String memberId) { + public Member getById(String memberId) { MemberJpaEntity jpaEntity = memberRepository.findById(memberId) .orElseThrow(() -> new NotFoundException("[Member] id: " + memberId + "에 해당하는 회원이 존재하지 않습니다.")); - return memberMapper.toDomainEntity(jpaEntity); + return memberMapper.toDomain(jpaEntity); } @Override public List findAll() { List jpaEntities = memberRepository.findAll(); return jpaEntities.stream() - .map(memberMapper::toDomainEntity) + .map(memberMapper::toDomain) .toList(); } public Page findMemberRoleInfoByConditions(String memberId, String memberName, Role role, Pageable pageable) { Page jpaEntities = memberRepository.findMemberRoleInfoByConditions(memberId, memberName, role, pageable); - return jpaEntities.map(memberMapper::toDomainEntity); + return jpaEntities.map(memberMapper::toDomain); } @Override public Page findAllByOrderByCreatedAtDesc(Pageable pageable) { Page jpaEntities = memberRepository.findAllByOrderByCreatedAtDesc(pageable); - return jpaEntities.map(memberMapper::toDomainEntity); + return jpaEntities.map(memberMapper::toDomain); } @Override - public Member findByEmailOrThrow(String email) { + public Member getByEmail(String email) { MemberJpaEntity jpaEntity = memberRepository.findByEmail(email) .orElseThrow(() -> new NotFoundException("[Member] email: " + email + "을 사용하는 회원이 존재하지 않습니다.")); - return memberMapper.toDomainEntity(jpaEntity); + return memberMapper.toDomain(jpaEntity); } @Override public Page findBirthdaysThisMonth(int month, Pageable pageable) { Page jpaEntities = memberRepository.findBirthdaysThisMonth(month, pageable); - return jpaEntities.map(memberMapper::toDomainEntity); + return jpaEntities.map(memberMapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/mapper/MemberDtoMapper.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/mapper/MemberDtoMapper.java new file mode 100644 index 000000000..8607a36c5 --- /dev/null +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/mapper/MemberDtoMapper.java @@ -0,0 +1,160 @@ +package page.clab.api.domain.memberManagement.member.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.member.application.dto.request.MemberRequestDto; +import page.clab.api.domain.memberManagement.member.application.dto.response.MemberBirthdayResponseDto; +import page.clab.api.domain.memberManagement.member.application.dto.response.MemberResponseDto; +import page.clab.api.domain.memberManagement.member.application.dto.response.MemberRoleInfoResponseDto; +import page.clab.api.domain.memberManagement.member.application.dto.response.MyProfileResponseDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBorrowerInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberEmailInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberPositionInfoDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberReviewInfoDto; +import page.clab.api.domain.memberManagement.member.domain.Member; +import page.clab.api.domain.memberManagement.member.domain.Role; +import page.clab.api.global.common.domain.Contact; + +@Component +public class MemberDtoMapper { + + public Member fromDto(MemberRequestDto requestDto) { + return Member.builder() + .id(requestDto.getId()) + .password(requestDto.getPassword()) + .name(requestDto.getName()) + .contact(Contact.of(requestDto.getContact()).getValue()) + .email(requestDto.getEmail()) + .department(requestDto.getDepartment()) + .grade(requestDto.getGrade()) + .birth(requestDto.getBirth()) + .address(requestDto.getAddress()) + .interests(requestDto.getInterests()) + .githubUrl(requestDto.getGithubUrl()) + .studentStatus(requestDto.getStudentStatus()) + .imageUrl(requestDto.getImageUrl()) + .role(Role.USER) + .isOtpEnabled(false) + .isDeleted(false) + .build(); + } + + public MemberResponseDto toDto(Member member) { + return MemberResponseDto.builder() + .id(member.getId()) + .name(member.getName()) + .contact(member.getContact()) + .email(member.getEmail()) + .department(member.getDepartment()) + .grade(member.getGrade()) + .birth(member.getBirth()) + .address(member.getAddress()) + .interests(member.getInterests()) + .githubUrl(member.getGithubUrl()) + .studentStatus(member.getStudentStatus()) + .imageUrl(member.getImageUrl()) + .role(member.getRole()) + .lastLoginTime(member.getLastLoginTime()) + .loanSuspensionDate(member.getLoanSuspensionDate()) + .isOtpEnabled(member.getIsOtpEnabled()) + .createdAt(member.getCreatedAt()) + .build(); + } + + public MemberBirthdayResponseDto toBirthdayDto(Member member) { + return MemberBirthdayResponseDto.builder() + .id(member.getId()) + .name(member.getName()) + .birth(member.getBirth()) + .imageUrl(member.getImageUrl()) + .build(); + } + + public MemberRoleInfoResponseDto toRoleInfoDto(Member member) { + return MemberRoleInfoResponseDto.builder() + .id(member.getId()) + .name(member.getName()) + .role(member.getRole()) + .build(); + } + + public MyProfileResponseDto toMyProfileDto(Member member) { + return MyProfileResponseDto.builder() + .name(member.getName()) + .id(member.getId()) + .interests(member.getInterests()) + .contact(member.getContact()) + .email(member.getEmail()) + .address(member.getAddress()) + .githubUrl(member.getGithubUrl()) + .studentStatus(member.getStudentStatus()) + .imageUrl(member.getImageUrl()) + .roleLevel(member.getRole().toRoleLevel()) + .isOtpEnabled(member.getIsOtpEnabled()) + .createdAt(member.getCreatedAt()) + .build(); + } + + + public MemberReviewInfoDto toReviewInfoDto(Member member) { + return MemberReviewInfoDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .department(member.getDepartment()) + .build(); + } + + public MemberPositionInfoDto toPositionInfoDto(Member member) { + return MemberPositionInfoDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .email(member.getEmail()) + .imageUrl(member.getImageUrl()) + .interests(member.getInterests()) + .githubUrl(member.getGithubUrl()) + .build(); + } + + public MemberLoginInfoDto toLoginInfoDto(Member member) { + return MemberLoginInfoDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .role(member.getRole()) + .isOtpEnabled(member.getIsOtpEnabled()) + .build(); + } + + public MemberEmailInfoDto toEmailInfoDto(Member member) { + return MemberEmailInfoDto.builder() + .memberName(member.getName()) + .email(member.getEmail()) + .build(); + } + + public MemberDetailedInfoDto toDetailedInfoDto(Member member) { + return MemberDetailedInfoDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .roleLevel(member.getRole().toRoleLevel()) + .imageUrl(member.getImageUrl()) + .isGraduated(member.isGraduated()) + .build(); + } + + public MemberBorrowerInfoDto toBorrowerInfoDto(Member member) { + return MemberBorrowerInfoDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .loanSuspensionDate(member.getLoanSuspensionDate()) + .build(); + } + + public MemberBasicInfoDto toBasicInfoDto(Member member) { + return MemberBasicInfoDto.builder() + .memberId(member.getId()) + .memberName(member.getName()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/request/MemberRequestDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/request/MemberRequestDto.java index 4041daf81..fba2527f7 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/request/MemberRequestDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/request/MemberRequestDto.java @@ -4,10 +4,7 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.memberManagement.member.domain.Member; -import page.clab.api.domain.memberManagement.member.domain.Role; import page.clab.api.domain.memberManagement.member.domain.StudentStatus; -import page.clab.api.global.common.domain.Contact; import java.time.LocalDate; @@ -63,25 +60,4 @@ public class MemberRequestDto { @Schema(description = "프로필 이미지", example = "https://www.clab.page/assets/dongmin-860f3a1e.jpeg") private String imageUrl; - - public static Member toEntity(MemberRequestDto requestDto) { - return Member.builder() - .id(requestDto.getId()) - .password(requestDto.getPassword()) - .name(requestDto.getName()) - .contact(Contact.of(requestDto.getContact()).getValue()) - .email(requestDto.getEmail()) - .department(requestDto.getDepartment()) - .grade(requestDto.getGrade()) - .birth(requestDto.getBirth()) - .address(requestDto.getAddress()) - .interests(requestDto.getInterests()) - .githubUrl(requestDto.getGithubUrl()) - .studentStatus(requestDto.getStudentStatus()) - .imageUrl(requestDto.getImageUrl()) - .role(Role.USER) - .isOtpEnabled(false) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberBirthdayResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberBirthdayResponseDto.java index 450a1e60a..16931edef 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberBirthdayResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberBirthdayResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; import java.time.LocalDate; @@ -14,13 +13,4 @@ public class MemberBirthdayResponseDto { private String name; private LocalDate birth; private String imageUrl; - - public static MemberBirthdayResponseDto toDto(Member member) { - return MemberBirthdayResponseDto.builder() - .id(member.getId()) - .name(member.getName()) - .birth(member.getBirth()) - .imageUrl(member.getImageUrl()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberResponseDto.java index 4306fa38b..74aab3140 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.domain.memberManagement.member.domain.Role; import page.clab.api.domain.memberManagement.member.domain.StudentStatus; @@ -30,26 +29,4 @@ public class MemberResponseDto { private LocalDateTime loanSuspensionDate; private Boolean isOtpEnabled; private LocalDateTime createdAt; - - public static MemberResponseDto toDto(Member member) { - return MemberResponseDto.builder() - .id(member.getId()) - .name(member.getName()) - .contact(member.getContact()) - .email(member.getEmail()) - .department(member.getDepartment()) - .grade(member.getGrade()) - .birth(member.getBirth()) - .address(member.getAddress()) - .interests(member.getInterests()) - .githubUrl(member.getGithubUrl()) - .studentStatus(member.getStudentStatus()) - .imageUrl(member.getImageUrl()) - .role(member.getRole()) - .lastLoginTime(member.getLastLoginTime()) - .loanSuspensionDate(member.getLoanSuspensionDate()) - .isOtpEnabled(member.getIsOtpEnabled()) - .createdAt(member.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberRoleInfoResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberRoleInfoResponseDto.java index 35b2b3976..775059567 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberRoleInfoResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MemberRoleInfoResponseDto.java @@ -2,11 +2,8 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.domain.memberManagement.member.domain.Role; -import java.util.List; - @Getter @Builder public class MemberRoleInfoResponseDto { @@ -14,18 +11,4 @@ public class MemberRoleInfoResponseDto { private String id; private String name; private Role role; - - public static List toDto(List members) { - return members.stream() - .map(MemberRoleInfoResponseDto::toDto) - .toList(); - } - - public static MemberRoleInfoResponseDto toDto(Member member) { - return MemberRoleInfoResponseDto.builder() - .id(member.getId()) - .name(member.getName()) - .role(member.getRole()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MyProfileResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MyProfileResponseDto.java index 81898e36a..c083bdecd 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MyProfileResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/response/MyProfileResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.domain.memberManagement.member.domain.StudentStatus; import java.time.LocalDateTime; @@ -23,21 +22,4 @@ public class MyProfileResponseDto { private Long roleLevel; private Boolean isOtpEnabled; private LocalDateTime createdAt; - - public static MyProfileResponseDto toDto(Member member) { - return MyProfileResponseDto.builder() - .name(member.getName()) - .id(member.getId()) - .interests(member.getInterests()) - .contact(member.getContact()) - .email(member.getEmail()) - .address(member.getAddress()) - .githubUrl(member.getGithubUrl()) - .studentStatus(member.getStudentStatus()) - .imageUrl(member.getImageUrl()) - .roleLevel(member.getRole().toRoleLevel()) - .isOtpEnabled(member.getIsOtpEnabled()) - .createdAt(member.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBasicInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBasicInfoDto.java index 018ea21bc..4716fc1bd 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBasicInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBasicInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -10,11 +9,4 @@ public class MemberBasicInfoDto { private String memberId; private String memberName; - - public static MemberBasicInfoDto create(Member member) { - return MemberBasicInfoDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBorrowerInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBorrowerInfoDto.java index 8f3dc8d2a..852788a5d 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBorrowerInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberBorrowerInfoDto.java @@ -3,7 +3,6 @@ import lombok.Builder; import lombok.Getter; import page.clab.api.domain.library.bookLoanRecord.application.exception.LoanSuspensionException; -import page.clab.api.domain.memberManagement.member.domain.Member; import java.time.LocalDateTime; @@ -15,14 +14,6 @@ public class MemberBorrowerInfoDto { private String memberName; private LocalDateTime loanSuspensionDate; - public static MemberBorrowerInfoDto create(Member member) { - return MemberBorrowerInfoDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .loanSuspensionDate(member.getLoanSuspensionDate()) - .build(); - } - public void handleOverdueAndSuspension(long overdueDays) { if (overdueDays > 0) { LocalDateTime currentDate = LocalDateTime.now(); diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberDetailedInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberDetailedInfoDto.java index e061ec684..a9841554e 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberDetailedInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberDetailedInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -14,16 +13,6 @@ public class MemberDetailedInfoDto { private String imageUrl; private boolean isGraduated; - public static MemberDetailedInfoDto create(Member member) { - return MemberDetailedInfoDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .roleLevel(member.getRole().toRoleLevel()) - .imageUrl(member.getImageUrl()) - .isGraduated(member.isGraduated()) - .build(); - } - public boolean isAdminRole() { return roleLevel >= 2; } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberEmailInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberEmailInfoDto.java index 77f58fef9..a2af18ead 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberEmailInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberEmailInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -10,11 +9,4 @@ public class MemberEmailInfoDto { private String memberName; private String email; - - public static MemberEmailInfoDto create(Member member) { - return MemberEmailInfoDto.builder() - .memberName(member.getName()) - .email(member.getEmail()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberLoginInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberLoginInfoDto.java index dfed0f502..f565de91b 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberLoginInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberLoginInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.domain.memberManagement.member.domain.Role; @Getter @@ -14,15 +13,6 @@ public class MemberLoginInfoDto { private Role role; private boolean isOtpEnabled; - public static MemberLoginInfoDto create(Member member) { - return MemberLoginInfoDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .role(member.getRole()) - .isOtpEnabled(member.getIsOtpEnabled()) - .build(); - } - public boolean isAdminRole() { return role.toRoleLevel() >= 2; } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberPositionInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberPositionInfoDto.java index 2143209ca..3b9038be5 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberPositionInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberPositionInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -14,15 +13,4 @@ public class MemberPositionInfoDto { private String imageUrl; private String interests; private String githubUrl; - - public static MemberPositionInfoDto create(Member member) { - return MemberPositionInfoDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .email(member.getEmail()) - .imageUrl(member.getImageUrl()) - .interests(member.getInterests()) - .githubUrl(member.getGithubUrl()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberReviewInfoDto.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberReviewInfoDto.java index 2cdf1995a..57315a774 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberReviewInfoDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/dto/shared/MemberReviewInfoDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.domain.Member; @Getter @Builder @@ -11,12 +10,4 @@ public class MemberReviewInfoDto { private String memberId; private String memberName; private String department; - - public static MemberReviewInfoDto create(Member member) { - return MemberReviewInfoDto.builder() - .memberId(member.getId()) - .memberName(member.getName()) - .department(member.getDepartment()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/port/out/RetrieveMemberPort.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/port/out/RetrieveMemberPort.java index 13092928d..83aec72ef 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/port/out/RetrieveMemberPort.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/port/out/RetrieveMemberPort.java @@ -12,7 +12,7 @@ public interface RetrieveMemberPort { Optional findById(String memberId); - Member findByIdOrThrow(String memberId); + Member getById(String memberId); List findAll(); @@ -20,11 +20,11 @@ public interface RetrieveMemberPort { Page findAllByOrderByCreatedAtDesc(Pageable pageable); - Member findByEmailOrThrow(String email); + Member getByEmail(String email); Page findBirthdaysThisMonth(int month, Pageable pageable); Page findByConditions(String id, String name, Pageable pageable); - Member findFirstByRoleOrThrow(Role role); + Member getFirstByRole(Role role); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberBirthdayRetrievalThisMonthService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberBirthdayRetrievalThisMonthService.java index aa9c556da..634aad00d 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberBirthdayRetrievalThisMonthService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberBirthdayRetrievalThisMonthService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.member.application.dto.mapper.MemberDtoMapper; import page.clab.api.domain.memberManagement.member.application.dto.response.MemberBirthdayResponseDto; import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMemberBirthdaysThisMonthUseCase; import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort; @@ -16,11 +17,12 @@ public class MemberBirthdayRetrievalThisMonthService implements RetrieveMemberBirthdaysThisMonthUseCase { private final RetrieveMemberPort retrieveMemberPort; + private final MemberDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveBirthdaysThisMonth(int month, Pageable pageable) { Page birthdayMembers = retrieveMemberPort.findBirthdaysThisMonth(month, pageable); - return new PagedResponseDto<>(birthdayMembers.map(MemberBirthdayResponseDto::toDto)); + return new PagedResponseDto<>(birthdayMembers.map(mapper::toBirthdayDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberPasswordManagementService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberPasswordManagementService.java index e3250518c..130ccd99d 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberPasswordManagementService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberPasswordManagementService.java @@ -29,7 +29,7 @@ public class MemberPasswordManagementService implements ManageMemberPasswordUseC @Transactional @Override public String resendMemberPassword(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + Member member = retrieveMemberPort.getById(memberId); String newPassword = verificationService.generateVerificationCode(); member.updatePassword(newPassword, passwordEncoder); @@ -52,7 +52,7 @@ public String requestMemberPasswordReset(MemberResetPasswordRequestDto requestDt @Transactional @Override public String verifyMemberPasswordReset(VerificationRequestDto requestDto) { - Member member = retrieveMemberPort.findByIdOrThrow(requestDto.getMemberId()); + Member member = retrieveMemberPort.getById(requestDto.getMemberId()); Verification verification = verificationService.validateVerificationCode(requestDto, member); updateMemberPasswordWithVerificationCode(verification.getVerificationCode(), member); return registerMemberPort.save(member).getId(); @@ -66,7 +66,7 @@ public String generateOrRetrievePassword(String password) { } private Member validateResetPasswordRequest(MemberResetPasswordRequestDto requestDto) { - Member member = retrieveMemberPort.findByIdOrThrow(requestDto.getId()); + Member member = retrieveMemberPort.getById(requestDto.getId()); if (!member.isSameName(requestDto.getName()) || !member.isSameEmail(requestDto.getEmail())) { throw new InvalidInformationException("올바르지 않은 정보입니다."); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRegisterService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRegisterService.java index a227b6c1e..4d80b9ef7 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRegisterService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRegisterService.java @@ -4,6 +4,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.member.application.dto.mapper.MemberDtoMapper; import page.clab.api.domain.memberManagement.member.application.dto.request.MemberRequestDto; import page.clab.api.domain.memberManagement.member.application.exception.DuplicateMemberContactException; import page.clab.api.domain.memberManagement.member.application.exception.DuplicateMemberEmailException; @@ -32,12 +33,27 @@ public class MemberRegisterService implements RegisterMemberUseCase { private final ExternalRetrievePositionUseCase externalRetrievePositionUseCase; private final EmailService emailService; private final PasswordEncoder passwordEncoder; + private final MemberDtoMapper mapper; + /** + * 새 멤버를 등록합니다. + * + *

입력된 회원 정보를 검증하고, 중복되는 회원 정보(ID, 연락처, 이메일)가 없는지 확인합니다. + * 비밀번호가 입력되지 않은 경우 새 비밀번호를 생성하거나, 입력된 비밀번호를 사용합니다. + * 등록이 완료되면 기본 직책(Position)을 생성하며, 최종적으로 생성된 계정 정보를 + * 이메일을 통해 사용자에게 전송합니다.

+ * + * @param requestDto 회원 등록 요청 정보를 담은 DTO + * @return 생성된 멤버의 ID + * @throws DuplicateMemberIdException 중복된 아이디가 있을 경우 예외 발생 + * @throws DuplicateMemberContactException 중복된 연락처가 있을 경우 예외 발생 + * @throws DuplicateMemberEmailException 중복된 이메일이 있을 경우 예외 발생 + */ @Transactional @Override public String registerMember(MemberRequestDto requestDto) { checkMemberUniqueness(requestDto); - Member member = MemberRequestDto.toEntity(requestDto); + Member member = mapper.fromDto(requestDto); String finalPassword = manageMemberPasswordUseCase.generateOrRetrievePassword(requestDto.getPassword()); member.updatePassword(finalPassword, passwordEncoder); registerMemberPort.save(member); diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRemoveService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRemoveService.java index d8786d822..8ea48ceb9 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRemoveService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRemoveService.java @@ -21,7 +21,7 @@ public class MemberRemoveService implements RemoveMemberUseCase { @Transactional @Override public String removeMember(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + Member member = retrieveMemberPort.getById(memberId); member.delete(); registerMemberPort.save(member); eventPublisher.publishEvent(new MemberDeletedEvent(this, member.getId())); diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRetrievalService.java index 70134b44f..9b61660d3 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRetrievalService.java @@ -16,6 +16,6 @@ public class MemberRetrievalService implements RetrieveMemberUseCase { @Override public Member getCurrentMember() { String memberId = AuthUtil.getAuthenticationInfoMemberId(); - return retrieveMemberPort.findByIdOrThrow(memberId); + return retrieveMemberPort.getById(memberId); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleInfoRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleInfoRetrievalService.java index 145f0d3f1..dde2bb970 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleInfoRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleInfoRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.member.application.dto.mapper.MemberDtoMapper; import page.clab.api.domain.memberManagement.member.application.dto.response.MemberRoleInfoResponseDto; import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMemberRoleInfoUseCase; import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort; @@ -17,11 +18,12 @@ public class MemberRoleInfoRetrievalService implements RetrieveMemberRoleInfoUseCase { private final RetrieveMemberPort retrieveMemberPort; + private final MemberDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveMemberRoleInfo(String memberId, String memberName, Role role, Pageable pageable) { Page members = retrieveMemberPort.findMemberRoleInfoByConditions(memberId, memberName, role, pageable); - return new PagedResponseDto<>(members.map(MemberRoleInfoResponseDto::toDto)); + return new PagedResponseDto<>(members.map(mapper::toRoleInfoDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java index f73236acd..ceed5fb73 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java @@ -2,6 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.request.ChangeMemberRoleRequest; @@ -11,8 +12,8 @@ import page.clab.api.domain.memberManagement.member.application.port.out.UpdateMemberPort; import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.domain.memberManagement.member.domain.Role; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -20,12 +21,13 @@ public class MemberRoleManagementService implements ManageMemberRoleUseCase { private final RetrieveMemberPort retrieveMemberPort; private final UpdateMemberPort updateMemberPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; @Transactional @Override - public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId, ChangeMemberRoleRequest request) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId, + ChangeMemberRoleRequest request) { + Member member = retrieveMemberPort.getById(memberId); Role oldRole = member.getRole(); Role newRole = request.getRole(); @@ -34,9 +36,13 @@ public String changeMemberRole(HttpServletRequest httpServletRequest, String mem member.changeRole(newRole); updateMemberPort.update(member); - slackService.sendSecurityAlertNotification(httpServletRequest, SecurityAlertType.MEMBER_ROLE_CHANGED, - String.format("[%s] %s: %s -> %s", - member.getId(), member.getName(), oldRole, newRole)); + + String memberRoleChangedMessage = String.format("[%s] %s: %s -> %s", member.getId(), member.getName(), oldRole, + newRole); + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.MEMBER_ROLE_CHANGED, httpServletRequest, + memberRoleChangedMessage)); + return memberId; } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberUpdateService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberUpdateService.java index b4b46fe49..2f9a1009f 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberUpdateService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberUpdateService.java @@ -13,7 +13,7 @@ import page.clab.api.domain.memberManagement.member.application.port.out.UpdateMemberPort; import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.global.common.file.application.FileService; -import page.clab.api.global.common.file.dto.request.DeleteFileRequestDto; +import page.clab.api.global.common.file.dto.mapper.FileDtoMapper; import page.clab.api.global.exception.PermissionDeniedException; @Service @@ -26,12 +26,13 @@ public class MemberUpdateService implements UpdateMemberUseCase { private final FileService fileService; private final PasswordEncoder passwordEncoder; private final ApplicationEventPublisher eventPublisher; + private final FileDtoMapper mapper; @Transactional @Override public String updateMember(String memberId, MemberUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = retrieveMemberUseCase.getCurrentMember(); - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + Member member = retrieveMemberPort.getById(memberId); member.validateAccessPermission(currentMember); updateMember(requestDto, member); updateMemberPort.update(member); @@ -44,7 +45,7 @@ private void updateMember(MemberUpdateRequestDto requestDto, Member member) thro member.update(requestDto, passwordEncoder); if (requestDto.getImageUrl() != null && requestDto.getImageUrl().isEmpty()) { member.clearImageUrl(); - fileService.deleteFile(DeleteFileRequestDto.create(previousImageUrl)); + fileService.deleteFile(mapper.of(previousImageUrl)); } } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MembersByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MembersByConditionsRetrievalService.java index 82ef0ae76..9add7ec50 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MembersByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MembersByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.member.application.dto.mapper.MemberDtoMapper; import page.clab.api.domain.memberManagement.member.application.dto.response.MemberResponseDto; import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMembersByConditionsUseCase; import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort; @@ -16,11 +17,12 @@ public class MembersByConditionsRetrievalService implements RetrieveMembersByConditionsUseCase { private final RetrieveMemberPort retrieveMemberPort; + private final MemberDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveMembers(String id, String name, Pageable pageable) { Page members = retrieveMemberPort.findByConditions(id, name, pageable); - return new PagedResponseDto<>(members.map(MemberResponseDto::toDto)); + return new PagedResponseDto<>(members.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MyProfileRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MyProfileRetrievalService.java index a28fd6668..5e60852c0 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MyProfileRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MyProfileRetrievalService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.member.application.dto.mapper.MemberDtoMapper; import page.clab.api.domain.memberManagement.member.application.dto.response.MyProfileResponseDto; import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMemberUseCase; import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMyProfileUseCase; @@ -13,11 +14,12 @@ public class MyProfileRetrievalService implements RetrieveMyProfileUseCase { private final RetrieveMemberUseCase retrieveMemberUseCase; + private final MemberDtoMapper mapper; @Transactional(readOnly = true) @Override public MyProfileResponseDto retrieveMyProfile() { Member currentMember = retrieveMemberUseCase.getCurrentMember(); - return MyProfileResponseDto.toDto(currentMember); + return mapper.toMyProfileDto(currentMember); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationMapper.java b/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationMapper.java index 9d69e9d94..ddea92fa5 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationMapper.java +++ b/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface NotificationMapper { - NotificationJpaEntity toJpaEntity(Notification notification); + NotificationJpaEntity toEntity(Notification notification); - Notification toDomainEntity(NotificationJpaEntity jpaEntity); + Notification toDomain(NotificationJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationPersistenceAdapter.java b/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationPersistenceAdapter.java index e5bcd78aa..a3014cb6c 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/memberManagement/notification/adapter/out/persistence/NotificationPersistenceAdapter.java @@ -22,36 +22,36 @@ public class NotificationPersistenceAdapter implements @Override public Notification save(Notification notification) { - NotificationJpaEntity entity = mapper.toJpaEntity(notification); + NotificationJpaEntity entity = mapper.toEntity(notification); NotificationJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override public void saveAll(List notifications) { List entities = notifications.stream() - .map(mapper::toJpaEntity) + .map(mapper::toEntity) .toList(); repository.saveAll(entities); } @Override - public Notification findByIdOrThrow(Long id) { + public Notification getById(Long id) { return repository.findById(id) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[Notification] id: " + id + "에 해당하는 알림이 존재하지 않습니다.")); } @Override public List findByMemberId(String memberId) { return repository.findByMemberId(memberId).stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } @Override public Page findByMemberId(String memberId, Pageable pageable) { return repository.findByMemberId(memberId, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/mapper/NotificationDtoMapper.java b/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/mapper/NotificationDtoMapper.java new file mode 100644 index 000000000..61accf5da --- /dev/null +++ b/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/mapper/NotificationDtoMapper.java @@ -0,0 +1,17 @@ +package page.clab.api.domain.memberManagement.notification.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.notification.application.dto.response.NotificationResponseDto; +import page.clab.api.domain.memberManagement.notification.domain.Notification; + +@Component +public class NotificationDtoMapper { + + public NotificationResponseDto toDto(Notification notification) { + return NotificationResponseDto.builder() + .id(notification.getId()) + .content(notification.getContent()) + .createdAt(notification.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/response/NotificationResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/response/NotificationResponseDto.java index 4ff8aefae..dcbc680f8 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/response/NotificationResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/notification/application/dto/response/NotificationResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.notification.domain.Notification; import java.time.LocalDateTime; @@ -13,12 +12,4 @@ public class NotificationResponseDto { private Long id; private String content; private LocalDateTime createdAt; - - public static NotificationResponseDto toDto(Notification notification) { - return NotificationResponseDto.builder() - .id(notification.getId()) - .content(notification.getContent()) - .createdAt(notification.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/notification/application/port/out/RetrieveNotificationPort.java b/src/main/java/page/clab/api/domain/memberManagement/notification/application/port/out/RetrieveNotificationPort.java index 93647cb03..693b67252 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/notification/application/port/out/RetrieveNotificationPort.java +++ b/src/main/java/page/clab/api/domain/memberManagement/notification/application/port/out/RetrieveNotificationPort.java @@ -8,7 +8,7 @@ public interface RetrieveNotificationPort { - Notification findByIdOrThrow(Long id); + Notification getById(Long id); List findByMemberId(String memberId); diff --git a/src/main/java/page/clab/api/domain/memberManagement/notification/application/service/NotificationsRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/notification/application/service/NotificationsRetrievalService.java index 607042743..0fea2a83b 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/notification/application/service/NotificationsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/notification/application/service/NotificationsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.notification.application.dto.mapper.NotificationDtoMapper; import page.clab.api.domain.memberManagement.notification.application.dto.response.NotificationResponseDto; import page.clab.api.domain.memberManagement.notification.application.port.in.RetrieveNotificationsUseCase; import page.clab.api.domain.memberManagement.notification.application.port.out.RetrieveNotificationPort; @@ -18,12 +19,13 @@ public class NotificationsRetrievalService implements RetrieveNotificationsUseCa private final RetrieveNotificationPort retrieveNotificationPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final NotificationDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveNotifications(Pageable pageable) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); Page notifications = retrieveNotificationPort.findByMemberId(currentMemberId, pageable); - return new PagedResponseDto<>(notifications.map(NotificationResponseDto::toDto)); + return new PagedResponseDto<>(notifications.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionMapper.java b/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionMapper.java index 2d093e031..d3b308379 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionMapper.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface PositionMapper { - PositionJpaEntity toJpaEntity(Position position); + PositionJpaEntity toEntity(Position position); - Position toDomainEntity(PositionJpaEntity jpaEntity); + Position toDomain(PositionJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionPersistenceAdapter.java b/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionPersistenceAdapter.java index 7f7c46ff5..4d4c820d8 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/adapter/out/persistence/PositionPersistenceAdapter.java @@ -26,56 +26,56 @@ public class PositionPersistenceAdapter implements @Override public Position save(Position position) { - PositionJpaEntity entity = mapper.toJpaEntity(position); + PositionJpaEntity entity = mapper.toEntity(position); PositionJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override public void saveAll(List positions) { List entities = positions.stream() - .map(mapper::toJpaEntity) + .map(mapper::toEntity) .toList(); repository.saveAll(entities); } @Override public Position update(Position position) { - PositionJpaEntity entity = mapper.toJpaEntity(position); + PositionJpaEntity entity = mapper.toEntity(position); PositionJpaEntity updatedEntity = repository.save(entity); - return mapper.toDomainEntity(updatedEntity); + return mapper.toDomain(updatedEntity); } @Override - public Position findByIdOrThrow(Long id) { + public Position getById(Long id) { return repository.findById(id) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[Position] id: " + id + "에 해당하는 직책이 존재하지 않습니다.")); } @Override public Optional findByMemberIdAndYearAndPositionType(String memberId, String year, PositionType positionType) { return repository.findByMemberIdAndYearAndPositionType(memberId, year, positionType) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public List findAllByMemberIdAndYearOrderByPositionTypeAsc(String memberId, String year) { return repository.findAllByMemberIdAndYearOrderByPositionTypeAsc(memberId, year).stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } @Override public Page findByConditions(String year, PositionType positionType, Pageable pageable) { return repository.findByConditions(year, positionType, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public List findByMemberId(String memberId) { return repository.findByMemberId(memberId).stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/mapper/PositionDtoMapper.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/mapper/PositionDtoMapper.java new file mode 100644 index 000000000..b9b47805c --- /dev/null +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/mapper/PositionDtoMapper.java @@ -0,0 +1,55 @@ +package page.clab.api.domain.memberManagement.position.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberPositionInfoDto; +import page.clab.api.domain.memberManagement.position.application.dto.request.PositionRequestDto; +import page.clab.api.domain.memberManagement.position.application.dto.response.PositionMyResponseDto; +import page.clab.api.domain.memberManagement.position.application.dto.response.PositionResponseDto; +import page.clab.api.domain.memberManagement.position.domain.Position; +import page.clab.api.domain.memberManagement.position.domain.PositionType; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class PositionDtoMapper { + + public Position fromDto(PositionRequestDto positionRequestDto) { + return Position.builder() + .memberId(positionRequestDto.getMemberId()) + .positionType(positionRequestDto.getPositionType()) + .year(positionRequestDto.getYear()) + .isDeleted(false) + .build(); + } + + public PositionMyResponseDto toDto(List positions, MemberPositionInfoDto memberInfo) { + Map> positionTypesByYear = positions.stream() + .collect(Collectors.groupingBy( + Position::getYear, + Collectors.mapping(Position::getPositionType, Collectors.toList()) + )); + return PositionMyResponseDto.builder() + .name(memberInfo.getMemberName()) + .email(memberInfo.getEmail()) + .imageUrl(memberInfo.getImageUrl()) + .interests(memberInfo.getInterests()) + .githubUrl(memberInfo.getGithubUrl()) + .positionTypes(positionTypesByYear) + .build(); + } + + public PositionResponseDto toDto(Position position, MemberPositionInfoDto memberInfo) { + return PositionResponseDto.builder() + .id(position.getId()) + .name(memberInfo.getMemberName()) + .email(memberInfo.getEmail()) + .imageUrl(memberInfo.getImageUrl()) + .interests(memberInfo.getInterests()) + .githubUrl(memberInfo.getGithubUrl()) + .positionType(position.getPositionType()) + .year(position.getYear()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/request/PositionRequestDto.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/request/PositionRequestDto.java index 62812c417..b20f7aa18 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/request/PositionRequestDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/request/PositionRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.memberManagement.position.domain.Position; import page.clab.api.domain.memberManagement.position.domain.PositionType; @Getter @@ -22,13 +21,4 @@ public class PositionRequestDto { @NotNull(message = "{notNull.position.year}") @Schema(description = "연도", example = "2023", required = true) private String year; - - public static Position toEntity(PositionRequestDto positionRequestDto) { - return Position.builder() - .memberId(positionRequestDto.getMemberId()) - .positionType(positionRequestDto.getPositionType()) - .year(positionRequestDto.getYear()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionMyResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionMyResponseDto.java index f0a72e371..a7908ae61 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionMyResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionMyResponseDto.java @@ -2,13 +2,10 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberPositionInfoDto; -import page.clab.api.domain.memberManagement.position.domain.Position; import page.clab.api.domain.memberManagement.position.domain.PositionType; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Getter @Builder @@ -21,20 +18,4 @@ public class PositionMyResponseDto { private String interests; private String githubUrl; private Map> positionTypes; - - public static PositionMyResponseDto toDto(List positions, MemberPositionInfoDto memberInfo) { - Map> positionTypesByYear = positions.stream() - .collect(Collectors.groupingBy( - Position::getYear, - Collectors.mapping(Position::getPositionType, Collectors.toList()) - )); - return PositionMyResponseDto.builder() - .name(memberInfo.getMemberName()) - .email(memberInfo.getEmail()) - .imageUrl(memberInfo.getImageUrl()) - .interests(memberInfo.getInterests()) - .githubUrl(memberInfo.getGithubUrl()) - .positionTypes(positionTypesByYear) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionResponseDto.java index 95b82b175..0520477d6 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/dto/response/PositionResponseDto.java @@ -2,8 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberPositionInfoDto; -import page.clab.api.domain.memberManagement.position.domain.Position; import page.clab.api.domain.memberManagement.position.domain.PositionType; @Getter @@ -18,17 +16,4 @@ public class PositionResponseDto { private String githubUrl; private PositionType positionType; private String year; - - public static PositionResponseDto toDto(Position position, MemberPositionInfoDto memberInfo) { - return PositionResponseDto.builder() - .id(position.getId()) - .name(memberInfo.getMemberName()) - .email(memberInfo.getEmail()) - .imageUrl(memberInfo.getImageUrl()) - .interests(memberInfo.getInterests()) - .githubUrl(memberInfo.getGithubUrl()) - .positionType(position.getPositionType()) - .year(position.getYear()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/port/out/RetrievePositionPort.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/port/out/RetrievePositionPort.java index 6324023bb..4265c6388 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/port/out/RetrievePositionPort.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/port/out/RetrievePositionPort.java @@ -10,7 +10,7 @@ public interface RetrievePositionPort { - Position findByIdOrThrow(Long id); + Position getById(Long id); List findAllByMemberIdAndYearOrderByPositionTypeAsc(String memberId, String year); diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/MyPositionsByYearRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/MyPositionsByYearRetrievalService.java index 8eba18a39..9e76ab185 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/MyPositionsByYearRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/MyPositionsByYearRetrievalService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberPositionInfoDto; +import page.clab.api.domain.memberManagement.position.application.dto.mapper.PositionDtoMapper; import page.clab.api.domain.memberManagement.position.application.dto.response.PositionMyResponseDto; import page.clab.api.domain.memberManagement.position.application.port.in.RetrieveMyPositionsByYearUseCase; import page.clab.api.domain.memberManagement.position.application.port.out.RetrievePositionPort; @@ -19,6 +20,7 @@ public class MyPositionsByYearRetrievalService implements RetrieveMyPositionsByY private final RetrievePositionPort retrievePositionPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final PositionDtoMapper mapper; @Transactional(readOnly = true) public PositionMyResponseDto retrieveMyPositionsByYear(String year) { @@ -28,6 +30,6 @@ public PositionMyResponseDto retrieveMyPositionsByYear(String year) { if (positions.isEmpty()) { throw new NotFoundException("해당 멤버의 " + year + "년도 직책이 존재하지 않습니다."); } - return PositionMyResponseDto.toDto(positions, currentMemberInfo); + return mapper.toDto(positions, currentMemberInfo); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRegisterService.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRegisterService.java index ce763df44..a40fa4062 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRegisterService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.position.application.dto.mapper.PositionDtoMapper; import page.clab.api.domain.memberManagement.position.application.dto.request.PositionRequestDto; import page.clab.api.domain.memberManagement.position.application.port.in.RegisterPositionUseCase; import page.clab.api.domain.memberManagement.position.application.port.out.RegisterPositionPort; @@ -17,6 +18,7 @@ public class PositionRegisterService implements RegisterPositionUseCase { private final RegisterPositionPort registerPositionPort; private final RetrievePositionPort retrievePositionPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final PositionDtoMapper mapper; @Transactional public Long registerPosition(PositionRequestDto requestDto) { @@ -25,7 +27,7 @@ public Long registerPosition(PositionRequestDto requestDto) { requestDto.getMemberId(), requestDto.getYear(), requestDto.getPositionType()) .map(Position::getId) .orElseGet(() -> { - Position position = PositionRequestDto.toEntity(requestDto); + Position position = mapper.fromDto(requestDto); return registerPositionPort.save(position).getId(); }); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRemoveService.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRemoveService.java index 06abe5331..04f027e28 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRemoveService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionRemoveService.java @@ -17,7 +17,7 @@ public class PositionRemoveService implements RemovePositionUseCase { @Transactional public Long removePosition(Long positionId) { - Position position = retrievePositionPort.findByIdOrThrow(positionId); + Position position = retrievePositionPort.getById(positionId); position.delete(); return updatePositionPort.update(position).getId(); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionsByConditionsRetrievalService.java index 0614a0725..5b2d69b1a 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionsByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/position/application/service/PositionsByConditionsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberPositionInfoDto; +import page.clab.api.domain.memberManagement.position.application.dto.mapper.PositionDtoMapper; import page.clab.api.domain.memberManagement.position.application.dto.response.PositionResponseDto; import page.clab.api.domain.memberManagement.position.application.port.in.RetrievePositionsByConditionsUseCase; import page.clab.api.domain.memberManagement.position.application.port.out.RetrievePositionPort; @@ -20,11 +21,12 @@ public class PositionsByConditionsRetrievalService implements RetrievePositionsB private final RetrievePositionPort retrievePositionPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final PositionDtoMapper mapper; @Transactional(readOnly = true) public PagedResponseDto retrievePositions(String year, PositionType positionType, Pageable pageable) { MemberPositionInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberPositionInfo(); Page positions = retrievePositionPort.findByConditions(year, positionType, pageable); - return new PagedResponseDto<>(positions.map(position -> PositionResponseDto.toDto(position, currentMemberInfo))); + return new PagedResponseDto<>(positions.map(position -> mapper.toDto(position, currentMemberInfo))); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperienceMapper.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperienceMapper.java index 60cb2bf3e..7398c35a0 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperienceMapper.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperienceMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface WorkExperienceMapper { - WorkExperienceJpaEntity toJpaEntity(WorkExperience workExperience); + WorkExperienceJpaEntity toEntity(WorkExperience workExperience); - WorkExperience toDomainEntity(WorkExperienceJpaEntity jpaEntity); + WorkExperience toDomain(WorkExperienceJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperiencePersistenceAdapter.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperiencePersistenceAdapter.java index ab15c3213..0f91a3f70 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperiencePersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/adapter/out/persistence/WorkExperiencePersistenceAdapter.java @@ -25,15 +25,15 @@ public class WorkExperiencePersistenceAdapter implements @Override public WorkExperience save(WorkExperience workExperience) { - WorkExperienceJpaEntity entity = mapper.toJpaEntity(workExperience); + WorkExperienceJpaEntity entity = mapper.toEntity(workExperience); WorkExperienceJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override public void saveAll(List workExperiences) { List entities = workExperiences.stream() - .map(mapper::toJpaEntity) + .map(mapper::toEntity) .collect(Collectors.toList()); repository.saveAll(entities); } @@ -41,13 +41,13 @@ public void saveAll(List workExperiences) { @Override public Page findAllByIsDeletedTrue(Pageable pageable) { return repository.findAllByIsDeletedTrue(pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public List findByMemberId(String memberId) { return repository.findByMemberId(memberId).stream() - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .toList(); } @@ -55,26 +55,26 @@ public List findByMemberId(String memberId) { public Page findByMemberId(String memberId, Pageable pageable) { Page workExperienceJpaEntities = repository.findByMemberId(memberId, pageable); return workExperienceJpaEntities - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public Page findByConditions(String memberId, Pageable pageable) { return repository.findByMemberId(memberId, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public WorkExperience update(WorkExperience workExperience) { - WorkExperienceJpaEntity entity = mapper.toJpaEntity(workExperience); + WorkExperienceJpaEntity entity = mapper.toEntity(workExperience); WorkExperienceJpaEntity updatedEntity = repository.save(entity); - return mapper.toDomainEntity(updatedEntity); + return mapper.toDomain(updatedEntity); } @Override - public WorkExperience findByIdOrThrow(Long id) { + public WorkExperience getById(Long id) { return repository.findById(id) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[WorkExperience] id: " + id + "에 해당하는 경력사항이 존재하지 않습니다.")); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/mapper/WorkExperienceDtoMapper.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/mapper/WorkExperienceDtoMapper.java new file mode 100644 index 000000000..e92dc518c --- /dev/null +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/mapper/WorkExperienceDtoMapper.java @@ -0,0 +1,31 @@ +package page.clab.api.domain.memberManagement.workExperience.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.workExperience.application.dto.request.WorkExperienceRequestDto; +import page.clab.api.domain.memberManagement.workExperience.application.dto.response.WorkExperienceResponseDto; +import page.clab.api.domain.memberManagement.workExperience.domain.WorkExperience; + +@Component +public class WorkExperienceDtoMapper { + + public WorkExperience fromDto(WorkExperienceRequestDto requestDto, String memberId) { + return WorkExperience.builder() + .companyName(requestDto.getCompanyName()) + .position(requestDto.getPosition()) + .startDate(requestDto.getStartDate()) + .endDate(requestDto.getEndDate()) + .memberId(memberId) + .isDeleted(false) + .build(); + } + + public WorkExperienceResponseDto toDto(WorkExperience workExperience) { + return WorkExperienceResponseDto.builder() + .id(workExperience.getId()) + .companyName(workExperience.getCompanyName()) + .position(workExperience.getPosition()) + .startDate(workExperience.getStartDate()) + .endDate(workExperience.getEndDate()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/request/WorkExperienceRequestDto.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/request/WorkExperienceRequestDto.java index 2f14fae45..47714e804 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/request/WorkExperienceRequestDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/request/WorkExperienceRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.memberManagement.workExperience.domain.WorkExperience; import java.time.LocalDate; @@ -27,15 +26,4 @@ public class WorkExperienceRequestDto { @NotNull(message = "{notNull.workExperience.endDate}") @Schema(description = "종료일", example = "2023-12-31", required = true) private LocalDate endDate; - - public static WorkExperience toEntity(WorkExperienceRequestDto requestDto, String memberId) { - return WorkExperience.builder() - .companyName(requestDto.getCompanyName()) - .position(requestDto.getPosition()) - .startDate(requestDto.getStartDate()) - .endDate(requestDto.getEndDate()) - .memberId(memberId) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/response/WorkExperienceResponseDto.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/response/WorkExperienceResponseDto.java index 4f96db2a2..9806c6d11 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/response/WorkExperienceResponseDto.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/dto/response/WorkExperienceResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.workExperience.domain.WorkExperience; import java.time.LocalDate; @@ -15,14 +14,4 @@ public class WorkExperienceResponseDto { private String position; private LocalDate startDate; private LocalDate endDate; - - public static WorkExperienceResponseDto toDto(WorkExperience workExperience) { - return WorkExperienceResponseDto.builder() - .id(workExperience.getId()) - .companyName(workExperience.getCompanyName()) - .position(workExperience.getPosition()) - .startDate(workExperience.getStartDate()) - .endDate(workExperience.getEndDate()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/port/out/RetrieveWorkExperiencePort.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/port/out/RetrieveWorkExperiencePort.java index 9bf4f3784..539d7b4f2 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/port/out/RetrieveWorkExperiencePort.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/port/out/RetrieveWorkExperiencePort.java @@ -8,7 +8,7 @@ public interface RetrieveWorkExperiencePort { - WorkExperience findByIdOrThrow(Long id); + WorkExperience getById(Long id); Page findAllByIsDeletedTrue(Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/DeletedWorkExperiencesRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/DeletedWorkExperiencesRetrievalService.java index 282477dfc..6befc1f38 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/DeletedWorkExperiencesRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/DeletedWorkExperiencesRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.workExperience.application.dto.mapper.WorkExperienceDtoMapper; import page.clab.api.domain.memberManagement.workExperience.application.dto.response.WorkExperienceResponseDto; import page.clab.api.domain.memberManagement.workExperience.application.port.in.RetrieveDeletedWorkExperiencesUseCase; import page.clab.api.domain.memberManagement.workExperience.application.port.out.RetrieveWorkExperiencePort; @@ -16,11 +17,12 @@ public class DeletedWorkExperiencesRetrievalService implements RetrieveDeletedWorkExperiencesUseCase { private final RetrieveWorkExperiencePort retrieveWorkExperiencePort; + private final WorkExperienceDtoMapper mapper; @Override @Transactional(readOnly = true) public PagedResponseDto retrieveDeletedWorkExperiences(Pageable pageable) { Page workExperiences = retrieveWorkExperiencePort.findAllByIsDeletedTrue(pageable); - return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::toDto)); + return new PagedResponseDto<>(workExperiences.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/MyWorkExperienceRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/MyWorkExperienceRetrievalService.java index 15cbb729c..cbe5184e9 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/MyWorkExperienceRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/MyWorkExperienceRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.workExperience.application.dto.mapper.WorkExperienceDtoMapper; import page.clab.api.domain.memberManagement.workExperience.application.dto.response.WorkExperienceResponseDto; import page.clab.api.domain.memberManagement.workExperience.application.port.in.RetrieveMyWorkExperienceUseCase; import page.clab.api.domain.memberManagement.workExperience.application.port.out.RetrieveWorkExperiencePort; @@ -18,12 +19,13 @@ public class MyWorkExperienceRetrievalService implements RetrieveMyWorkExperienc private final RetrieveWorkExperiencePort retrieveWorkExperiencePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final WorkExperienceDtoMapper mapper; @Override @Transactional(readOnly = true) public PagedResponseDto retrieveMyWorkExperience(Pageable pageable) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); Page workExperiences = retrieveWorkExperiencePort.findByMemberId(currentMemberId, pageable); - return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::toDto)); + return new PagedResponseDto<>(workExperiences.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRegisterService.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRegisterService.java index e5c465a78..93753ed2b 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRegisterService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.workExperience.application.dto.mapper.WorkExperienceDtoMapper; import page.clab.api.domain.memberManagement.workExperience.application.dto.request.WorkExperienceRequestDto; import page.clab.api.domain.memberManagement.workExperience.application.port.in.RegisterWorkExperienceUseCase; import page.clab.api.domain.memberManagement.workExperience.application.port.out.RegisterWorkExperiencePort; @@ -15,12 +16,13 @@ public class WorkExperienceRegisterService implements RegisterWorkExperienceUseC private final RegisterWorkExperiencePort registerWorkExperiencePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final WorkExperienceDtoMapper mapper; @Override @Transactional public Long registerWorkExperience(WorkExperienceRequestDto requestDto) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); - WorkExperience workExperience = WorkExperienceRequestDto.toEntity(requestDto, currentMemberId); + WorkExperience workExperience = mapper.fromDto(requestDto, currentMemberId); workExperience.validateBusinessRules(); return registerWorkExperiencePort.save(workExperience).getId(); } diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRemoveService.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRemoveService.java index 01ded13a3..880e1addb 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRemoveService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceRemoveService.java @@ -23,7 +23,7 @@ public class WorkExperienceRemoveService implements RemoveWorkExperienceUseCase @Transactional public Long removeWorkExperience(Long workExperienceId) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - WorkExperience workExperience = retrieveWorkExperiencePort.findByIdOrThrow(workExperienceId); + WorkExperience workExperience = retrieveWorkExperiencePort.getById(workExperienceId); workExperience.validateAccessPermission(currentMemberInfo); workExperience.delete(); return registerWorkExperiencePort.save(workExperience).getId(); diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceUpdateService.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceUpdateService.java index f76a6cb8d..f3af3c0e2 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceUpdateService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperienceUpdateService.java @@ -24,7 +24,7 @@ public class WorkExperienceUpdateService implements UpdateWorkExperienceUseCase @Transactional public Long updateWorkExperience(Long workExperienceId, WorkExperienceUpdateRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - WorkExperience workExperience = retrieveWorkExperiencePort.findByIdOrThrow(workExperienceId); + WorkExperience workExperience = retrieveWorkExperiencePort.getById(workExperienceId); workExperience.validateAccessPermission(currentMemberInfo); workExperience.update(requestDto); workExperience.validateBusinessRules(); diff --git a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperiencesByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperiencesByConditionsRetrievalService.java index 7a1fe331d..513d805f7 100644 --- a/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperiencesByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/memberManagement/workExperience/application/service/WorkExperiencesByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.workExperience.application.dto.mapper.WorkExperienceDtoMapper; import page.clab.api.domain.memberManagement.workExperience.application.dto.response.WorkExperienceResponseDto; import page.clab.api.domain.memberManagement.workExperience.application.port.in.RetrieveWorkExperiencesByConditionsUseCase; import page.clab.api.domain.memberManagement.workExperience.application.port.out.RetrieveWorkExperiencePort; @@ -16,11 +17,12 @@ public class WorkExperiencesByConditionsRetrievalService implements RetrieveWorkExperiencesByConditionsUseCase { private final RetrieveWorkExperiencePort retrieveWorkExperiencePort; + private final WorkExperienceDtoMapper mapper; @Override @Transactional(readOnly = true) public PagedResponseDto retrieveWorkExperiences(String memberId, Pageable pageable) { Page workExperiences = retrieveWorkExperiencePort.findByConditions(memberId, pageable); - return new PagedResponseDto<>(workExperiences.map(WorkExperienceResponseDto::toDto)); + return new PagedResponseDto<>(workExperiences.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoMapper.java b/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoMapper.java index 71eaaca71..eef7cdbec 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoMapper.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface ActivityPhotoMapper { - ActivityPhotoJpaEntity toJpaEntity(ActivityPhoto activityPhoto); + ActivityPhotoJpaEntity toEntity(ActivityPhoto activityPhoto); ActivityPhoto toDomain(ActivityPhotoJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoPersistenceAdapter.java b/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoPersistenceAdapter.java index feeaad272..1816f4147 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/adapter/out/persistence/ActivityPhotoPersistenceAdapter.java @@ -20,13 +20,13 @@ public class ActivityPhotoPersistenceAdapter implements @Override public ActivityPhoto save(ActivityPhoto activityPhoto) { - ActivityPhotoJpaEntity entity = activityPhotoMapper.toJpaEntity(activityPhoto); + ActivityPhotoJpaEntity entity = activityPhotoMapper.toEntity(activityPhoto); ActivityPhotoJpaEntity savedEntity = activityPhotoRepository.save(entity); return activityPhotoMapper.toDomain(savedEntity); } @Override - public ActivityPhoto findByIdOrThrow(Long activityPhotoId) { + public ActivityPhoto getById(Long activityPhotoId) { return activityPhotoRepository.findById(activityPhotoId) .map(activityPhotoMapper::toDomain) .orElseThrow(() -> new NotFoundException("[ActivityPhoto] id: " + activityPhotoId + "에 해당하는 활동 사진이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/mapper/ActivityPhotoDtoMapper.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/mapper/ActivityPhotoDtoMapper.java new file mode 100644 index 000000000..619bc29f5 --- /dev/null +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/mapper/ActivityPhotoDtoMapper.java @@ -0,0 +1,39 @@ +package page.clab.api.domain.members.activityPhoto.application.dto.mapper; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import page.clab.api.domain.members.activityPhoto.application.dto.request.ActivityPhotoRequestDto; +import page.clab.api.domain.members.activityPhoto.application.dto.response.ActivityPhotoResponseDto; +import page.clab.api.domain.members.activityPhoto.domain.ActivityPhoto; +import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.common.file.dto.mapper.FileDtoMapper; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class ActivityPhotoDtoMapper { + + private final FileDtoMapper mapper; + + public ActivityPhoto fromDto(ActivityPhotoRequestDto requestDto, List uploadedFiles) { + return ActivityPhoto.builder() + .title(requestDto.getTitle()) + .uploadedFiles(uploadedFiles) + .date(requestDto.getDate()) + .isPublic(false) + .isDeleted(false) + .build(); + } + + public ActivityPhotoResponseDto toDto(ActivityPhoto activityPhoto) { + return ActivityPhotoResponseDto.builder() + .id(activityPhoto.getId()) + .title(activityPhoto.getTitle()) + .files(mapper.toDto(activityPhoto.getUploadedFiles())) + .date(activityPhoto.getDate()) + .isPublic(activityPhoto.getIsPublic()) + .createdAt(activityPhoto.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/request/ActivityPhotoRequestDto.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/request/ActivityPhotoRequestDto.java index 3f5f30f04..b35e2efc6 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/request/ActivityPhotoRequestDto.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/request/ActivityPhotoRequestDto.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.members.activityPhoto.domain.ActivityPhoto; -import page.clab.api.global.common.file.domain.UploadedFile; import java.time.LocalDate; import java.util.List; @@ -25,14 +23,4 @@ public class ActivityPhotoRequestDto { @NotNull(message = "{notNull.activityPhoto.date}") @Schema(description = "활동 날짜", example = "2021-01-01", required = true) private LocalDate date; - - public static ActivityPhoto toEntity(ActivityPhotoRequestDto requestDto, List uploadedFiles) { - return ActivityPhoto.builder() - .title(requestDto.getTitle()) - .uploadedFiles(uploadedFiles) - .date(requestDto.getDate()) - .isPublic(false) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/response/ActivityPhotoResponseDto.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/response/ActivityPhotoResponseDto.java index 994ba2a5d..01b763ba6 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/response/ActivityPhotoResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/dto/response/ActivityPhotoResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.members.activityPhoto.domain.ActivityPhoto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import java.time.LocalDate; @@ -19,15 +18,4 @@ public class ActivityPhotoResponseDto { private LocalDate date; private Boolean isPublic; private LocalDateTime createdAt; - - public static ActivityPhotoResponseDto toDto(ActivityPhoto activityPhoto) { - return ActivityPhotoResponseDto.builder() - .id(activityPhoto.getId()) - .title(activityPhoto.getTitle()) - .files(UploadedFileResponseDto.toDto(activityPhoto.getUploadedFiles())) - .date(activityPhoto.getDate()) - .isPublic(activityPhoto.getIsPublic()) - .createdAt(activityPhoto.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/port/out/RetrieveActivityPhotoPort.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/port/out/RetrieveActivityPhotoPort.java index de627d113..cdd6ce9d7 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/port/out/RetrieveActivityPhotoPort.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/port/out/RetrieveActivityPhotoPort.java @@ -6,7 +6,7 @@ public interface RetrieveActivityPhotoPort { - ActivityPhoto findByIdOrThrow(Long activityPhotoId); + ActivityPhoto getById(Long activityPhotoId); Page findByConditions(Boolean isPublic, Pageable pageable); } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRegisterService.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRegisterService.java index 2c54e534a..5482a9a9d 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRegisterService.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.activityPhoto.application.dto.mapper.ActivityPhotoDtoMapper; import page.clab.api.domain.members.activityPhoto.application.dto.request.ActivityPhotoRequestDto; import page.clab.api.domain.members.activityPhoto.application.port.in.RegisterActivityPhotoUseCase; import page.clab.api.domain.members.activityPhoto.application.port.out.RegisterActivityPhotoPort; @@ -18,12 +19,13 @@ public class ActivityPhotoRegisterService implements RegisterActivityPhotoUseCas private final RegisterActivityPhotoPort registerActivityPhotoPort; private final UploadedFileService uploadedFileService; + private final ActivityPhotoDtoMapper mapper; @Transactional @Override public Long registerActivityPhoto(ActivityPhotoRequestDto requestDto) { List uploadedFiles = uploadedFileService.getUploadedFilesByUrls(requestDto.getFileUrlList()); - ActivityPhoto activityPhoto = ActivityPhotoRequestDto.toEntity(requestDto, uploadedFiles); + ActivityPhoto activityPhoto = mapper.fromDto(requestDto, uploadedFiles); return registerActivityPhotoPort.save(activityPhoto).getId(); } } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRemoveService.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRemoveService.java index 992ebfd3a..0520eb1f5 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRemoveService.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRemoveService.java @@ -18,7 +18,7 @@ public class ActivityPhotoRemoveService implements RemoveActivityPhotoUseCase { @Transactional @Override public Long removeActivityPhoto(Long activityPhotoId) { - ActivityPhoto activityPhoto = retrieveActivityPhotoPort.findByIdOrThrow(activityPhotoId); + ActivityPhoto activityPhoto = retrieveActivityPhotoPort.getById(activityPhotoId); activityPhoto.delete(); return registerActivityPhotoPort.save(activityPhoto).getId(); } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRetrievalService.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRetrievalService.java index ea0b96882..55ff567f4 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.activityPhoto.application.dto.mapper.ActivityPhotoDtoMapper; import page.clab.api.domain.members.activityPhoto.application.dto.response.ActivityPhotoResponseDto; import page.clab.api.domain.members.activityPhoto.application.port.in.RetrieveActivityPhotoUseCase; import page.clab.api.domain.members.activityPhoto.application.port.out.RetrieveActivityPhotoPort; @@ -16,11 +17,12 @@ public class ActivityPhotoRetrievalService implements RetrieveActivityPhotoUseCase { private final RetrieveActivityPhotoPort retrieveActivityPhotoPort; + private final ActivityPhotoDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveActivityPhotos(Boolean isPublic, Pageable pageable) { Page activityPhotos = retrieveActivityPhotoPort.findByConditions(isPublic, pageable); - return new PagedResponseDto<>(activityPhotos.map(ActivityPhotoResponseDto::toDto)); + return new PagedResponseDto<>(activityPhotos.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoVisibilityService.java b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoVisibilityService.java index 9472666f6..8d359e4fe 100644 --- a/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoVisibilityService.java +++ b/src/main/java/page/clab/api/domain/members/activityPhoto/application/service/ActivityPhotoVisibilityService.java @@ -18,7 +18,7 @@ public class ActivityPhotoVisibilityService implements ToggleActivityPhotoVisibi @Transactional @Override public Long toggleActivityPhotoVisibility(Long activityPhotoId) { - ActivityPhoto activityPhoto = retrieveActivityPhotoPort.findByIdOrThrow(activityPhotoId); + ActivityPhoto activityPhoto = retrieveActivityPhotoPort.getById(activityPhotoId); activityPhoto.togglePublicStatus(); return registerActivityPhotoPort.save(activityPhoto).getId(); } diff --git a/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogMapper.java b/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogMapper.java index 58d1101fe..efff83a83 100644 --- a/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogMapper.java +++ b/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface BlogMapper { - BlogJpaEntity toJpaEntity(Blog blog); + BlogJpaEntity toEntity(Blog blog); Blog toDomain(BlogJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogPersistenceAdapter.java b/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogPersistenceAdapter.java index 3737b377a..ab374db25 100644 --- a/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/members/blog/adapter/out/persistence/BlogPersistenceAdapter.java @@ -20,13 +20,13 @@ public class BlogPersistenceAdapter implements @Override public Blog save(Blog blog) { - BlogJpaEntity entity = blogMapper.toJpaEntity(blog); + BlogJpaEntity entity = blogMapper.toEntity(blog); BlogJpaEntity savedEntity = blogRepository.save(entity); return blogMapper.toDomain(savedEntity); } @Override - public Blog findByIdOrThrow(Long blogId) { + public Blog getById(Long blogId) { return blogRepository.findById(blogId) .map(blogMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Blog] id: " + blogId + "에 해당하는 게시글이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/members/blog/application/dto/mapper/BlogDtoMapper.java b/src/main/java/page/clab/api/domain/members/blog/application/dto/mapper/BlogDtoMapper.java new file mode 100644 index 000000000..5d0803380 --- /dev/null +++ b/src/main/java/page/clab/api/domain/members/blog/application/dto/mapper/BlogDtoMapper.java @@ -0,0 +1,50 @@ +package page.clab.api.domain.members.blog.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.blog.application.dto.request.BlogRequestDto; +import page.clab.api.domain.members.blog.application.dto.response.BlogDetailsResponseDto; +import page.clab.api.domain.members.blog.application.dto.response.BlogResponseDto; +import page.clab.api.domain.members.blog.domain.Blog; + +@Component +public class BlogDtoMapper { + + public Blog fromDto(BlogRequestDto requestDto, String memberId) { + return Blog.builder() + .memberId(memberId) + .title(requestDto.getTitle()) + .subTitle(requestDto.getSubTitle()) + .content(requestDto.getContent()) + .hyperlink(requestDto.getHyperlink()) + .imageUrl(requestDto.getImageUrl()) + .isDeleted(false) + .build(); + } + + public BlogDetailsResponseDto toDto(Blog blog, MemberBasicInfoDto memberInfo, boolean isOwner) { + return BlogDetailsResponseDto.builder() + .id(blog.getId()) + .memberId(memberInfo.getMemberId()) + .name(memberInfo.getMemberName()) + .title(blog.getTitle()) + .subTitle(blog.getSubTitle()) + .content(blog.getContent()) + .imageUrl(blog.getImageUrl()) + .hyperlink(blog.getHyperlink()) + .isOwner(isOwner) + .createdAt(blog.getCreatedAt()) + .build(); + } + + public BlogResponseDto toDto(Blog blog) { + return BlogResponseDto.builder() + .id(blog.getId()) + .title(blog.getTitle()) + .subTitle(blog.getSubTitle()) + .imageUrl(blog.getImageUrl()) + .hyperlink(blog.getHyperlink()) + .createdAt(blog.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/members/blog/application/dto/request/BlogRequestDto.java b/src/main/java/page/clab/api/domain/members/blog/application/dto/request/BlogRequestDto.java index 81b4493b0..e9c79e273 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/dto/request/BlogRequestDto.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/dto/request/BlogRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.members.blog.domain.Blog; @Getter @Setter @@ -27,16 +26,4 @@ public class BlogRequestDto { @Schema(description = "하이퍼링크", example = "https://www.clab.page") private String hyperlink; - - public static Blog toEntity(BlogRequestDto requestDto, String memberId) { - return Blog.builder() - .memberId(memberId) - .title(requestDto.getTitle()) - .subTitle(requestDto.getSubTitle()) - .content(requestDto.getContent()) - .hyperlink(requestDto.getHyperlink()) - .imageUrl(requestDto.getImageUrl()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogDetailsResponseDto.java b/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogDetailsResponseDto.java index 775eae94f..672971b4b 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogDetailsResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogDetailsResponseDto.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; -import page.clab.api.domain.members.blog.domain.Blog; import java.time.LocalDateTime; @@ -24,19 +22,4 @@ public class BlogDetailsResponseDto { @JsonProperty("isOwner") private Boolean isOwner; private LocalDateTime createdAt; - - public static BlogDetailsResponseDto toDto(Blog blog, MemberBasicInfoDto memberInfo, boolean isOwner) { - return BlogDetailsResponseDto.builder() - .id(blog.getId()) - .memberId(memberInfo.getMemberId()) - .name(memberInfo.getMemberName()) - .title(blog.getTitle()) - .subTitle(blog.getSubTitle()) - .content(blog.getContent()) - .imageUrl(blog.getImageUrl()) - .hyperlink(blog.getHyperlink()) - .isOwner(isOwner) - .createdAt(blog.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogResponseDto.java b/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogResponseDto.java index ffb0284ae..7fce3e1c3 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/dto/response/BlogResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.members.blog.domain.Blog; import java.time.LocalDateTime; @@ -16,15 +15,4 @@ public class BlogResponseDto { private String imageUrl; private String hyperlink; private LocalDateTime createdAt; - - public static BlogResponseDto toDto(Blog blog) { - return BlogResponseDto.builder() - .id(blog.getId()) - .title(blog.getTitle()) - .subTitle(blog.getSubTitle()) - .imageUrl(blog.getImageUrl()) - .hyperlink(blog.getHyperlink()) - .createdAt(blog.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/blog/application/port/out/RetrieveBlogPort.java b/src/main/java/page/clab/api/domain/members/blog/application/port/out/RetrieveBlogPort.java index 512fc7110..8a4cc0832 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/port/out/RetrieveBlogPort.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/port/out/RetrieveBlogPort.java @@ -6,7 +6,7 @@ public interface RetrieveBlogPort { - Blog findByIdOrThrow(Long blogId); + Blog getById(Long blogId); Page findByConditions(String title, String memberName, Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogDetailsRetrievalService.java b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogDetailsRetrievalService.java index d0f7874c2..bc8121fc5 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogDetailsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogDetailsRetrievalService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.blog.application.dto.mapper.BlogDtoMapper; import page.clab.api.domain.members.blog.application.dto.response.BlogDetailsResponseDto; import page.clab.api.domain.members.blog.application.port.in.RetrieveBlogDetailsUseCase; import page.clab.api.domain.members.blog.application.port.out.RetrieveBlogPort; @@ -16,13 +17,14 @@ public class BlogDetailsRetrievalService implements RetrieveBlogDetailsUseCase { private final RetrieveBlogPort retrieveBlogPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BlogDtoMapper mapper; @Transactional(readOnly = true) @Override public BlogDetailsResponseDto retrieveBlogDetails(Long blogId) { MemberBasicInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo(); - Blog blog = retrieveBlogPort.findByIdOrThrow(blogId); + Blog blog = retrieveBlogPort.getById(blogId); boolean isOwner = blog.isOwner(currentMemberInfo.getMemberId()); - return BlogDetailsResponseDto.toDto(blog, currentMemberInfo, isOwner); + return mapper.toDto(blog, currentMemberInfo, isOwner); } } diff --git a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRegisterService.java b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRegisterService.java index ceaa1aeb9..d8d803be3 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRegisterService.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.blog.application.dto.mapper.BlogDtoMapper; import page.clab.api.domain.members.blog.application.dto.request.BlogRequestDto; import page.clab.api.domain.members.blog.application.port.in.RegisterBlogUseCase; import page.clab.api.domain.members.blog.application.port.out.RegisterBlogPort; @@ -15,12 +16,13 @@ public class BlogRegisterService implements RegisterBlogUseCase { private final RegisterBlogPort registerBlogPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BlogDtoMapper mapper; @Transactional @Override public Long registerBlog(BlogRequestDto requestDto) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); - Blog blog = BlogRequestDto.toEntity(requestDto, currentMemberId); + Blog blog = mapper.fromDto(requestDto, currentMemberId); return registerBlogPort.save(blog).getId(); } } diff --git a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRemoveService.java b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRemoveService.java index 58ee0829f..12b583fad 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRemoveService.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogRemoveService.java @@ -23,7 +23,7 @@ public class BlogRemoveService implements RemoveBlogUseCase { @Override public Long removeBlog(Long blogId) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - Blog blog = retrieveBlogPort.findByIdOrThrow(blogId); + Blog blog = retrieveBlogPort.getById(blogId); blog.validateAccessPermission(currentMember); blog.delete(); return registerBlogPort.save(blog).getId(); diff --git a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogUpdateService.java b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogUpdateService.java index 1d6f4fe3d..32f690c0f 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogUpdateService.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogUpdateService.java @@ -24,7 +24,7 @@ public class BlogUpdateService implements UpdateBlogUseCase { @Override public Long updateBlog(Long blogId, BlogUpdateRequestDto requestDto) throws PermissionDeniedException { Member currentMember = externalRetrieveMemberUseCase.getCurrentMember(); - Blog blog = retrieveBlogPort.findByIdOrThrow(blogId); + Blog blog = retrieveBlogPort.getById(blogId); blog.validateAccessPermission(currentMember); blog.update(requestDto); return registerBlogPort.save(blog).getId(); diff --git a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogsRetrievalService.java b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogsRetrievalService.java index c31f84497..051e24184 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/service/BlogsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/service/BlogsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.blog.application.dto.mapper.BlogDtoMapper; import page.clab.api.domain.members.blog.application.dto.response.BlogResponseDto; import page.clab.api.domain.members.blog.application.port.in.RetrieveBlogsUseCase; import page.clab.api.domain.members.blog.application.port.out.RetrieveBlogPort; @@ -16,11 +17,12 @@ public class BlogsRetrievalService implements RetrieveBlogsUseCase { private final RetrieveBlogPort retrieveBlogPort; + private final BlogDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveBlogs(String title, String memberName, Pageable pageable) { Page blogs = retrieveBlogPort.findByConditions(title, memberName, pageable); - return new PagedResponseDto<>(blogs.map(BlogResponseDto::toDto)); + return new PagedResponseDto<>(blogs.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/blog/application/service/DeletedBlogsRetrievalService.java b/src/main/java/page/clab/api/domain/members/blog/application/service/DeletedBlogsRetrievalService.java index b494d8a5d..aeb179c17 100644 --- a/src/main/java/page/clab/api/domain/members/blog/application/service/DeletedBlogsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/blog/application/service/DeletedBlogsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.blog.application.dto.mapper.BlogDtoMapper; import page.clab.api.domain.members.blog.application.dto.response.BlogDetailsResponseDto; import page.clab.api.domain.members.blog.application.port.in.RetrieveDeletedBlogsUseCase; import page.clab.api.domain.members.blog.application.port.out.RetrieveBlogPort; @@ -19,12 +20,13 @@ public class DeletedBlogsRetrievalService implements RetrieveDeletedBlogsUseCase private final RetrieveBlogPort retrieveBlogPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final BlogDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveDeletedBlogs(Pageable pageable) { MemberBasicInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo(); Page blogs = retrieveBlogPort.findAllByIsDeletedTrue(pageable); - return new PagedResponseDto<>(blogs.map(blog -> BlogDetailsResponseDto.toDto(blog, currentMemberInfo, blog.isOwner(currentMemberInfo.getMemberId())))); + return new PagedResponseDto<>(blogs.map(blog -> mapper.toDto(blog, currentMemberInfo, blog.isOwner(currentMemberInfo.getMemberId())))); } } diff --git a/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationMapper.java b/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationMapper.java index 86a4145b9..784092c23 100644 --- a/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationMapper.java +++ b/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface DonationMapper { - DonationJpaEntity toJpaEntity(Donation domain); + DonationJpaEntity toEntity(Donation domain); Donation toDomain(DonationJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationPersistenceAdapter.java b/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationPersistenceAdapter.java index 29ea1f0d8..0a25f2b96 100644 --- a/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/members/donation/adapter/out/persistence/DonationPersistenceAdapter.java @@ -22,7 +22,7 @@ public class DonationPersistenceAdapter implements @Override public Donation save(Donation donation) { - DonationJpaEntity entity = donationMapper.toJpaEntity(donation); + DonationJpaEntity entity = donationMapper.toEntity(donation); DonationJpaEntity savedEntity = repository.save(entity); return donationMapper.toDomain(savedEntity); } @@ -40,7 +40,7 @@ public Page findByConditions(String memberId, String name, LocalDate s } @Override - public Donation findByIdOrThrow(Long donationId) { + public Donation getById(Long donationId) { return repository.findById(donationId) .map(donationMapper::toDomain) .orElseThrow(() -> new NotFoundException("[Donation] id: " + donationId + "에 해당하는 후원이 존재하지 않습니다.")); diff --git a/src/main/java/page/clab/api/domain/members/donation/application/dto/mapper/DonationDtoMapper.java b/src/main/java/page/clab/api/domain/members/donation/application/dto/mapper/DonationDtoMapper.java new file mode 100644 index 000000000..466002f8d --- /dev/null +++ b/src/main/java/page/clab/api/domain/members/donation/application/dto/mapper/DonationDtoMapper.java @@ -0,0 +1,30 @@ +package page.clab.api.domain.members.donation.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.members.donation.application.dto.request.DonationRequestDto; +import page.clab.api.domain.members.donation.application.dto.response.DonationResponseDto; +import page.clab.api.domain.members.donation.domain.Donation; + +@Component +public class DonationDtoMapper { + + public Donation fromDto(DonationRequestDto requestDto, String memberId) { + return Donation.builder() + .memberId(memberId) + .amount(requestDto.getAmount()) + .message(requestDto.getMessage()) + .isDeleted(false) + .build(); + } + + public DonationResponseDto toDto(Donation donation, String memberName) { + return DonationResponseDto.builder() + .id(donation.getId()) + .memberId(donation.getMemberId()) + .memberName(memberName) + .amount(donation.getAmount()) + .message(donation.getMessage()) + .createdAt(donation.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/members/donation/application/dto/request/DonationRequestDto.java b/src/main/java/page/clab/api/domain/members/donation/application/dto/request/DonationRequestDto.java index a44132d41..7836e1a8f 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/dto/request/DonationRequestDto.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/dto/request/DonationRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.members.donation.domain.Donation; @Getter @Setter @@ -17,13 +16,4 @@ public class DonationRequestDto { @NotNull(message = "{notNull.donation.message}") @Schema(description = "후원 메시지", example = "대회 상금 일부 후원", required = true) private String message; - - public static Donation toEntity(DonationRequestDto requestDto, String memberId) { - return Donation.builder() - .memberId(memberId) - .amount(requestDto.getAmount()) - .message(requestDto.getMessage()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/donation/application/dto/response/DonationResponseDto.java b/src/main/java/page/clab/api/domain/members/donation/application/dto/response/DonationResponseDto.java index d700b0fd5..3de241d90 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/dto/response/DonationResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/dto/response/DonationResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.members.donation.domain.Donation; import java.time.LocalDateTime; @@ -16,15 +15,4 @@ public class DonationResponseDto { private Double amount; private String message; private LocalDateTime createdAt; - - public static DonationResponseDto toDto(Donation donation, String memberName) { - return DonationResponseDto.builder() - .id(donation.getId()) - .memberId(donation.getMemberId()) - .memberName(memberName) - .amount(donation.getAmount()) - .message(donation.getMessage()) - .createdAt(donation.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/donation/application/port/out/RetrieveDonationPort.java b/src/main/java/page/clab/api/domain/members/donation/application/port/out/RetrieveDonationPort.java index acd99a85b..052991247 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/port/out/RetrieveDonationPort.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/port/out/RetrieveDonationPort.java @@ -8,7 +8,7 @@ public interface RetrieveDonationPort { - Donation findByIdOrThrow(Long donationId); + Donation getById(Long donationId); Page findAllByIsDeletedTrue(Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/members/donation/application/service/DeletedDonationsRetrievalService.java b/src/main/java/page/clab/api/domain/members/donation/application/service/DeletedDonationsRetrievalService.java index de7f0f2e1..87109b0c2 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/service/DeletedDonationsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/service/DeletedDonationsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.donation.application.dto.mapper.DonationDtoMapper; import page.clab.api.domain.members.donation.application.dto.response.DonationResponseDto; import page.clab.api.domain.members.donation.application.port.in.RetrieveDeletedDonationsUseCase; import page.clab.api.domain.members.donation.application.port.out.RetrieveDonationPort; @@ -19,6 +20,7 @@ public class DeletedDonationsRetrievalService implements RetrieveDeletedDonation private final RetrieveDonationPort retrieveDonationPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final DonationDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -26,7 +28,7 @@ public PagedResponseDto retrieveDeletedDonations(Pageable p Page donations = retrieveDonationPort.findAllByIsDeletedTrue(pageable); return new PagedResponseDto<>(donations.map(donation -> { MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberBasicInfoById(donation.getMemberId()); - return DonationResponseDto.toDto(donation, memberInfo.getMemberName()); + return mapper.toDto(donation, memberInfo.getMemberName()); })); } } diff --git a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRegisterService.java b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRegisterService.java index c1c437c6e..fe463746f 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRegisterService.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.donation.application.dto.mapper.DonationDtoMapper; import page.clab.api.domain.members.donation.application.dto.request.DonationRequestDto; import page.clab.api.domain.members.donation.application.port.in.RegisterDonationUseCase; import page.clab.api.domain.members.donation.application.port.out.RegisterDonationPort; @@ -15,12 +16,13 @@ public class DonationRegisterService implements RegisterDonationUseCase { private final RegisterDonationPort registerDonationPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final DonationDtoMapper mapper; @Transactional @Override public Long registerDonation(DonationRequestDto requestDto) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); - Donation donation = DonationRequestDto.toEntity(requestDto, currentMemberId); + Donation donation = mapper.fromDto(requestDto, currentMemberId); return registerDonationPort.save(donation).getId(); } } diff --git a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRemoveService.java b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRemoveService.java index 105b2bb5d..9c9db0dd5 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRemoveService.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationRemoveService.java @@ -23,7 +23,7 @@ public class DonationRemoveService implements RemoveDonationUseCase { @Override public Long removeDonation(Long donationId) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Donation donation = retrieveDonationPort.findByIdOrThrow(donationId); + Donation donation = retrieveDonationPort.getById(donationId); donation.validateAccessPermission(currentMemberInfo.isSuperAdminRole()); donation.delete(); return registerDonationPort.save(donation).getId(); diff --git a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationUpdateService.java b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationUpdateService.java index 7dff513df..4bfc49e9c 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationUpdateService.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationUpdateService.java @@ -24,7 +24,7 @@ public class DonationUpdateService implements UpdateDonationUseCase { @Override public Long updateDonation(Long donationId, DonationUpdateRequestDto donationUpdateRequestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Donation donation = retrieveDonationPort.findByIdOrThrow(donationId); + Donation donation = retrieveDonationPort.getById(donationId); donation.validateAccessPermission(currentMemberInfo.isSuperAdminRole()); donation.update(donationUpdateRequestDto); return registerDonationPort.save(donation).getId(); diff --git a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationsByConditionsRetrievalService.java index 18cc7dfae..158e6d3e7 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/service/DonationsByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/service/DonationsByConditionsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.donation.application.dto.mapper.DonationDtoMapper; import page.clab.api.domain.members.donation.application.dto.response.DonationResponseDto; import page.clab.api.domain.members.donation.application.port.in.RetrieveDonationsByConditionsUseCase; import page.clab.api.domain.members.donation.application.port.out.RetrieveDonationPort; @@ -21,6 +22,7 @@ public class DonationsByConditionsRetrievalService implements RetrieveDonationsB private final RetrieveDonationPort retrieveDonationPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final DonationDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -28,7 +30,7 @@ public PagedResponseDto retrieveDonations(String memberId, Page donations = retrieveDonationPort.findByConditions(memberId, name, startDate, endDate, pageable); return new PagedResponseDto<>(donations.map(donation -> { MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberBasicInfoById(donation.getMemberId()); - return DonationResponseDto.toDto(donation, memberInfo.getMemberName()); + return mapper.toDto(donation, memberInfo.getMemberName()); })); } } diff --git a/src/main/java/page/clab/api/domain/members/donation/application/service/MyDonationsRetrievalService.java b/src/main/java/page/clab/api/domain/members/donation/application/service/MyDonationsRetrievalService.java index 075205809..80fad7322 100644 --- a/src/main/java/page/clab/api/domain/members/donation/application/service/MyDonationsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/donation/application/service/MyDonationsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.donation.application.dto.mapper.DonationDtoMapper; import page.clab.api.domain.members.donation.application.dto.response.DonationResponseDto; import page.clab.api.domain.members.donation.application.port.in.RetrieveMyDonationsUseCase; import page.clab.api.domain.members.donation.application.port.out.RetrieveDonationPort; @@ -19,6 +20,7 @@ public class MyDonationsRetrievalService implements RetrieveMyDonationsUseCase { private final RetrieveDonationPort retrieveDonationPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final DonationDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -27,7 +29,7 @@ public PagedResponseDto retrieveMyDonations(Pageable pageab Page donations = retrieveDonationPort.findByMemberId(currentMemberId, pageable); return new PagedResponseDto<>(donations.map(donation -> { MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberBasicInfoById(donation.getMemberId()); - return DonationResponseDto.toDto(donation, memberInfo.getMemberName()); + return mapper.toDto(donation, memberInfo.getMemberName()); })); } } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeeMapper.java b/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeeMapper.java index e2d0a1a25..13f13d292 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeeMapper.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeeMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface MembershipFeeMapper { - MembershipFeeJpaEntity toJpaEntity(MembershipFee membershipFee); + MembershipFeeJpaEntity toEntity(MembershipFee membershipFee); - MembershipFee toDomainEntity(MembershipFeeJpaEntity jpaEntity); + MembershipFee toDomain(MembershipFeeJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeePersistenceAdapter.java b/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeePersistenceAdapter.java index 382978bed..d31891117 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeePersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/adapter/out/persistence/MembershipFeePersistenceAdapter.java @@ -22,35 +22,35 @@ public class MembershipFeePersistenceAdapter implements private final MembershipFeeMapper mapper; @Override - public MembershipFee findByIdOrThrow(Long id) { + public MembershipFee getById(Long id) { return repository.findById(id) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("존재하지 않는 회비 내역입니다.")); } @Override public MembershipFee save(MembershipFee membershipFee) { - MembershipFeeJpaEntity entity = mapper.toJpaEntity(membershipFee); + MembershipFeeJpaEntity entity = mapper.toEntity(membershipFee); MembershipFeeJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override public MembershipFee update(MembershipFee membershipFee) { - MembershipFeeJpaEntity entity = mapper.toJpaEntity(membershipFee); + MembershipFeeJpaEntity entity = mapper.toEntity(membershipFee); MembershipFeeJpaEntity updatedEntity = repository.save(entity); - return mapper.toDomainEntity(updatedEntity); + return mapper.toDomain(updatedEntity); } @Override public Page findAllByIsDeletedTrue(Pageable pageable) { return repository.findAllByIsDeletedTrue(pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public Page findByConditions(String memberId, String memberName, String category, MembershipFeeStatus status, Pageable pageable) { return repository.findByConditions(memberId, memberName, category, status, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/mapper/MembershipFeeDtoMapper.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/mapper/MembershipFeeDtoMapper.java new file mode 100644 index 000000000..87de4413b --- /dev/null +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/mapper/MembershipFeeDtoMapper.java @@ -0,0 +1,39 @@ +package page.clab.api.domain.members.membershipFee.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.members.membershipFee.application.dto.request.MembershipFeeRequestDto; +import page.clab.api.domain.members.membershipFee.application.dto.response.MembershipFeeResponseDto; +import page.clab.api.domain.members.membershipFee.domain.MembershipFee; +import page.clab.api.domain.members.membershipFee.domain.MembershipFeeStatus; + +@Component +public class MembershipFeeDtoMapper { + + public MembershipFee fromDto(MembershipFeeRequestDto requestDto, String memberId) { + return MembershipFee.builder() + .memberId(memberId) + .category(requestDto.getCategory()) + .account(requestDto.getAccount()) + .amount(requestDto.getAmount()) + .content(requestDto.getContent()) + .imageUrl(requestDto.getImageUrl()) + .status(MembershipFeeStatus.PENDING) + .isDeleted(false) + .build(); + } + + public MembershipFeeResponseDto toDto(MembershipFee membershipFee, String memberName, boolean isAdminRole) { + return MembershipFeeResponseDto.builder() + .id(membershipFee.getId()) + .memberId(membershipFee.getMemberId()) + .memberName(memberName) + .category(membershipFee.getCategory()) + .account(isAdminRole ? membershipFee.getAccount() : null) + .amount(membershipFee.getAmount()) + .content(membershipFee.getContent()) + .imageUrl(membershipFee.getImageUrl()) + .status(membershipFee.getStatus()) + .createdAt(membershipFee.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/request/MembershipFeeRequestDto.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/request/MembershipFeeRequestDto.java index bc27092d9..48fa55d5e 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/request/MembershipFeeRequestDto.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/request/MembershipFeeRequestDto.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.members.membershipFee.domain.MembershipFee; -import page.clab.api.domain.members.membershipFee.domain.MembershipFeeStatus; @Getter @Setter @@ -28,17 +26,4 @@ public class MembershipFeeRequestDto { @Schema(description = "증빙 사진", example = "https://images.chosun.com/resizer/mcbrEkwTr5YKQZ89QPO9hmdb0iE=/616x0/smart/cloudfront-ap-northeast-1.images.arcpublishing.com/chosun/LPCZYYKZ4FFIJPDD344FSGCLCY.jpg") private String imageUrl; - - public static MembershipFee toEntity(MembershipFeeRequestDto requestDto, String memberId) { - return MembershipFee.builder() - .memberId(memberId) - .category(requestDto.getCategory()) - .account(requestDto.getAccount()) - .amount(requestDto.getAmount()) - .content(requestDto.getContent()) - .imageUrl(requestDto.getImageUrl()) - .status(MembershipFeeStatus.PENDING) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/response/MembershipFeeResponseDto.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/response/MembershipFeeResponseDto.java index d7daeb8c4..b32506e3c 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/response/MembershipFeeResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/dto/response/MembershipFeeResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.members.membershipFee.domain.MembershipFee; import page.clab.api.domain.members.membershipFee.domain.MembershipFeeStatus; import java.time.LocalDateTime; @@ -21,19 +20,4 @@ public class MembershipFeeResponseDto { private String imageUrl; private MembershipFeeStatus status; private LocalDateTime createdAt; - - public static MembershipFeeResponseDto toDto(MembershipFee membershipFee, String memberName, boolean isAdminRole) { - return MembershipFeeResponseDto.builder() - .id(membershipFee.getId()) - .memberId(membershipFee.getMemberId()) - .memberName(memberName) - .category(membershipFee.getCategory()) - .account(isAdminRole ? membershipFee.getAccount() : null) - .amount(membershipFee.getAmount()) - .content(membershipFee.getContent()) - .imageUrl(membershipFee.getImageUrl()) - .status(membershipFee.getStatus()) - .createdAt(membershipFee.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/port/out/RetrieveMembershipFeePort.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/port/out/RetrieveMembershipFeePort.java index 8f19a6281..bd5d5ce94 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/port/out/RetrieveMembershipFeePort.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/port/out/RetrieveMembershipFeePort.java @@ -7,7 +7,7 @@ public interface RetrieveMembershipFeePort { - MembershipFee findByIdOrThrow(Long id); + MembershipFee getById(Long id); Page findAllByIsDeletedTrue(Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/DeletedMembershipFeesRetrievalService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/DeletedMembershipFeesRetrievalService.java index d8c9fba02..70baa3246 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/DeletedMembershipFeesRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/DeletedMembershipFeesRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; +import page.clab.api.domain.members.membershipFee.application.dto.mapper.MembershipFeeDtoMapper; import page.clab.api.domain.members.membershipFee.application.dto.response.MembershipFeeResponseDto; import page.clab.api.domain.members.membershipFee.application.port.in.RetrieveDeletedMembershipFeesUseCase; import page.clab.api.domain.members.membershipFee.application.port.out.RetrieveMembershipFeePort; @@ -19,6 +20,7 @@ public class DeletedMembershipFeesRetrievalService implements RetrieveDeletedMem private final RetrieveMembershipFeePort retrieveMembershipFeePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final MembershipFeeDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -26,6 +28,6 @@ public PagedResponseDto retrieveDeletedMembershipFees( MemberDetailedInfoDto memberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); Page membershipFees = retrieveMembershipFeePort.findAllByIsDeletedTrue(pageable); return new PagedResponseDto<>(membershipFees.map(membershipFee -> - MembershipFeeResponseDto.toDto(membershipFee, memberInfo.getMemberName(), memberInfo.isAdminRole()))); + mapper.toDto(membershipFee, memberInfo.getMemberName(), memberInfo.isAdminRole()))); } } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java index 933e4b700..533b7d879 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java @@ -1,17 +1,20 @@ package page.clab.api.domain.members.membershipFee.application.service; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.membershipFee.application.dto.mapper.MembershipFeeDtoMapper; import page.clab.api.domain.members.membershipFee.application.dto.request.MembershipFeeRequestDto; import page.clab.api.domain.members.membershipFee.application.port.in.RegisterMembershipFeeUseCase; import page.clab.api.domain.members.membershipFee.application.port.out.RegisterMembershipFeePort; import page.clab.api.domain.members.membershipFee.domain.MembershipFee; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SlackMembershipFeeInfo; +import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType; @Service @RequiredArgsConstructor @@ -20,16 +23,19 @@ public class MembershipFeeRegisterService implements RegisterMembershipFeeUseCas private final RegisterMembershipFeePort registerMembershipFeePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalSendNotificationUseCase externalSendNotificationUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; + private final MembershipFeeDtoMapper mapper; @Transactional @Override public Long registerMembershipFee(MembershipFeeRequestDto requestDto) { MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo(); - MembershipFee membershipFee = MembershipFeeRequestDto.toEntity(requestDto, memberInfo.getMemberId()); + MembershipFee membershipFee = mapper.fromDto(requestDto, memberInfo.getMemberId()); externalSendNotificationUseCase.sendNotificationToAdmins("새로운 회비 내역이 등록되었습니다."); - SlackMembershipFeeInfo membershipFeeInfo = SlackMembershipFeeInfo.create(membershipFee, memberInfo); - slackService.sendNewMembershipFeeNotification(membershipFeeInfo); + MembershipFeeNotificationInfo membershipFeeInfo = MembershipFeeNotificationInfo.create(membershipFee, + memberInfo); + eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_MEMBERSHIP_FEE, null, + membershipFeeInfo)); return registerMembershipFeePort.save(membershipFee).getId(); } } diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRemoveService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRemoveService.java index 20c88f078..f9c5d6624 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRemoveService.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRemoveService.java @@ -23,7 +23,7 @@ public class MembershipFeeRemoveService implements RemoveMembershipFeeUseCase { @Override public Long removeMembershipFee(Long membershipFeeId) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - MembershipFee membershipFee = retrieveMembershipFeePort.findByIdOrThrow(membershipFeeId); + MembershipFee membershipFee = retrieveMembershipFeePort.getById(membershipFeeId); membershipFee.validateAccessPermission(currentMemberInfo); membershipFee.delete(); registerMembershipFeePort.save(membershipFee); diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeUpdateService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeUpdateService.java index 92b981f87..a5bb7c95f 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeUpdateService.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeUpdateService.java @@ -24,7 +24,7 @@ public class MembershipFeeUpdateService implements UpdateMembershipFeeUseCase { @Override public Long updateMembershipFee(Long membershipFeeId, MembershipFeeUpdateRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - MembershipFee membershipFee = retrieveMembershipFeePort.findByIdOrThrow(membershipFeeId); + MembershipFee membershipFee = retrieveMembershipFeePort.getById(membershipFeeId); membershipFee.validateAccessPermission(currentMemberInfo); membershipFee.update(requestDto); return updateMembershipFeePort.update(membershipFee).getId(); diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeesByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeesByConditionsRetrievalService.java index 2175af583..153e9ccdb 100644 --- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeesByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeesByConditionsRetrievalService.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; +import page.clab.api.domain.members.membershipFee.application.dto.mapper.MembershipFeeDtoMapper; import page.clab.api.domain.members.membershipFee.application.dto.response.MembershipFeeResponseDto; import page.clab.api.domain.members.membershipFee.application.port.in.RetrieveMembershipFeesByConditionsUseCase; import page.clab.api.domain.members.membershipFee.application.port.out.RetrieveMembershipFeePort; @@ -20,6 +21,7 @@ public class MembershipFeesByConditionsRetrievalService implements RetrieveMembe private final RetrieveMembershipFeePort retrieveMembershipFeePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final MembershipFeeDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -31,6 +33,6 @@ public PagedResponseDto retrieveMembershipFees(String private MembershipFeeResponseDto getMembershipFeeResponseDto(MembershipFee membershipFee, boolean isAdminRole) { MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getMemberBasicInfoById(membershipFee.getMemberId()); - return MembershipFeeResponseDto.toDto(membershipFee, memberInfo.getMemberName(), isAdminRole); + return mapper.toDto(membershipFee, memberInfo.getMemberName(), isAdminRole); } } diff --git a/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductMapper.java b/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductMapper.java index fa91004e4..3af75b58c 100644 --- a/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductMapper.java +++ b/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface ProductMapper { - ProductJpaEntity toJpaEntity(Product product); + ProductJpaEntity toEntity(Product product); - Product toDomainEntity(ProductJpaEntity jpaEntity); + Product toDomain(ProductJpaEntity jpaEntity); } diff --git a/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductPersistenceAdapter.java b/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductPersistenceAdapter.java index 98784c6aa..744508665 100644 --- a/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductPersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/members/product/adapter/out/persistence/ProductPersistenceAdapter.java @@ -22,34 +22,34 @@ public class ProductPersistenceAdapter implements @Override public Product save(Product product) { - ProductJpaEntity entity = mapper.toJpaEntity(product); + ProductJpaEntity entity = mapper.toEntity(product); ProductJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override public Product update(Product product) { - ProductJpaEntity entity = mapper.toJpaEntity(product); + ProductJpaEntity entity = mapper.toEntity(product); ProductJpaEntity updatedEntity = repository.save(entity); - return mapper.toDomainEntity(updatedEntity); + return mapper.toDomain(updatedEntity); } @Override - public Product findByIdOrThrow(Long productId) { + public Product getById(Long productId) { return repository.findById(productId) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[Product] id: " + productId + "에 해당하는 상품이 존재하지 않습니다.")); } @Override public Page findAllByIsDeletedTrue(Pageable pageable) { return repository.findAllByIsDeletedTrue(pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public Page findByConditions(String productName, Pageable pageable) { return repository.findByConditions(productName, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/members/product/application/dto/mapper/ProductDtoMapper.java b/src/main/java/page/clab/api/domain/members/product/application/dto/mapper/ProductDtoMapper.java new file mode 100644 index 000000000..503fa73e4 --- /dev/null +++ b/src/main/java/page/clab/api/domain/members/product/application/dto/mapper/ProductDtoMapper.java @@ -0,0 +1,29 @@ +package page.clab.api.domain.members.product.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.members.product.application.dto.request.ProductRequestDto; +import page.clab.api.domain.members.product.application.dto.response.ProductResponseDto; +import page.clab.api.domain.members.product.domain.Product; + +@Component +public class ProductDtoMapper { + + public Product fromDto(ProductRequestDto requestDto) { + return Product.builder() + .name(requestDto.getName()) + .description(requestDto.getDescription()) + .url(requestDto.getUrl()) + .isDeleted(false) + .build(); + } + + public ProductResponseDto toDto(Product product) { + return ProductResponseDto.builder() + .id(product.getId()) + .name(product.getName()) + .description(product.getDescription()) + .url(product.getUrl()) + .createdAt(product.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/members/product/application/dto/request/ProductRequestDto.java b/src/main/java/page/clab/api/domain/members/product/application/dto/request/ProductRequestDto.java index 946b05221..68e1b8d94 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/dto/request/ProductRequestDto.java +++ b/src/main/java/page/clab/api/domain/members/product/application/dto/request/ProductRequestDto.java @@ -4,7 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.members.product.domain.Product; @Getter @Setter @@ -20,13 +19,4 @@ public class ProductRequestDto { @Schema(description = "URL", example = "https://github.com/KGU-C-Lab/petmily-server") private String url; - - public static Product toEntity(ProductRequestDto requestDto) { - return Product.builder() - .name(requestDto.getName()) - .description(requestDto.getDescription()) - .url(requestDto.getUrl()) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/product/application/dto/response/ProductResponseDto.java b/src/main/java/page/clab/api/domain/members/product/application/dto/response/ProductResponseDto.java index aa7422aaa..d52a22d16 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/dto/response/ProductResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/product/application/dto/response/ProductResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.members.product.domain.Product; import java.time.LocalDateTime; @@ -15,14 +14,4 @@ public class ProductResponseDto { private String description; private String url; private LocalDateTime createdAt; - - public static ProductResponseDto toDto(Product product) { - return ProductResponseDto.builder() - .id(product.getId()) - .name(product.getName()) - .description(product.getDescription()) - .url(product.getUrl()) - .createdAt(product.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/product/application/port/out/RetrieveProductPort.java b/src/main/java/page/clab/api/domain/members/product/application/port/out/RetrieveProductPort.java index 4ec607df4..d42e23c1b 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/port/out/RetrieveProductPort.java +++ b/src/main/java/page/clab/api/domain/members/product/application/port/out/RetrieveProductPort.java @@ -6,7 +6,7 @@ public interface RetrieveProductPort { - Product findByIdOrThrow(Long productId); + Product getById(Long productId); Page findAllByIsDeletedTrue(Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/members/product/application/service/DeletedProductsRetrievalService.java b/src/main/java/page/clab/api/domain/members/product/application/service/DeletedProductsRetrievalService.java index 7fa94112a..d305afd3c 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/service/DeletedProductsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/product/application/service/DeletedProductsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.product.application.dto.mapper.ProductDtoMapper; import page.clab.api.domain.members.product.application.dto.response.ProductResponseDto; import page.clab.api.domain.members.product.application.port.in.RetrieveDeletedProductsUseCase; import page.clab.api.domain.members.product.application.port.out.RetrieveProductPort; @@ -16,11 +17,12 @@ public class DeletedProductsRetrievalService implements RetrieveDeletedProductsUseCase { private final RetrieveProductPort retrieveProductPort; + private final ProductDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveDeletedProducts(Pageable pageable) { Page products = retrieveProductPort.findAllByIsDeletedTrue(pageable); - return new PagedResponseDto<>(products.map(ProductResponseDto::toDto)); + return new PagedResponseDto<>(products.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/product/application/service/ProductRegisterService.java b/src/main/java/page/clab/api/domain/members/product/application/service/ProductRegisterService.java index 0dbbb8628..d94d86db5 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/service/ProductRegisterService.java +++ b/src/main/java/page/clab/api/domain/members/product/application/service/ProductRegisterService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.product.application.dto.mapper.ProductDtoMapper; import page.clab.api.domain.members.product.application.dto.request.ProductRequestDto; import page.clab.api.domain.members.product.application.port.in.RegisterProductUseCase; import page.clab.api.domain.members.product.application.port.out.RegisterProductPort; @@ -13,11 +14,12 @@ public class ProductRegisterService implements RegisterProductUseCase { private final RegisterProductPort registerProductPort; + private final ProductDtoMapper mapper; @Transactional @Override public Long registerProduct(ProductRequestDto requestDto) { - Product product = ProductRequestDto.toEntity(requestDto); + Product product = mapper.fromDto(requestDto); return registerProductPort.save(product).getId(); } } diff --git a/src/main/java/page/clab/api/domain/members/product/application/service/ProductRemoveService.java b/src/main/java/page/clab/api/domain/members/product/application/service/ProductRemoveService.java index 1114f7b78..e97d52114 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/service/ProductRemoveService.java +++ b/src/main/java/page/clab/api/domain/members/product/application/service/ProductRemoveService.java @@ -18,7 +18,7 @@ public class ProductRemoveService implements RemoveProductUseCase { @Transactional @Override public Long removeProduct(Long productId) { - Product product = retrieveProductPort.findByIdOrThrow(productId); + Product product = retrieveProductPort.getById(productId); product.delete(); return updateProductPort.update(product).getId(); } diff --git a/src/main/java/page/clab/api/domain/members/product/application/service/ProductUpdateService.java b/src/main/java/page/clab/api/domain/members/product/application/service/ProductUpdateService.java index a550d706a..83b570e27 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/service/ProductUpdateService.java +++ b/src/main/java/page/clab/api/domain/members/product/application/service/ProductUpdateService.java @@ -19,7 +19,7 @@ public class ProductUpdateService implements UpdateProductUseCase { @Transactional @Override public Long updateProduct(Long productId, ProductUpdateRequestDto requestDto) { - Product product = retrieveProductPort.findByIdOrThrow(productId); + Product product = retrieveProductPort.getById(productId); product.update(requestDto); return updateProductPort.update(product).getId(); } diff --git a/src/main/java/page/clab/api/domain/members/product/application/service/ProductsByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/members/product/application/service/ProductsByConditionsRetrievalService.java index 91431f241..bb5a1482c 100644 --- a/src/main/java/page/clab/api/domain/members/product/application/service/ProductsByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/product/application/service/ProductsByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.product.application.dto.mapper.ProductDtoMapper; import page.clab.api.domain.members.product.application.dto.response.ProductResponseDto; import page.clab.api.domain.members.product.application.port.in.RetrieveProductsByConditionsUseCase; import page.clab.api.domain.members.product.application.port.out.RetrieveProductPort; @@ -16,11 +17,12 @@ public class ProductsByConditionsRetrievalService implements RetrieveProductsByConditionsUseCase { private final RetrieveProductPort retrieveProductPort; + private final ProductDtoMapper mapper; @Transactional(readOnly = true) @Override public PagedResponseDto retrieveProducts(String productName, Pageable pageable) { Page products = retrieveProductPort.findByConditions(productName, pageable); - return new PagedResponseDto<>(products.map(ProductResponseDto::toDto)); + return new PagedResponseDto<>(products.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleMapper.java b/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleMapper.java index b2b0cbcc6..4fd541eb7 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleMapper.java +++ b/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleMapper.java @@ -6,7 +6,7 @@ @Mapper(componentModel = "spring") public interface ScheduleMapper { - ScheduleJpaEntity toJpaEntity(Schedule schedule); + ScheduleJpaEntity toEntity(Schedule schedule); - Schedule toDomainEntity(ScheduleJpaEntity entity); + Schedule toDomain(ScheduleJpaEntity entity); } diff --git a/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/SchedulePersistenceAdapter.java b/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/SchedulePersistenceAdapter.java index 86b1106a9..012102629 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/SchedulePersistenceAdapter.java +++ b/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/SchedulePersistenceAdapter.java @@ -26,22 +26,22 @@ public class SchedulePersistenceAdapter implements @Override public Schedule save(Schedule schedule) { - ScheduleJpaEntity entity = mapper.toJpaEntity(schedule); + ScheduleJpaEntity entity = mapper.toEntity(schedule); ScheduleJpaEntity savedEntity = repository.save(entity); - return mapper.toDomainEntity(savedEntity); + return mapper.toDomain(savedEntity); } @Override - public Schedule findByIdOrThrow(Long id) { + public Schedule getById(Long id) { return repository.findById(id) - .map(mapper::toDomainEntity) + .map(mapper::toDomain) .orElseThrow(() -> new NotFoundException("[Schedule] id: " + id + "에 해당하는 스케줄이 존재하지 않습니다.")); } @Override public Page findActivitySchedulesByDateRangeAndMemberId(LocalDate startDate, LocalDate endDate, String memberId, Pageable pageable) { return repository.findActivitySchedulesByDateRangeAndMemberId(startDate, endDate, memberId, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override @@ -52,12 +52,12 @@ public ScheduleCollectResponseDto findCollectSchedules() { @Override public Page findByConditions(Integer year, Integer month, SchedulePriority priority, Pageable pageable) { return repository.findByConditions(year, month, priority, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } @Override public Page findByDateRangeAndMember(LocalDate startDate, LocalDate endDate, List myGroups, Pageable pageable) { return repository.findByDateRangeAndMember(startDate, endDate, myGroups, pageable) - .map(mapper::toDomainEntity); + .map(mapper::toDomain); } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleRepositoryImpl.java b/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleRepositoryImpl.java index 87a89e500..98b40e471 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleRepositoryImpl.java +++ b/src/main/java/page/clab/api/domain/members/schedule/adapter/out/persistence/ScheduleRepositoryImpl.java @@ -8,6 +8,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; +import page.clab.api.domain.members.schedule.application.dto.mapper.ScheduleDtoMapper; import page.clab.api.domain.members.schedule.application.dto.response.ScheduleCollectResponseDto; import page.clab.api.domain.members.schedule.domain.SchedulePriority; import page.clab.api.domain.members.schedule.domain.ScheduleType; @@ -22,6 +23,7 @@ public class ScheduleRepositoryImpl implements ScheduleRepositoryCustom { private final JPAQueryFactory queryFactory; + private final ScheduleDtoMapper mapper; @Override public Page findByDateRangeAndMember(LocalDate startDate, LocalDate endDate, List myGroups, Pageable pageable) { @@ -132,6 +134,6 @@ public ScheduleCollectResponseDto findCollectSchedules() { .where(builder) .fetchCount(); - return ScheduleCollectResponseDto.toDto(total, highPriorityCount); + return mapper.of(total, highPriorityCount); } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/dto/mapper/ScheduleDtoMapper.java b/src/main/java/page/clab/api/domain/members/schedule/application/dto/mapper/ScheduleDtoMapper.java new file mode 100644 index 000000000..5c462192a --- /dev/null +++ b/src/main/java/page/clab/api/domain/members/schedule/application/dto/mapper/ScheduleDtoMapper.java @@ -0,0 +1,45 @@ +package page.clab.api.domain.members.schedule.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; +import page.clab.api.domain.members.schedule.application.dto.request.ScheduleRequestDto; +import page.clab.api.domain.members.schedule.application.dto.response.ScheduleCollectResponseDto; +import page.clab.api.domain.members.schedule.application.dto.response.ScheduleResponseDto; +import page.clab.api.domain.members.schedule.domain.Schedule; + +@Component +public class ScheduleDtoMapper { + + public Schedule fromDto(ScheduleRequestDto requestDto, String memberId, ActivityGroup activityGroup) { + return Schedule.builder() + .scheduleType(requestDto.getScheduleType()) + .title(requestDto.getTitle()) + .detail(requestDto.getDetail()) + .startDateTime(requestDto.getStartDateTime()) + .endDateTime(requestDto.getEndDateTime()) + .priority(requestDto.getPriority()) + .scheduleWriter(memberId) + .activityGroup(activityGroup) + .isDeleted(false) + .build(); + } + + public ScheduleResponseDto toDto(Schedule schedule) { + return ScheduleResponseDto.builder() + .id(schedule.getId()) + .title(schedule.getTitle()) + .detail(schedule.getDetail()) + .activityName(schedule.isAllSchedule() ? null : schedule.getActivityGroup().getName()) + .startDateTime(schedule.getStartDateTime()) + .endDateTime(schedule.getEndDateTime()) + .priority(schedule.getPriority()) + .build(); + } + + public ScheduleCollectResponseDto of(Long totalScheduleCount, Long totalEventCount) { + return ScheduleCollectResponseDto.builder() + .totalScheduleCount(totalScheduleCount) + .totalEventCount(totalEventCount) + .build(); + } +} diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/dto/request/ScheduleRequestDto.java b/src/main/java/page/clab/api/domain/members/schedule/application/dto/request/ScheduleRequestDto.java index 1bd14be48..be6383f23 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/dto/request/ScheduleRequestDto.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/dto/request/ScheduleRequestDto.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.Setter; -import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; -import page.clab.api.domain.members.schedule.domain.Schedule; import page.clab.api.domain.members.schedule.domain.SchedulePriority; import page.clab.api.domain.members.schedule.domain.ScheduleType; @@ -40,18 +38,4 @@ public class ScheduleRequestDto { private SchedulePriority priority; private Long activityGroupId; - - public static Schedule toEntity(ScheduleRequestDto requestDto, String memberId, ActivityGroup activityGroup) { - return Schedule.builder() - .scheduleType(requestDto.getScheduleType()) - .title(requestDto.getTitle()) - .detail(requestDto.getDetail()) - .startDateTime(requestDto.getStartDateTime()) - .endDateTime(requestDto.getEndDateTime()) - .priority(requestDto.getPriority()) - .scheduleWriter(memberId) - .activityGroup(activityGroup) - .isDeleted(false) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleCollectResponseDto.java b/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleCollectResponseDto.java index 186505449..fac642d8a 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleCollectResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleCollectResponseDto.java @@ -9,11 +9,4 @@ public class ScheduleCollectResponseDto { private Long totalScheduleCount; private Long totalEventCount; - - public static ScheduleCollectResponseDto toDto(Long totalScheduleCount, Long totalEventCount) { - return ScheduleCollectResponseDto.builder() - .totalScheduleCount(totalScheduleCount) - .totalEventCount(totalEventCount) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleResponseDto.java b/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleResponseDto.java index bc34c6a5b..a4e037a2c 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleResponseDto.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/dto/response/ScheduleResponseDto.java @@ -2,7 +2,6 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.domain.members.schedule.domain.Schedule; import page.clab.api.domain.members.schedule.domain.SchedulePriority; import java.time.LocalDateTime; @@ -18,16 +17,4 @@ public class ScheduleResponseDto { private LocalDateTime startDateTime; private LocalDateTime endDateTime; private SchedulePriority priority; - - public static ScheduleResponseDto toDto(Schedule schedule) { - return ScheduleResponseDto.builder() - .id(schedule.getId()) - .title(schedule.getTitle()) - .detail(schedule.getDetail()) - .activityName(schedule.isAllSchedule() ? null : schedule.getActivityGroup().getName()) - .startDateTime(schedule.getStartDateTime()) - .endDateTime(schedule.getEndDateTime()) - .priority(schedule.getPriority()) - .build(); - } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/port/out/RetrieveSchedulePort.java b/src/main/java/page/clab/api/domain/members/schedule/application/port/out/RetrieveSchedulePort.java index cea0c45ba..51f46c649 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/port/out/RetrieveSchedulePort.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/port/out/RetrieveSchedulePort.java @@ -12,7 +12,7 @@ public interface RetrieveSchedulePort { - Schedule findByIdOrThrow(Long id); + Schedule getById(Long id); Page findByConditions(Integer year, Integer month, SchedulePriority priority, Pageable pageable); diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/service/ActivitySchedulesRetrievalService.java b/src/main/java/page/clab/api/domain/members/schedule/application/service/ActivitySchedulesRetrievalService.java index 43d5de211..2cf8198b5 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/service/ActivitySchedulesRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/service/ActivitySchedulesRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.schedule.application.dto.mapper.ScheduleDtoMapper; import page.clab.api.domain.members.schedule.application.dto.response.ScheduleResponseDto; import page.clab.api.domain.members.schedule.application.port.in.RetrieveActivitySchedulesUseCase; import page.clab.api.domain.members.schedule.application.port.out.RetrieveSchedulePort; @@ -20,12 +21,13 @@ public class ActivitySchedulesRetrievalService implements RetrieveActivitySchedu private final RetrieveSchedulePort retrieveSchedulePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; + private final ScheduleDtoMapper mapper; @Override @Transactional(readOnly = true) public PagedResponseDto retrieveActivitySchedules(LocalDate startDate, LocalDate endDate, Pageable pageable) { String currentMemberId = externalRetrieveMemberUseCase.getCurrentMemberId(); Page schedules = retrieveSchedulePort.findActivitySchedulesByDateRangeAndMemberId(startDate, endDate, currentMemberId, pageable); - return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); + return new PagedResponseDto<>(schedules.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRegisterService.java b/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRegisterService.java index 0560954c3..cdac8c5d4 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRegisterService.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRegisterService.java @@ -10,6 +10,7 @@ import page.clab.api.domain.activity.activitygroup.domain.ActivityGroupRole; import page.clab.api.domain.activity.activitygroup.domain.GroupMember; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; +import page.clab.api.domain.members.schedule.application.dto.mapper.ScheduleDtoMapper; import page.clab.api.domain.members.schedule.application.dto.request.ScheduleRequestDto; import page.clab.api.domain.members.schedule.application.port.in.RegisterScheduleUseCase; import page.clab.api.domain.members.schedule.application.port.out.RegisterSchedulePort; @@ -29,13 +30,14 @@ public class ScheduleRegisterService implements RegisterScheduleUseCase { private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ActivityGroupMemberService activityGroupMemberService; private final ActivityGroupAdminService activityGroupAdminService; + private final ScheduleDtoMapper mapper; @Override @Transactional public Long registerSchedule(ScheduleRequestDto requestDto) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); ActivityGroup activityGroup = resolveActivityGroupForSchedule(requestDto, currentMemberInfo); - Schedule schedule = ScheduleRequestDto.toEntity(requestDto, currentMemberInfo.getMemberId(), activityGroup); + Schedule schedule = mapper.fromDto(requestDto, currentMemberInfo.getMemberId(), activityGroup); schedule.validateAccessPermissionForCreation(currentMemberInfo); schedule.validateBusinessRules(); return registerSchedulePort.save(schedule).getId(); @@ -47,7 +49,7 @@ private ActivityGroup resolveActivityGroupForSchedule(ScheduleRequestDto request if (!scheduleType.equals(ScheduleType.ALL)) { Long activityGroupId = Optional.ofNullable(requestDto.getActivityGroupId()) .orElseThrow(() -> new NullPointerException("스터디 또는 프로젝트 일정은 그룹 id를 입력해야 합니다.")); - activityGroup = activityGroupAdminService.getActivityGroupByIdOrThrow(activityGroupId); + activityGroup = activityGroupAdminService.getActivityGroupById(activityGroupId); validateMemberIsGroupLeaderOrAdmin(memberInfo, activityGroup); } return activityGroup; diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRemoveService.java b/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRemoveService.java index e3c718310..19ac86b46 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRemoveService.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/service/ScheduleRemoveService.java @@ -23,7 +23,7 @@ public class ScheduleRemoveService implements RemoveScheduleUseCase { @Transactional public Long removeSchedule(Long scheduleId) throws PermissionDeniedException { MemberDetailedInfoDto currentMemberInfo = externalRetrieveMemberUseCase.getCurrentMemberDetailedInfo(); - Schedule schedule = retrieveSchedulePort.findByIdOrThrow(scheduleId); + Schedule schedule = retrieveSchedulePort.getById(scheduleId); schedule.validateAccessPermission(currentMemberInfo); schedule.delete(); return registerSchedulePort.save(schedule).getId(); diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesByConditionsRetrievalService.java b/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesByConditionsRetrievalService.java index e18fe7074..5018d061f 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesByConditionsRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesByConditionsRetrievalService.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.members.schedule.application.dto.mapper.ScheduleDtoMapper; import page.clab.api.domain.members.schedule.application.dto.response.ScheduleResponseDto; import page.clab.api.domain.members.schedule.application.port.in.RetrieveSchedulesByConditionsUseCase; import page.clab.api.domain.members.schedule.application.port.out.RetrieveSchedulePort; @@ -17,11 +18,12 @@ public class SchedulesByConditionsRetrievalService implements RetrieveSchedulesByConditionsUseCase { private final RetrieveSchedulePort retrieveSchedulePort; + private final ScheduleDtoMapper mapper; @Override @Transactional(readOnly = true) public PagedResponseDto retrieveSchedules(Integer year, Integer month, SchedulePriority priority, Pageable pageable) { Page schedules = retrieveSchedulePort.findByConditions(year, month, priority, pageable); - return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); + return new PagedResponseDto<>(schedules.map(mapper::toDto)); } } diff --git a/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesWithinDateRangeRetrievalService.java b/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesWithinDateRangeRetrievalService.java index 13ee060fd..dc1d4547e 100644 --- a/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesWithinDateRangeRetrievalService.java +++ b/src/main/java/page/clab/api/domain/members/schedule/application/service/SchedulesWithinDateRangeRetrievalService.java @@ -8,6 +8,7 @@ import page.clab.api.domain.activity.activitygroup.application.ActivityGroupMemberService; import page.clab.api.domain.activity.activitygroup.domain.ActivityGroup; import page.clab.api.domain.activity.activitygroup.domain.GroupMember; +import page.clab.api.domain.members.schedule.application.dto.mapper.ScheduleDtoMapper; import page.clab.api.domain.members.schedule.application.dto.response.ScheduleResponseDto; import page.clab.api.domain.members.schedule.application.port.in.RetrieveSchedulesWithinDateRangeUseCase; import page.clab.api.domain.members.schedule.application.port.out.RetrieveSchedulePort; @@ -26,6 +27,7 @@ public class SchedulesWithinDateRangeRetrievalService implements RetrieveSchedul private final RetrieveSchedulePort retrieveSchedulePort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ActivityGroupMemberService activityGroupMemberService; + private final ScheduleDtoMapper mapper; @Override @Transactional(readOnly = true) @@ -34,7 +36,7 @@ public PagedResponseDto retrieveSchedulesWithinDateRange(Lo List groupMembers = activityGroupMemberService.getGroupMemberByMemberId(currentMemberId); List myGroups = getMyActivityGroups(groupMembers); Page schedules = retrieveSchedulePort.findByDateRangeAndMember(startDate, endDate, myGroups, pageable); - return new PagedResponseDto<>(schedules.map(ScheduleResponseDto::toDto)); + return new PagedResponseDto<>(schedules.map(mapper::toDto)); } private List getMyActivityGroups(List groupMembers) { diff --git a/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java b/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java index 9ed361f32..436c68313 100644 --- a/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java +++ b/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.accountLockInfo.application.port.out.RegisterAccountLockInfoPort; @@ -13,8 +14,8 @@ import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; import page.clab.api.external.auth.accountLockInfo.application.ExternalManageAccountLockUseCase; import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -23,7 +24,7 @@ public class ExternalAccountLockManagementService implements ExternalManageAccou private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort; private final RegisterAccountLockInfoPort registerAccountLockInfoPort; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; @Value("${security.login-attempt.max-failures}") private int maxLoginFailures; @@ -31,6 +32,16 @@ public class ExternalAccountLockManagementService implements ExternalManageAccou @Value("${security.login-attempt.lock-duration-minutes}") private int lockDurationMinutes; + /** + * 계정 잠금 정보를 처리합니다. + * + *

해당 멤버가 존재하는지 확인하고, 계정 잠금 상태를 검증한 후, + * 계정을 잠금 해제하고 업데이트된 계정 잠금 정보를 저장합니다.

+ * + * @param memberId 잠금 해제하려는 멤버의 ID + * @throws MemberLockedException 계정이 현재 잠겨 있을 경우 예외 발생 + * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생 + */ @Transactional @Override public void handleAccountLockInfo(String memberId) throws MemberLockedException, LoginFailedException { @@ -41,9 +52,21 @@ public void handleAccountLockInfo(String memberId) throws MemberLockedException, registerAccountLockInfoPort.save(accountLockInfo); } + /** + * 로그인 실패를 처리하고 계정 잠금을 관리합니다. + * + *

로그인 실패 시 멤버의 존재 여부와 계정 잠금 상태를 확인합니다. + * 로그인 실패 횟수를 증가시키며, 설정된 최대 실패 횟수에 도달하면 계정을 잠그고 Slack에 보안 알림을 전송합니다.

+ * + * @param request 현재 HTTP 요청 객체 + * @param memberId 로그인 실패를 기록할 멤버의 ID + * @throws MemberLockedException 계정이 현재 잠겨 있을 경우 예외 발생 + * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생 + */ @Transactional @Override - public void handleLoginFailure(HttpServletRequest request, String memberId) throws MemberLockedException, LoginFailedException { + public void handleLoginFailure(HttpServletRequest request, String memberId) + throws MemberLockedException, LoginFailedException { ensureMemberExists(memberId); AccountLockInfo accountLockInfo = ensureAccountLockInfo(memberId); validateAccountLockStatus(accountLockInfo); @@ -77,7 +100,10 @@ private void sendSlackLoginFailureNotification(HttpServletRequest request, Strin String memberName = memberInfo.getMemberName(); if (memberInfo.isAdminRole()) { request.setAttribute("member", memberId + " " + memberName); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.REPEATED_LOGIN_FAILURES, "로그인 실패 횟수 초과로 계정이 잠겼습니다."); + String repeatedLoginFailuresMessage = "로그인 실패 횟수 초과로 계정이 잠겼습니다."; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.REPEATED_LOGIN_FAILURES, request, + repeatedLoginFailuresMessage)); } } } diff --git a/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java b/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java index a600c8b3d..61ac9a980 100644 --- a/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java +++ b/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java @@ -4,14 +4,15 @@ import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RegisterIpAccessMonitorPort; import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RetrieveIpAccessMonitorPort; import page.clab.api.domain.auth.redisIpAccessMonitor.domain.RedisIpAccessMonitor; import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalRegisterIpAccessMonitorUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; @Service @RequiredArgsConstructor @@ -19,7 +20,7 @@ public class ExternalIpAccessMonitorRegisterService implements ExternalRegisterI private final RegisterIpAccessMonitorPort registerIpAccessMonitorPort; private final RetrieveIpAccessMonitorPort retrieveIpAccessMonitorPort; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; @Value("${security.ip-attempt.max-attempts}") private int maxAttempts; @@ -29,7 +30,10 @@ public class ExternalIpAccessMonitorRegisterService implements ExternalRegisterI public void registerIpAccessMonitor(HttpServletRequest request, String ipAddress) { RedisIpAccessMonitor redisIpAccessMonitor = getOrCreateRedisIpAccessMonitor(ipAddress); if (redisIpAccessMonitor.isBlocked()) { - slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_BLOCKED, "Blocked IP: " + ipAddress); + String abnormalAccessIpBlockedMessage = "Blocked IP: " + ipAddress; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_BLOCKED, request, + abnormalAccessIpBlockedMessage)); } registerIpAccessMonitorPort.save(redisIpAccessMonitor); } diff --git a/src/main/java/page/clab/api/external/community/board/application/port/ExternalRetrieveBoardUseCase.java b/src/main/java/page/clab/api/external/community/board/application/port/ExternalRetrieveBoardUseCase.java index 6fbf64d87..1a95beb52 100644 --- a/src/main/java/page/clab/api/external/community/board/application/port/ExternalRetrieveBoardUseCase.java +++ b/src/main/java/page/clab/api/external/community/board/application/port/ExternalRetrieveBoardUseCase.java @@ -5,7 +5,7 @@ public interface ExternalRetrieveBoardUseCase { - Board findByIdOrThrow(Long targetId); + Board getById(Long targetId); BoardCommentInfoDto getBoardCommentInfoById(Long boardId); } diff --git a/src/main/java/page/clab/api/external/community/board/application/service/ExternalBoardRetrievalService.java b/src/main/java/page/clab/api/external/community/board/application/service/ExternalBoardRetrievalService.java index 012e16260..b34c79852 100644 --- a/src/main/java/page/clab/api/external/community/board/application/service/ExternalBoardRetrievalService.java +++ b/src/main/java/page/clab/api/external/community/board/application/service/ExternalBoardRetrievalService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper; import page.clab.api.domain.community.board.application.dto.shared.BoardCommentInfoDto; import page.clab.api.domain.community.board.application.port.out.RetrieveBoardPort; import page.clab.api.domain.community.board.application.service.BoardRetrievalService; @@ -15,17 +16,18 @@ public class ExternalBoardRetrievalService implements ExternalRetrieveBoardUseCa private final BoardRetrievalService boardRetrievalService; private final RetrieveBoardPort retrieveBoardPort; + private final BoardDtoMapper mapper; @Transactional(readOnly = true) @Override - public Board findByIdOrThrow(Long targetId) { - return boardRetrievalService.findByIdOrThrow(targetId); + public Board getById(Long targetId) { + return boardRetrievalService.getById(targetId); } @Transactional(readOnly = true) @Override public BoardCommentInfoDto getBoardCommentInfoById(Long boardId) { Board board = retrieveBoardPort.findByIdRegardlessOfDeletion(boardId); - return BoardCommentInfoDto.create(board); + return mapper.toDto(board); } } diff --git a/src/main/java/page/clab/api/external/community/comment/application/port/ExternalRetrieveCommentUseCase.java b/src/main/java/page/clab/api/external/community/comment/application/port/ExternalRetrieveCommentUseCase.java index c076f3ff1..230046093 100644 --- a/src/main/java/page/clab/api/external/community/comment/application/port/ExternalRetrieveCommentUseCase.java +++ b/src/main/java/page/clab/api/external/community/comment/application/port/ExternalRetrieveCommentUseCase.java @@ -4,7 +4,7 @@ public interface ExternalRetrieveCommentUseCase { - Comment findByIdOrThrow(Long targetId); + Comment getById(Long targetId); Long countByBoardId(Long id); } diff --git a/src/main/java/page/clab/api/external/community/comment/application/service/ExternalCommentRetrievalService.java b/src/main/java/page/clab/api/external/community/comment/application/service/ExternalCommentRetrievalService.java index c144df942..db149e23e 100644 --- a/src/main/java/page/clab/api/external/community/comment/application/service/ExternalCommentRetrievalService.java +++ b/src/main/java/page/clab/api/external/community/comment/application/service/ExternalCommentRetrievalService.java @@ -17,8 +17,8 @@ public class ExternalCommentRetrievalService implements ExternalRetrieveCommentU @Transactional(readOnly = true) @Override - public Comment findByIdOrThrow(Long targetId) { - return retrieveCommentUseCase.findByIdOrThrow(targetId); + public Comment getById(Long targetId) { + return retrieveCommentUseCase.getById(targetId); } @Transactional(readOnly = true) diff --git a/src/main/java/page/clab/api/external/hiring/application/application/service/ExternalRecruitmentRetrievalService.java b/src/main/java/page/clab/api/external/hiring/application/application/service/ExternalRecruitmentRetrievalService.java index 7625c856a..64a695ff3 100644 --- a/src/main/java/page/clab/api/external/hiring/application/application/service/ExternalRecruitmentRetrievalService.java +++ b/src/main/java/page/clab/api/external/hiring/application/application/service/ExternalRecruitmentRetrievalService.java @@ -16,7 +16,7 @@ public class ExternalRecruitmentRetrievalService implements ExternalRetrieveRecr @Transactional(readOnly = true) @Override public void validateRecruitmentForApplication(Long recruitmentId) { - Recruitment recruitment = retrieveRecruitmentPort.findByIdOrThrow(recruitmentId); + Recruitment recruitment = retrieveRecruitmentPort.getById(recruitmentId); recruitment.validateRecruiting(); } } diff --git a/src/main/java/page/clab/api/external/library/book/application/port/ExternalRetrieveBookUseCase.java b/src/main/java/page/clab/api/external/library/book/application/port/ExternalRetrieveBookUseCase.java index 4c1e16c39..d9a692b93 100644 --- a/src/main/java/page/clab/api/external/library/book/application/port/ExternalRetrieveBookUseCase.java +++ b/src/main/java/page/clab/api/external/library/book/application/port/ExternalRetrieveBookUseCase.java @@ -4,7 +4,7 @@ public interface ExternalRetrieveBookUseCase { - Book findByIdOrThrow(Long bookId); + Book getById(Long bookId); int countByBorrowerId(String borrowerId); } diff --git a/src/main/java/page/clab/api/external/library/book/application/service/ExternalBookRetrievalService.java b/src/main/java/page/clab/api/external/library/book/application/service/ExternalBookRetrievalService.java index 271097c70..7967657aa 100644 --- a/src/main/java/page/clab/api/external/library/book/application/service/ExternalBookRetrievalService.java +++ b/src/main/java/page/clab/api/external/library/book/application/service/ExternalBookRetrievalService.java @@ -15,8 +15,8 @@ public class ExternalBookRetrievalService implements ExternalRetrieveBookUseCase @Transactional(readOnly = true) @Override - public Book findByIdOrThrow(Long bookId) { - return retrieveBookPort.findByIdOrThrow(bookId); + public Book getById(Long bookId) { + return retrieveBookPort.getById(bookId); } @Transactional(readOnly = true) diff --git a/src/main/java/page/clab/api/external/memberManagement/member/application/port/ExternalRetrieveMemberUseCase.java b/src/main/java/page/clab/api/external/memberManagement/member/application/port/ExternalRetrieveMemberUseCase.java index afea9592c..20b5cb4db 100644 --- a/src/main/java/page/clab/api/external/memberManagement/member/application/port/ExternalRetrieveMemberUseCase.java +++ b/src/main/java/page/clab/api/external/memberManagement/member/application/port/ExternalRetrieveMemberUseCase.java @@ -20,7 +20,7 @@ public interface ExternalRetrieveMemberUseCase { Optional findById(String id); - Member findByIdOrThrow(String memberId); + Member getById(String memberId); Member findByEmail(String address); diff --git a/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberRetrievalService.java b/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberRetrievalService.java index 12ad71a3e..5dedb7289 100644 --- a/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberRetrievalService.java +++ b/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberRetrievalService.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import page.clab.api.domain.memberManagement.member.application.dto.mapper.MemberDtoMapper; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBorrowerInfoDto; import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto; @@ -28,6 +29,7 @@ public class ExternalMemberRetrievalService implements ExternalRetrieveMemberUse private final RetrieveMemberPort retrieveMemberPort; private final CheckMemberExistencePort checkMemberExistencePort; + private final MemberDtoMapper mapper; @Transactional(readOnly = true) @Override @@ -51,21 +53,21 @@ public Optional findById(String id) { @Transactional(readOnly = true) @Override - public Member findByIdOrThrow(String memberId) { - return retrieveMemberPort.findByIdOrThrow(memberId); + public Member getById(String memberId) { + return retrieveMemberPort.getById(memberId); } @Transactional(readOnly = true) @Override public Member findByEmail(String address) { - return retrieveMemberPort.findByEmailOrThrow(address); + return retrieveMemberPort.getByEmail(address); } @Transactional(readOnly = true) @Override public Member getCurrentMember() { String memberId = AuthUtil.getAuthenticationInfoMemberId(); - return retrieveMemberPort.findByIdOrThrow(memberId); + return retrieveMemberPort.getById(memberId); } @Transactional(readOnly = true) @@ -79,7 +81,7 @@ public String getCurrentMemberId() { public List getMembers() { List members = retrieveMemberPort.findAll(); return members.stream() - .map(MemberEmailInfoDto::create) + .map(mapper::toEmailInfoDto) .collect(Collectors.toList()); } @@ -115,68 +117,67 @@ public List getSuperAdminIds() { @Transactional(readOnly = true) @Override public MemberBasicInfoDto getMemberBasicInfoById(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); - return MemberBasicInfoDto.create(member); + Member member = retrieveMemberPort.getById(memberId); + return mapper.toBasicInfoDto(member); } @Transactional(readOnly = true) @Override public MemberBasicInfoDto getCurrentMemberBasicInfo() { String currentMemberId = AuthUtil.getAuthenticationInfoMemberId(); - Member member = retrieveMemberPort.findByIdOrThrow(currentMemberId); - return MemberBasicInfoDto.create(member); + Member member = retrieveMemberPort.getById(currentMemberId); + return mapper.toBasicInfoDto(member); } @Transactional(readOnly = true) @Override public MemberDetailedInfoDto getMemberDetailedInfoById(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); - return MemberDetailedInfoDto.create(member); + Member member = retrieveMemberPort.getById(memberId); + return mapper.toDetailedInfoDto(member); } @Transactional(readOnly = true) @Override public MemberDetailedInfoDto getCurrentMemberDetailedInfo() { String currentMemberId = AuthUtil.getAuthenticationInfoMemberId(); - Member member = retrieveMemberPort.findByIdOrThrow(currentMemberId); - return MemberDetailedInfoDto.create(member); + Member member = retrieveMemberPort.getById(currentMemberId); + return mapper.toDetailedInfoDto(member); } @Transactional(readOnly = true) @Override public MemberBorrowerInfoDto getCurrentMemberBorrowerInfo() { String currentMemberId = AuthUtil.getAuthenticationInfoMemberId(); - Member member = retrieveMemberPort.findByIdOrThrow(currentMemberId); - return MemberBorrowerInfoDto.create(member); + Member member = retrieveMemberPort.getById(currentMemberId); + return mapper.toBorrowerInfoDto(member); } @Transactional(readOnly = true) @Override public MemberLoginInfoDto getMemberLoginInfoById(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); - return MemberLoginInfoDto.create(member); + Member member = retrieveMemberPort.getById(memberId); + return mapper.toLoginInfoDto(member); } @Transactional(readOnly = true) @Override public MemberLoginInfoDto getGuestMemberLoginInfo() { - Member guestMember = retrieveMemberPort.findFirstByRoleOrThrow(Role.GUEST); - return MemberLoginInfoDto.create(guestMember); + Member guestMember = retrieveMemberPort.getFirstByRole(Role.GUEST); + return mapper.toLoginInfoDto(guestMember); } @Transactional(readOnly = true) @Override public MemberPositionInfoDto getCurrentMemberPositionInfo() { String currentMemberId = AuthUtil.getAuthenticationInfoMemberId(); - Member member = retrieveMemberPort.findByIdOrThrow(currentMemberId); - return MemberPositionInfoDto.create(member); + Member member = retrieveMemberPort.getById(currentMemberId); + return mapper.toPositionInfoDto(member); } @Transactional(readOnly = true) @Override public MemberReviewInfoDto getMemberReviewInfoById(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); - return MemberReviewInfoDto.create(member); + Member member = retrieveMemberPort.getById(memberId); + return mapper.toReviewInfoDto(member); } - } diff --git a/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberUpdateService.java b/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberUpdateService.java index 60117a424..e188b25a7 100644 --- a/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberUpdateService.java +++ b/src/main/java/page/clab/api/external/memberManagement/member/application/service/ExternalMemberUpdateService.java @@ -20,7 +20,7 @@ public class ExternalMemberUpdateService implements ExternalUpdateMemberUseCase @Transactional @Override public void updateLastLoginTime(String memberId) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + Member member = retrieveMemberPort.getById(memberId); member.updateLastLoginTime(); registerMemberPort.save(member); } @@ -28,7 +28,7 @@ public void updateLastLoginTime(String memberId) { @Transactional @Override public void updateLoanSuspensionDate(String memberId, LocalDateTime loanSuspensionDate) { - Member member = retrieveMemberPort.findByIdOrThrow(memberId); + Member member = retrieveMemberPort.getById(memberId); member.updateLoanSuspensionDate(loanSuspensionDate); registerMemberPort.save(member); } diff --git a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java index c3511a6b9..ed2287c8e 100644 --- a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java @@ -4,8 +4,11 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Base64; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -15,35 +18,57 @@ import page.clab.api.external.auth.blacklistIp.application.port.ExternalRetrieveBlacklistIpUseCase; import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalCheckIpBlockedUseCase; import page.clab.api.global.auth.util.IpWhitelistValidator; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; import page.clab.api.global.util.HttpReqResUtil; import page.clab.api.global.util.ResponseUtil; import page.clab.api.global.util.WhitelistPathMatcher; -import java.io.IOException; -import java.util.Base64; - +/** + * {@code CustomBasicAuthenticationFilter}는 기본 인증 필터를 확장하여 추가적인 보안 기능을 제공합니다. + * + *

IP 주소 기반 접근 제한, 화이트리스트 경로 검증, 사용자 인증 정보를 바탕으로 + * Slack 보안 알림을 전송하는 기능을 포함합니다. 또한 Swagger 또는 Actuator에 대한 접근이 성공하거나 실패할 경우 이를 Slack에 알립니다.

+ * + *

이 필터는 다음과 같은 추가 검증을 수행합니다:

+ *
    + *
  • 화이트리스트 경로 검증: 화이트리스트에 있는 경로에 대해서만 필터링 수행
  • + *
  • IP 주소 검증: 허용된 IP 주소인지, 블랙리스트에 포함되어 있는지, IP가 차단되어 있는지 확인
  • + *
  • 사용자 인증: Basic Authentication 헤더를 확인하고 인증을 수행
  • + *
+ * + *

IP 또는 인증 정보가 유효하지 않은 경우, 응답으로 401 Unauthorized를 반환합니다. + * 또한 인증 성공 또는 실패에 대한 정보를 Slack 보안 채널로 전송합니다.

+ * + * @see BasicAuthenticationFilter + */ @Slf4j public class CustomBasicAuthenticationFilter extends BasicAuthenticationFilter { private final IpWhitelistValidator ipWhitelistValidator; - private final SlackService slackService; private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase; private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase; + private final ApplicationEventPublisher eventPublisher; public CustomBasicAuthenticationFilter( AuthenticationManager authenticationManager, IpWhitelistValidator ipWhitelistValidator, - SlackService slackService, ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase, - ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase + ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase, + ApplicationEventPublisher eventPublisher ) { super(authenticationManager); this.externalCheckIpBlockedUseCase = externalCheckIpBlockedUseCase; this.externalRetrieveBlacklistIpUseCase = externalRetrieveBlacklistIpUseCase; this.ipWhitelistValidator = ipWhitelistValidator; - this.slackService = slackService; + this.eventPublisher = eventPublisher; + } + + @NotNull + private static String[] decodeCredentials(String authorizationHeader) { + String base64Credentials = authorizationHeader.substring("Basic ".length()); + String credentials = new String(Base64.getDecoder().decode(base64Credentials)); + return credentials.split(":", 2); } @Override @@ -63,7 +88,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse super.doFilterInternal(request, response, chain); } - private boolean authenticateUserCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException { + private boolean authenticateUserCredentials(HttpServletRequest request, HttpServletResponse response) + throws IOException { String authorizationHeader = request.getHeader("Authorization"); if (authorizationHeader == null || !authorizationHeader.startsWith("Basic ")) { response.setHeader("WWW-Authenticate", "Basic realm=\"Please enter your username and password\""); @@ -103,28 +129,29 @@ private boolean verifyIpAddressAccess(HttpServletResponse response) throws IOExc return true; } - @NotNull - private static String[] decodeCredentials(String authorizationHeader) { - String base64Credentials = authorizationHeader.substring("Basic ".length()); - String credentials = new String(Base64.getDecoder().decode(base64Credentials)); - return credentials.split(":", 2); - } - private void sendAuthenticationSuccessAlertSlackMessage(HttpServletRequest request) { String path = request.getRequestURI(); if (WhitelistPathMatcher.isSwaggerIndexEndpoint(path)) { - slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 허가되었습니다."); + String apiDocsAccessMessage = "API 문서에 대한 접근이 허가되었습니다."; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.API_DOCS_ACCESS, request, apiDocsAccessMessage)); } else if (WhitelistPathMatcher.isActuatorRequest(path)) { - slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 허가되었습니다."); + String actuatorAccessMessage = "Actuator에 대한 접근이 허가되었습니다."; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.ACTUATOR_ACCESS, request, actuatorAccessMessage)); } } private void sendAuthenticationFailureAlertSlackMessage(HttpServletRequest request) { String path = request.getRequestURI(); if (WhitelistPathMatcher.isSwaggerIndexEndpoint(path)) { - slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 거부되었습니다."); + String apiDocsAccessMessage = "API 문서에 대한 접근이 거부되었습니다."; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.API_DOCS_ACCESS, request, apiDocsAccessMessage)); } else if (WhitelistPathMatcher.isActuatorRequest(path)) { - slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 거부되었습니다."); + String actuatorAccessMessage = "Actuator에 대한 접근이 거부되었습니다."; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.ACTUATOR_ACCESS, request, actuatorAccessMessage)); } } } diff --git a/src/main/java/page/clab/api/global/auth/filter/FileAccessControlFilter.java b/src/main/java/page/clab/api/global/auth/filter/FileAccessControlFilter.java index cf72aca72..169a5e119 100644 --- a/src/main/java/page/clab/api/global/auth/filter/FileAccessControlFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/FileAccessControlFilter.java @@ -12,6 +12,18 @@ import page.clab.api.global.common.file.application.FileService; import page.clab.api.global.util.ResponseUtil; +/** + * {@code FileAccessControlFilter}는 파일 접근 요청에 대한 접근 제어를 수행하는 필터입니다. + * + *

특정 URL로 시작하는 요청에 대해 사용자의 인증 정보를 검증하여, 접근 권한이 있는지 확인합니다. + * 파일에 대한 접근 권한이 없는 경우 401 Unauthorized 응답을 반환하며, 권한이 있는 경우 요청을 + * 필터 체인에서 계속 처리합니다.

+ * + *

이 필터는 주로 파일 다운로드 또는 이미지 접근 등 특정 파일 URL에 대한 접근 제어가 필요한 + * 상황에서 사용됩니다.

+ * + * @see OncePerRequestFilter + */ @RequiredArgsConstructor public class FileAccessControlFilter extends OncePerRequestFilter { diff --git a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java index a0b7f0949..5f7548fd1 100644 --- a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java @@ -6,33 +6,51 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp; import page.clab.api.external.auth.blacklistIp.application.port.ExternalRegisterBlacklistIpUseCase; import page.clab.api.external.auth.blacklistIp.application.port.ExternalRetrieveBlacklistIpUseCase; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; import page.clab.api.global.util.HttpReqResUtil; import page.clab.api.global.util.ResponseUtil; import page.clab.api.global.util.SecurityPatternChecker; -import java.io.IOException; - +/** + * {@code InvalidEndpointAccessFilter}는 서버 내부 파일 및 디렉토리에 대한 비정상적인 접근을 차단하고 보안 경고를 전송하는 필터입니다. + * + *

특정 패턴을 통해 비정상적인 접근 시도를 탐지하며, 비정상적인 경로로 접근을 시도한 IP를 + * 블랙리스트에 등록하고, Slack을 통해 보안 경고 메시지를 전송합니다.

+ * + *

이 필터는 다음과 같은 검증을 수행합니다:

+ *
    + *
  • 파일 접근 URL(fileURL)로 시작하는 요청을 제외하고, 보안 패턴에 맞는 경로에 대한 비정상 접근 여부 확인
  • + *
  • 비정상적인 접근이 감지될 경우 해당 IP를 블랙리스트에 추가
  • + *
  • 블랙리스트에 추가된 IP에 대해 Slack 알림을 전송하여 보안팀에 경고
  • + *
+ * + *

비정상적인 접근 시도에 대해서는 403 Forbidden 응답을 반환합니다.

+ * + * @see GenericFilterBean + */ @RequiredArgsConstructor @Slf4j public class InvalidEndpointAccessFilter extends GenericFilterBean { - private final SlackService slackService; private final String fileURL; private final ExternalRegisterBlacklistIpUseCase externalRegisterBlacklistIpUseCase; private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase; + private final ApplicationEventPublisher eventPublisher; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String path = httpRequest.getRequestURI(); boolean isUploadedFileAccess = path.startsWith(fileURL); @@ -57,7 +75,8 @@ private void handleSuspiciousAccess(HttpServletRequest request, HttpServletRespo private void logSuspiciousAccess(HttpServletRequest request, String clientIpAddress) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String id = (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); + String id = + (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); String requestUrl = request.getRequestURI(); String httpMethod = request.getMethod(); int statusCode = HttpServletResponse.SC_FORBIDDEN; @@ -79,7 +98,9 @@ private void sendSecurityAlerts(HttpServletRequest request, String clientIpAddre String abnormalAccessMessage = "서버 내부 파일 및 디렉토리에 대한 접근이 감지되었습니다."; String blacklistAddedMessage = "Added IP: " + clientIpAddress; - slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS, abnormalAccessMessage); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, blacklistAddedMessage); + eventPublisher.publishEvent(new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS, request, + abnormalAccessMessage)); + eventPublisher.publishEvent(new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request, + blacklistAddedMessage)); } } diff --git a/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java index 5af1a684b..b34382ad6 100644 --- a/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/IpAuthenticationFilter.java @@ -18,6 +18,26 @@ import java.util.List; import java.util.Objects; +/** + * {@code IpAuthenticationFilter}는 IP 주소를 기반으로 클라이언트의 접근 권한을 검증하는 필터입니다. + * + *

클라이언트의 IP 주소를 사용하여 국가 정보를 조회하고, 허용된 국가 목록에 포함되지 않은 경우 + * 접근을 차단합니다. 주로 국가별 접근 제한이 필요한 상황에서 사용됩니다.

+ * + *

이 필터는 IP 정보를 조회하기 위해 외부 IP 조회 서비스(IPinfo)를 사용하며, + * 국가 정보를 확인하여 허용된 국가 목록과 비교합니다. 허용되지 않은 국가에서 접근 시 + * 로그 경고 메시지를 기록하고 요청 체인을 종료합니다.

+ * + *

필터는 다음과 같은 주요 기능을 수행합니다:

+ *
    + *
  • 클라이언트의 IP 주소를 기반으로 국가 정보를 조회
  • + *
  • 허용된 국가 목록에 포함되지 않은 경우 접근 차단
  • + *
+ * + *

필터 초기화와 종료 시 로그 메시지를 기록하여 필터의 활성 상태를 확인할 수 있습니다.

+ * + * @see Filter + */ @Component @Slf4j public class IpAuthenticationFilter implements Filter { diff --git a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java index 25c09cf4a..fba999b53 100644 --- a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java @@ -6,8 +6,10 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; @@ -16,26 +18,42 @@ import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalCheckIpBlockedUseCase; import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase; import page.clab.api.global.auth.jwt.JwtTokenProvider; -import page.clab.api.global.common.slack.application.SlackService; -import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; import page.clab.api.global.util.HttpReqResUtil; import page.clab.api.global.util.ResponseUtil; import page.clab.api.global.util.WhitelistPathMatcher; -import java.io.IOException; - +/** + * {@code JwtAuthenticationFilter}는 JWT 토큰을 검증하고, IP 주소 기반 접근 제한을 수행하는 필터입니다. + * + *

이 필터는 다음과 같은 보안 검증을 수행합니다:

+ *
    + *
  • 화이트리스트 경로 확인: 화이트리스트에 있는 요청 경로에 대해서는 필터링을 생략
  • + *
  • IP 주소 검증: 블랙리스트에 포함된 IP 주소이거나 차단된 IP인 경우 접근 차단
  • + *
  • JWT 토큰 검증: 유효한 토큰인지 확인하고, 관리자 토큰의 경우 발급된 IP와 동일한지 검증
  • + *
+ * + *

특히, 관리자 토큰이 발급된 IP와 다른 IP로부터의 접근이 발생하면 해당 토큰을 삭제하고, + * Slack을 통해 보안 경고 메시지를 전송합니다.

+ * + *

검증 실패 시, 401 Unauthorized 응답을 반환하여 접근을 제한합니다.

+ * + * @see GenericFilterBean + */ @RequiredArgsConstructor @Slf4j public class JwtAuthenticationFilter extends GenericFilterBean { - private final SlackService slackService; private final JwtTokenProvider jwtTokenProvider; + private final ApplicationEventPublisher eventPublisher; private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase; private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase; private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; String path = httpServletRequest.getRequestURI(); @@ -54,7 +72,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } private boolean verifyIpAddressAccess(HttpServletResponse response, String clientIpAddress) throws IOException { - if (externalRetrieveBlacklistIpUseCase.existsByIpAddress(clientIpAddress) || externalCheckIpBlockedUseCase.isIpBlocked(clientIpAddress)) { + if (externalRetrieveBlacklistIpUseCase.existsByIpAddress(clientIpAddress) + || externalCheckIpBlockedUseCase.isIpBlocked(clientIpAddress)) { log.info("[{}] : 서비스 이용이 제한된 IP입니다.", clientIpAddress); ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED); return false; @@ -62,12 +81,15 @@ private boolean verifyIpAddressAccess(HttpServletResponse response, String clien return true; } - private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response, String clientIpAddress) throws IOException { + private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response, String clientIpAddress) + throws IOException { String token = jwtTokenProvider.resolveToken(request); // 토큰이 존재하고 유효한 경우 if (token != null && jwtTokenProvider.validateToken(token)) { - RedisToken redisToken = jwtTokenProvider.isRefreshToken(token) ? externalManageRedisTokenUseCase.findByRefreshToken(token) : externalManageRedisTokenUseCase.findByAccessToken(token); + RedisToken redisToken = + jwtTokenProvider.isRefreshToken(token) ? externalManageRedisTokenUseCase.findByRefreshToken(token) + : externalManageRedisTokenUseCase.findByAccessToken(token); if (redisToken == null) { log.warn("존재하지 않는 토큰입니다."); ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED); @@ -91,7 +113,9 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons private void sendSecurityAlertSlackMessage(HttpServletRequest request, RedisToken redisToken) { if (redisToken.isAdminToken()) { request.setAttribute("member", redisToken.getId()); - slackService.sendSecurityAlertNotification(request, SecurityAlertType.DUPLICATE_LOGIN, "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다."); + String duplicateLoginMessage = "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다."; + eventPublisher.publishEvent( + new NotificationEvent(this, SecurityAlertType.DUPLICATE_LOGIN, request, duplicateLoginMessage)); } } } diff --git a/src/main/java/page/clab/api/global/auth/util/AuthUtil.java b/src/main/java/page/clab/api/global/auth/util/AuthUtil.java index 3c59dc090..9dea70925 100644 --- a/src/main/java/page/clab/api/global/auth/util/AuthUtil.java +++ b/src/main/java/page/clab/api/global/auth/util/AuthUtil.java @@ -5,6 +5,21 @@ import org.springframework.security.core.userdetails.User; import page.clab.api.global.auth.exception.AuthenticationInfoNotFoundException; +/** + * {@code AuthUtil}은 인증 정보에 접근하기 위한 유틸리티 클래스로, 인증 정보 조회 및 사용자 인증 상태를 확인하는 메서드를 제공합니다. + * + *

이 클래스는 인증된 사용자의 정보를 가져오거나, 현재 인증 상태를 확인하는 데 사용됩니다. + * 인증 정보가 없을 경우 예외를 발생시킵니다.

+ * + *

주요 메서드:

+ *
    + *
  • {@link #getAuthenticationInfo()}: 현재 인증된 사용자의 정보를 반환
  • + *
  • {@link #getAuthenticationInfoMemberId()}: 인증된 사용자의 ID를 반환
  • + *
  • {@link #isUserUnAuthenticated(Authentication)}: 사용자가 인증되지 않았는지 확인
  • + *
+ * + * @throws AuthenticationInfoNotFoundException 인증 정보가 없거나 유효하지 않을 경우 발생 + */ public class AuthUtil { public static User getAuthenticationInfo() { diff --git a/src/main/java/page/clab/api/global/auth/util/IpWhitelistValidator.java b/src/main/java/page/clab/api/global/auth/util/IpWhitelistValidator.java index 0b82d933c..2f7787816 100644 --- a/src/main/java/page/clab/api/global/auth/util/IpWhitelistValidator.java +++ b/src/main/java/page/clab/api/global/auth/util/IpWhitelistValidator.java @@ -8,6 +8,21 @@ import java.util.List; import java.util.Objects; +/** + * {@code IpWhitelistValidator}는 요청된 IP 주소가 화이트리스트에 포함되는지 확인하는 유틸리티 클래스입니다. + * + *

화이트리스트 검증 기능은 애플리케이션 설정에 따라 활성화되며, 외부에서 설정된 IP 목록을 통해 + * IP 접근을 제한할 수 있습니다.

+ * + *

주요 기능:

+ *
    + *
  • {@link #isIpWhitelisted(String)}: 특정 IP 주소가 화이트리스트에 포함되는지 확인
  • + *
+ * + *

화이트리스트 기능이 활성화되지 않은 경우 기본적으로 모든 IP 접근이 허용됩니다.

+ * + * @see WhitelistFileLoader + */ @Component @RequiredArgsConstructor public class IpWhitelistValidator { diff --git a/src/main/java/page/clab/api/global/auth/util/WhitelistFileLoader.java b/src/main/java/page/clab/api/global/auth/util/WhitelistFileLoader.java index 8020132ca..a6a93eeb3 100644 --- a/src/main/java/page/clab/api/global/auth/util/WhitelistFileLoader.java +++ b/src/main/java/page/clab/api/global/auth/util/WhitelistFileLoader.java @@ -18,6 +18,23 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; +/** + * {@code WhitelistFileLoader}는 지정된 경로의 YAML 파일에서 IP 화이트리스트 목록을 로드하는 클래스입니다. + * + *

IP 화이트리스트 파일이 존재하지 않을 경우 기본 화이트리스트 파일을 생성하며, 로드된 IP 목록은 + * 보안 접근 제한 등에 사용됩니다. 화이트리스트 파일의 수정 및 접근은 스레드 안전하게 처리됩니다.

+ * + *

주요 기능:

+ *
    + *
  • {@link #loadWhitelistIps()}: 화이트리스트 파일에서 IP 목록을 로드
  • + *
  • {@link #createDefaultWhitelistFile(Path, Yaml)}: 기본 화이트리스트 파일 생성
  • + *
  • {@link #parseWhitelistFile(Yaml, Path)}: YAML 파일을 파싱하여 IP 목록 반환
  • + *
+ * + *

화이트리스트가 비어있거나 잘못된 경우, 빈 목록을 반환하며 오류가 발생할 경우 로그에 기록됩니다.

+ * + * @see Yaml + */ @Component @RequiredArgsConstructor @Slf4j diff --git a/src/main/java/page/clab/api/global/common/email/application/EmailAsyncService.java b/src/main/java/page/clab/api/global/common/email/application/EmailAsyncService.java index 4f0f230be..34b2f377a 100644 --- a/src/main/java/page/clab/api/global/common/email/application/EmailAsyncService.java +++ b/src/main/java/page/clab/api/global/common/email/application/EmailAsyncService.java @@ -24,6 +24,23 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; +/** + * {@code EmailAsyncService}는 비동기적으로 이메일을 전송하고 이메일 대기열을 관리하는 서비스 클래스입니다. + * + *

이 클래스는 Spring의 JavaMailSender를 사용하여 이메일을 전송하며, 대용량 이메일 전송을 위해 배치 처리를 지원합니다. + * 이메일은 일정 간격으로 대기열에서 가져와 전송됩니다. 또한, 이메일 템플릿에 포함된 이미지를 지원하며 첨부 파일도 추가할 수 있습니다.

+ * + * 주요 기능: + *
    + *
  • {@link #sendEmailAsync(String, String, String, List, EmailTemplateType)} - 비동기적으로 이메일을 대기열에 추가합니다.
  • + *
  • {@link #processEmailQueue()} - 대기열에 쌓인 이메일을 일정 간격으로 배치 전송합니다.
  • + *
  • {@link #sendBatchEmail(List)} - 주어진 배치 내 모든 이메일을 전송합니다.
  • + *
  • {@link #setImageInTemplate(MimeMessageHelper, EmailTemplateType)} - 템플릿에 따른 이미지를 추가합니다.
  • + *
+ * + * @see JavaMailSender + * @see MimeMessage + */ @Service @RequiredArgsConstructor @Slf4j @@ -36,12 +53,28 @@ public class EmailAsyncService { @Value("${spring.mail.username}") private String sender; + /** + * 비동기적으로 이메일을 대기열에 추가합니다. + * + * @param to 수신자 이메일 주소 + * @param subject 이메일 제목 + * @param content 이메일 본문 내용 (HTML 지원) + * @param files 첨부 파일 목록 (선택 사항) + * @param emailTemplateType 이메일 템플릿 유형 + * @throws MessagingException 이메일 생성 중 오류 발생 시 + */ @Async public void sendEmailAsync(String to, String subject, String content, List files, EmailTemplateType emailTemplateType) throws MessagingException { log.debug("Sending email to: {}", LogSanitizerUtil.sanitizeForLog(to)); emailQueue.add(new EmailTask(to, subject, content, files, emailTemplateType)); } + /** + * 일정 간격으로 이메일 대기열을 확인하고 배치 전송합니다. + * + *

대기열에서 이메일을 꺼내 배치를 구성하고, 구성된 배치가 일정 크기 이상이면 전송합니다. + * 남아 있는 이메일이 있으면 즉시 전송합니다.

+ */ @Async @Scheduled(fixedRate = 1000) public void processEmailQueue() { @@ -63,6 +96,13 @@ public void processEmailQueue() { } } + /** + * 주어진 이메일 목록을 배치로 전송합니다. + * + * @param emailTasks 전송할 이메일 작업 목록 + * @throws MessagingException 이메일 생성 중 오류 발생 시 + * @throws IOException 첨부 파일 처리 중 오류 발생 시 + */ public void sendBatchEmail(List emailTasks) throws MessagingException, IOException { MimeMessage[] mimeMessages = new MimeMessage[emailTasks.size()]; for (int i = 0; i < emailTasks.size(); i++) { @@ -92,6 +132,13 @@ public void sendBatchEmail(List emailTasks) throws MessagingException log.debug("Batch email sent successfully."); } + /** + * 이메일 템플릿에 맞는 이미지를 추가합니다. + * + * @param messageHelper MimeMessageHelper 객체, 이메일 생성에 사용 + * @param templateType 템플릿 유형을 나타내는 {@link EmailTemplateType} 객체 + * @throws MessagingException 이미지 삽입 중 오류 발생 시 + */ private void setImageInTemplate(MimeMessageHelper messageHelper, EmailTemplateType templateType) throws MessagingException { if (Objects.equals(templateType.getTemplateName(), "clabEmail.html")) { messageHelper.addInline("image-1", new ClassPathResource("images/image-1.png")); diff --git a/src/main/java/page/clab/api/global/common/email/application/EmailService.java b/src/main/java/page/clab/api/global/common/email/application/EmailService.java index 8a79fbeff..0e4045e47 100644 --- a/src/main/java/page/clab/api/global/common/email/application/EmailService.java +++ b/src/main/java/page/clab/api/global/common/email/application/EmailService.java @@ -8,12 +8,27 @@ import org.thymeleaf.spring6.SpringTemplateEngine; import page.clab.api.domain.memberManagement.member.domain.Member; import page.clab.api.global.common.email.domain.EmailTemplateType; +import page.clab.api.global.common.email.dto.mapper.EmailDtoMapper; import page.clab.api.global.common.email.dto.request.EmailDto; import page.clab.api.global.common.email.exception.MessageSendingFailedException; import page.clab.api.global.config.EmailTemplateProperties; import java.util.List; +/** + * {@code EmailService}는 사용자의 계정 생성, 비밀번호 재설정, 새 비밀번호 전송을 포함한 이메일 전송을 관리하는 서비스입니다. + * + *

이 클래스는 템플릿을 기반으로 이메일 콘텐츠를 생성하고, 비동기적으로 이메일을 전송하는 {@link EmailAsyncService}를 활용하여 + * 이메일을 전송합니다. 각 이메일 유형에 대해 적절한 템플릿을 적용하여 이메일의 제목과 내용을 동적으로 설정합니다.

+ * + * 주요 기능: + *
    + *
  • {@link #sendAccountCreationEmail(Member, String)} - 계정 생성 시 계정 정보가 포함된 이메일을 전송합니다.
  • + *
  • {@link #sendPasswordResetCodeEmail(Member, String)} - 비밀번호 재설정 코드가 포함된 이메일을 전송합니다.
  • + *
  • {@link #sendNewPasswordEmail(Member, String)} - 새 비밀번호가 포함된 이메일을 전송합니다.
  • + *
  • {@link #generateEmailContent(EmailDto, String)} - 템플릿을 사용하여 이메일 콘텐츠를 생성합니다.
  • + *
+ */ @Service @RequiredArgsConstructor @Slf4j @@ -22,7 +37,14 @@ public class EmailService { private final EmailAsyncService emailAsyncService; private final EmailTemplateProperties emailTemplateProperties; private final SpringTemplateEngine springTemplateEngine; - + private final EmailDtoMapper mapper; + + /** + * 계정 생성 안내 이메일을 전송합니다. + * + * @param member 생성된 계정의 멤버 정보 + * @param password 생성된 비밀번호 + */ public void sendAccountCreationEmail(Member member, String password) { EmailTemplateProperties.Template template = emailTemplateProperties.getTemplate(EmailTemplateType.ACCOUNT_CREATION); @@ -31,7 +53,7 @@ public void sendAccountCreationEmail(Member member, String password) { .replace("{{id}}", member.getId()) .replace("{{password}}", password); - EmailDto emailDto = EmailDto.create( + EmailDto emailDto = mapper.of( List.of(member.getEmail()), subject, content, @@ -41,6 +63,12 @@ public void sendAccountCreationEmail(Member member, String password) { sendEmail(emailDto, member, " 계정 발급 안내 메일 전송에 실패했습니다."); } + /** + * 비밀번호 재설정 코드가 포함된 이메일을 전송합니다. + * + * @param member 비밀번호 재설정을 요청한 멤버 정보 + * @param code 비밀번호 재설정 인증 코드 + */ public void sendPasswordResetCodeEmail(Member member, String code) { EmailTemplateProperties.Template template = emailTemplateProperties.getTemplate(EmailTemplateType.PASSWORD_RESET_CODE); @@ -48,7 +76,7 @@ public void sendPasswordResetCodeEmail(Member member, String code) { String content = template.getContent() .replace("{{code}}", code); - EmailDto emailDto = EmailDto.create( + EmailDto emailDto = mapper.of( List.of(member.getEmail()), subject, content, @@ -58,6 +86,12 @@ public void sendPasswordResetCodeEmail(Member member, String code) { sendEmail(emailDto, member, " 비밀번호 재발급 인증 메일 전송에 실패했습니다."); } + /** + * 새 비밀번호가 포함된 이메일을 전송합니다. + * + * @param member 비밀번호가 재설정된 멤버 정보 + * @param newPassword 새 비밀번호 + */ public void sendNewPasswordEmail(Member member, String newPassword) { EmailTemplateProperties.Template template = emailTemplateProperties.getTemplate(EmailTemplateType.NEW_PASSWORD); @@ -66,7 +100,7 @@ public void sendNewPasswordEmail(Member member, String newPassword) { .replace("{{id}}", member.getId()) .replace("{{password}}", newPassword); - EmailDto emailDto = EmailDto.create( + EmailDto emailDto = mapper.of( List.of(member.getEmail()), subject, content, @@ -76,6 +110,13 @@ public void sendNewPasswordEmail(Member member, String newPassword) { sendEmail(emailDto, member, " 비밀번호 재설정 안내 메일 전송에 실패했습니다."); } + /** + * 이메일 전송을 위한 정보를 기반으로 이메일을 생성하여 비동기 전송합니다. + * + * @param emailDto 이메일 전송 정보 객체 + * @param member 전송 대상 멤버 + * @param message 예외 발생 시 로깅될 메시지 + */ private void sendEmail(EmailDto emailDto, Member member, String message) { try { String emailContent = generateEmailContent(emailDto, member.getName()); @@ -85,6 +126,13 @@ private void sendEmail(EmailDto emailDto, Member member, String message) { } } + /** + * 이메일 템플릿을 사용하여 이메일 콘텐츠를 생성합니다. + * + * @param emailDto 이메일 정보를 포함한 객체 + * @param name 수신자의 이름 + * @return 생성된 이메일 콘텐츠 + */ private String generateEmailContent(EmailDto emailDto, String name) { Context context = new Context(); context.setVariable("title", emailDto.getSubject()); diff --git a/src/main/java/page/clab/api/global/common/email/dto/mapper/EmailDtoMapper.java b/src/main/java/page/clab/api/global/common/email/dto/mapper/EmailDtoMapper.java new file mode 100644 index 000000000..b27020309 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/email/dto/mapper/EmailDtoMapper.java @@ -0,0 +1,20 @@ +package page.clab.api.global.common.email.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.global.common.email.domain.EmailTemplateType; +import page.clab.api.global.common.email.dto.request.EmailDto; + +import java.util.List; + +@Component +public class EmailDtoMapper { + + public EmailDto of(List to, String subject, String content, EmailTemplateType emailTemplateType) { + return EmailDto.builder() + .to(to) + .subject(subject) + .content(content) + .emailTemplateType(emailTemplateType) + .build(); + } +} diff --git a/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java b/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java index 7cf6eb46d..138032563 100644 --- a/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java +++ b/src/main/java/page/clab/api/global/common/email/dto/request/EmailDto.java @@ -29,13 +29,4 @@ public class EmailDto { @NotNull(message = "{notNull.email.templateType}") @Schema(description = "이메일 템플릿", example = "ACCOUNT_CREATION") private EmailTemplateType emailTemplateType; - - public static EmailDto create(List to, String subject, String content, EmailTemplateType emailTemplateType) { - return EmailDto.builder() - .to(to) - .subject(subject) - .content(content) - .emailTemplateType(emailTemplateType) - .build(); - } } diff --git a/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java b/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java index c9b245080..5652be81c 100644 --- a/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java +++ b/src/main/java/page/clab/api/global/common/file/application/AutoDeleteService.java @@ -13,6 +13,22 @@ import java.util.Arrays; import java.util.List; +/** + * {@code AutoDeleteService}는 지정된 경로의 파일을 자동으로 삭제하는 서비스입니다. + * + *

이 서비스는 매일 자정에 스케줄된 작업을 수행하며, 파일의 유효 기간이 만료되었거나 + * 데이터베이스에서 해당 파일의 정보가 존재하지 않는 경우 해당 파일을 삭제합니다. + * 파일 삭제 작업은 다양한 카테고리에 대해 수행되며, 이를 통해 불필요한 파일을 자동으로 정리합니다.

+ * + * 주요 기능: + *
    + *
  • {@link #autoDeleteExpiredFiles()} - 스케줄에 따라 만료된 파일 또는 데이터베이스 정보가 없는 파일을 자동으로 삭제합니다.
  • + *
  • {@link #deleteUselessFilesInDirectory(File, LocalDateTime)} - 특정 디렉토리 내 파일을 순회하며 유효성 검사를 수행하고 필요 시 삭제합니다.
  • + *
  • {@link #checkAndDeleteFileIfExpired(File, LocalDateTime)} - 파일의 만료 여부를 확인하고 만료된 경우 삭제합니다.
  • + *
  • {@link #checkAndDeleteFileIfInformationDoesNotExistInDB(File)} - 데이터베이스에 파일 정보가 없는 경우 파일을 삭제합니다.
  • + *
  • {@link #deleteFile(File)} - 파일을 삭제하고 로그에 기록합니다.
  • + *
+ */ @Service @RequiredArgsConstructor @Slf4j diff --git a/src/main/java/page/clab/api/global/common/file/application/FileHandler.java b/src/main/java/page/clab/api/global/common/file/application/FileHandler.java index b97575c97..45eee5bc6 100644 --- a/src/main/java/page/clab/api/global/common/file/application/FileHandler.java +++ b/src/main/java/page/clab/api/global/common/file/application/FileHandler.java @@ -21,6 +21,19 @@ import java.util.Objects; import java.util.Set; +/** + * {@code FileHandler}는 파일 처리 기능을 담당하는 클래스입니다. + * + *

이 클래스는 파일의 저장, 삭제, 이미지 압축 등을 수행하며, 파일 관련 속성 및 경로를 구성합니다.

+ * + * 주요 기능: + *
    + *
  • {@link #saveQRCodeImage(byte[], String, String, String, String)} - QR 코드 이미지를 파일로 저장합니다.
  • + *
  • {@link #saveFile(MultipartFile, String, String)} - Multipart 파일을 저장하고 필요 시 이미지 압축을 수행합니다.
  • + *
  • {@link #compressImageIfPossible(String, String)} - 이미지 압축이 가능한 경우 압축을 시도합니다.
  • + *
  • {@link #deleteFile(String)} - 저장된 파일을 삭제합니다.
  • + *
+ */ @Component @Configuration @Slf4j diff --git a/src/main/java/page/clab/api/global/common/file/application/FileService.java b/src/main/java/page/clab/api/global/common/file/application/FileService.java index a8ddbb1e9..925b0a3a9 100644 --- a/src/main/java/page/clab/api/global/common/file/application/FileService.java +++ b/src/main/java/page/clab/api/global/common/file/application/FileService.java @@ -25,6 +25,7 @@ import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase; import page.clab.api.global.auth.util.AuthUtil; import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.common.file.dto.mapper.FileDtoMapper; import page.clab.api.global.common.file.dto.request.DeleteFileRequestDto; import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; import page.clab.api.global.common.file.exception.CloudStorageNotEnoughException; @@ -41,6 +42,23 @@ import java.util.List; import java.util.regex.Pattern; +/** + * {@code FileService}는 파일 저장, 삭제, 경로 검증 및 파일 접근 권한 관리와 같은 파일 관리 기능을 제공하는 클래스입니다. + * + *

이 서비스는 다양한 카테고리에 대해 파일 업로드 및 QR 코드 이미지 저장, 파일 삭제, 멤버의 클라우드 사용량 검증 등의 기능을 수행합니다. + * 또한 파일 접근 권한을 역할(Role) 및 경로에 따라 관리하여 안전한 파일 접근을 보장합니다.

+ * + * 주요 기능: + *
    + *
  • {@link #saveQRCodeImage(byte[], String, long, String)} - QR 코드 이미지를 파일로 저장합니다.
  • + *
  • {@link #saveFiles(List, String, long)} - 여러 개의 파일을 저장하고 파일 접근 권한을 검증합니다.
  • + *
  • {@link #saveFile(MultipartFile, String, long)} - 단일 파일을 저장하고 접근 권한을 검증합니다.
  • + *
  • {@link #deleteFile(DeleteFileRequestDto)} - 파일을 삭제하고 파일의 존재 여부와 접근 권한을 확인합니다.
  • + *
  • {@link #validatePathVariable(String)} - 경로 유효성 및 접근 권한을 검증하여 파일 저장 경로의 적합성을 확인합니다.
  • + *
+ * + *

각 역할(Role)에 따른 파일 접근 제한 및 유효성 검증을 통해 파일 시스템의 보안을 유지하며, 유저별 클라우드 저장 용량을 제한하는 검증 로직을 포함하고 있습니다.

+ */ @Service @RequiredArgsConstructor public class FileService { @@ -53,6 +71,7 @@ public class FileService { private final GroupMemberRepository groupMemberRepository; private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase; private final ExternalRetrieveCloudUsageByMemberIdUseCase externalRetrieveCloudUsageByMemberIdUseCase; + private final FileDtoMapper mapper; @Value("${resource.file.url}") private String fileURL; @@ -118,7 +137,7 @@ public UploadedFileResponseDto saveFile(MultipartFile multipartFile, String path UploadedFile uploadedFile = UploadedFile.create(currentMemberId, multipartFile.getOriginalFilename(), fileName, savedFilePath, url, multipartFile.getSize(), multipartFile.getContentType(), storagePeriod, path); uploadedFileService.saveUploadedFile(uploadedFile); - return UploadedFileResponseDto.toDto(uploadedFile); + return mapper.toDto(uploadedFile); } public String deleteFile(DeleteFileRequestDto deleteFileRequestDto) throws PermissionDeniedException { @@ -200,7 +219,7 @@ private void validateSubmitPath(String[] pathParts) throws InvalidPathVariableEx validateActivityGroupExist(activityGroupId); validateIsMemberPartOfActivity(memberId, activityGroupId); - ActivityGroupBoard activityGroupBoard = activityGroupBoardService.getActivityGroupBoardByIdOrThrow(activityGroupBoardId); + ActivityGroupBoard activityGroupBoard = activityGroupBoardService.getActivityGroupBoardById(activityGroupBoardId); validateIsParentBoardAssignment(activityGroupBoard); } @@ -286,7 +305,7 @@ public boolean isUserAccessibleByCategory(String category, String url, Authentic private boolean isNonSubmitCategoryAccessible(String url, Authentication authentication) { String memberId = authentication.getName(); - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(memberId); + Member member = externalRetrieveMemberUseCase.getById(memberId); String[] parts = url.split("/"); Long activityGroupId = Long.parseLong(parts[4]); @@ -304,7 +323,7 @@ private boolean isSubmitAccessible(String url, Authentication authentication) { UploadedFile uploadedFile = uploadedFileService.getUploadedFileByUrl(url); String uploaderId = uploadedFile.getUploader(); String memberId = authentication.getName(); - Member member = externalRetrieveMemberUseCase.findByIdOrThrow(memberId); + Member member = externalRetrieveMemberUseCase.getById(memberId); String[] parts = url.split("/"); Long activityGroupId = Long.parseLong(parts[4]); diff --git a/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java b/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java index b1de509a7..55bc28f68 100644 --- a/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java +++ b/src/main/java/page/clab/api/global/common/file/application/UploadedFileService.java @@ -10,6 +10,23 @@ import java.util.ArrayList; import java.util.List; +/** + * {@code UploadedFileService}는 업로드된 파일을 저장, 조회, 및 유효성 검사를 수행하는 서비스입니다. + * + *

이 서비스는 파일의 URL을 기반으로 파일을 조회하거나, 특정 카테고리에 속하는 최신 파일을 조회하며, + * 여러 URL 목록을 받아 해당하는 파일들을 검색하는 등의 기능을 제공합니다.

+ * + * 주요 기능: + *
    + *
  • {@link #saveUploadedFile(UploadedFile)} - 파일 정보를 데이터베이스에 저장합니다.
  • + *
  • {@link #getUploadedFileByUrl(String)} - URL을 기반으로 파일을 조회합니다.
  • + *
  • {@link #getUniqueUploadedFileByCategory(String)} - 특정 카테고리의 최신 파일을 조회합니다.
  • + *
  • {@link #getUploadedFilesByUrls(List)} - URL 목록에 해당하는 파일들을 조회하며, 누락된 파일이 있을 경우 예외를 발생시킵니다.
  • + *
+ * + *

이 서비스는 파일 조회와 관련된 기본적인 예외 처리도 포함하고 있으며, 요청된 파일이 존재하지 않을 경우 + * {@link NotFoundException}을 발생시킵니다.

+ */ @Service @RequiredArgsConstructor @Slf4j diff --git a/src/main/java/page/clab/api/global/common/file/dto/mapper/FileDtoMapper.java b/src/main/java/page/clab/api/global/common/file/dto/mapper/FileDtoMapper.java new file mode 100644 index 000000000..554bb25aa --- /dev/null +++ b/src/main/java/page/clab/api/global/common/file/dto/mapper/FileDtoMapper.java @@ -0,0 +1,49 @@ +package page.clab.api.global.common.file.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.global.common.file.domain.UploadedFile; +import page.clab.api.global.common.file.dto.request.DeleteFileRequestDto; +import page.clab.api.global.common.file.dto.response.FileInfo; +import page.clab.api.global.common.file.dto.response.UploadedFileResponseDto; + +import java.io.File; +import java.util.Date; +import java.util.List; + +@Component +public class FileDtoMapper { + + public List toDto(List uploadedFiles) { + return uploadedFiles.stream() + .map(this::toDto) + .toList(); + } + + public UploadedFileResponseDto toDto(UploadedFile uploadedFile) { + return UploadedFileResponseDto.builder() + .fileUrl(uploadedFile.getUrl()) + .originalFileName(uploadedFile.getOriginalFileName()) + .storagePeriod(uploadedFile.getStoragePeriod()) + .createdAt(uploadedFile.getCreatedAt()) + .build(); + } + + public DeleteFileRequestDto of(String url) { + return DeleteFileRequestDto.builder() + .url(url) + .build(); + } + + public FileInfo create(File file) { + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + + return FileInfo.builder() + .fileName(file.getName()) + .fileSizeInBytes(file.length()) + .creationDate(new Date(file.lastModified())) + .modificationDate(new Date(file.lastModified())) + .build(); + } +} diff --git a/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java b/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java index 5354e2341..27aa8a63b 100644 --- a/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java +++ b/src/main/java/page/clab/api/global/common/file/dto/request/DeleteFileRequestDto.java @@ -14,10 +14,4 @@ public class DeleteFileRequestDto { @NotNull(message = "{notNull.file.url}") @Schema(description = "파일경로", example = "/resources/files/forms/123456.png", required = true) private String url; - - public static DeleteFileRequestDto create(String url) { - return DeleteFileRequestDto.builder() - .url(url) - .build(); - } } diff --git a/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java b/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java index d3a01d269..f3279ad60 100644 --- a/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java +++ b/src/main/java/page/clab/api/global/common/file/dto/response/FileInfo.java @@ -3,7 +3,6 @@ import lombok.Builder; import lombok.Getter; -import java.io.File; import java.util.Date; @Getter @@ -14,17 +13,4 @@ public class FileInfo { private Long fileSizeInBytes; private Date creationDate; private Date modificationDate; - - public static FileInfo toDto(File file) { - if (file == null || !file.exists() || !file.isFile()) { - return null; - } - - return FileInfo.builder() - .fileName(file.getName()) - .fileSizeInBytes(file.length()) - .creationDate(new Date(file.lastModified())) - .modificationDate(new Date(file.lastModified())) - .build(); - } } diff --git a/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java b/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java index 908802e6a..d05313c0e 100644 --- a/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java +++ b/src/main/java/page/clab/api/global/common/file/dto/response/UploadedFileResponseDto.java @@ -2,10 +2,8 @@ import lombok.Builder; import lombok.Getter; -import page.clab.api.global.common.file.domain.UploadedFile; import java.time.LocalDateTime; -import java.util.List; @Getter @Builder @@ -15,19 +13,4 @@ public class UploadedFileResponseDto { private String originalFileName; private Long storagePeriod; private LocalDateTime createdAt; - - public static List toDto(List uploadedFiles) { - return uploadedFiles.stream() - .map(UploadedFileResponseDto::toDto) - .toList(); - } - - public static UploadedFileResponseDto toDto(UploadedFile uploadedFile) { - return UploadedFileResponseDto.builder() - .fileUrl(uploadedFile.getUrl()) - .originalFileName(uploadedFile.getOriginalFileName()) - .storagePeriod(uploadedFile.getStoragePeriod()) - .createdAt(uploadedFile.getCreatedAt()) - .build(); - } } diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java new file mode 100644 index 000000000..00117991b --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java @@ -0,0 +1,30 @@ +package page.clab.api.global.common.notificationSetting.adapter.in.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.notificationSetting.application.dto.response.NotificationSettingResponseDto; +import page.clab.api.global.common.notificationSetting.application.port.in.RetrieveNotificationSettingUseCase; + +@RestController +@RequestMapping("/api/v1/notification-settings") +@RequiredArgsConstructor +@Tag(name = "Notification Setting", description = "웹훅 알림 설정") +public class NotificationSettingRetrieveController { + + private final RetrieveNotificationSettingUseCase retrieveNotificationSettingUseCase; + + @Operation(summary = "[S] 웹훅 알림 조회", description = "ROLE_SUPER 이상의 권한이 필요함") + @PreAuthorize("hasRole('SUPER')") + @GetMapping("") + public ApiResponse> getNotificationSettings() { + List notificationSettings = retrieveNotificationSettingUseCase.retrieveNotificationSettings(); + return ApiResponse.success(notificationSettings); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java new file mode 100644 index 000000000..74023a6bc --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java @@ -0,0 +1,33 @@ +package page.clab.api.global.common.notificationSetting.adapter.in.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import page.clab.api.global.common.dto.ApiResponse; +import page.clab.api.global.common.notificationSetting.application.dto.request.NotificationSettingToggleRequestDto; +import page.clab.api.global.common.notificationSetting.application.port.in.ManageNotificationSettingUseCase; + +@RestController +@RequestMapping("/api/v1/notification-settings") +@RequiredArgsConstructor +@Tag(name = "Notification Setting", description = "웹훅 알림 설정") +public class NotificationSettingToggleController { + + private final ManageNotificationSettingUseCase manageNotificationSettingUseCase; + + @Operation(summary = "[S] 웹훅 알림 설정 변경", description = "ROLE_SUPER 이상의 권한이 필요함") + @PreAuthorize("hasRole('SUPER')") + @PutMapping("") + public ApiResponse toggleNotificationSetting( + @Valid @RequestBody NotificationSettingToggleRequestDto requestDto + ) { + manageNotificationSettingUseCase.toggleNotificationSetting(requestDto.getAlertType(), requestDto.isEnabled()); + return ApiResponse.success(); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java new file mode 100644 index 000000000..4bae839c3 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java @@ -0,0 +1,34 @@ +package page.clab.api.global.common.notificationSetting.adapter.out.persistence; + +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import page.clab.api.global.common.notificationSetting.application.port.out.RetrieveNotificationSettingPort; +import page.clab.api.global.common.notificationSetting.application.port.out.UpdateNotificationSettingPort; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +@Component +@RequiredArgsConstructor +public class NotificationSettingPersistenceAdapter implements + RetrieveNotificationSettingPort, + UpdateNotificationSettingPort { + + private final NotificationSettingRepository repository; + + @Override + public List findAll() { + return repository.findAll(); + } + + @Override + public Optional findByAlertType(AlertType alertType) { + return repository.findByAlertType(alertType); + } + + @Override + public NotificationSetting save(NotificationSetting setting) { + return repository.save(setting); + } +} diff --git a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java similarity index 52% rename from src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java rename to src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java index e4cab3c38..4be9efb7b 100644 --- a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java @@ -1,10 +1,9 @@ -package page.clab.api.global.common.slack.dao; - -import org.springframework.data.jpa.repository.JpaRepository; -import page.clab.api.global.common.slack.domain.AlertType; -import page.clab.api.global.common.slack.domain.NotificationSetting; +package page.clab.api.global.common.notificationSetting.adapter.out.persistence; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; public interface NotificationSettingRepository extends JpaRepository { diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java new file mode 100644 index 000000000..d67f0a17f --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java @@ -0,0 +1,17 @@ +package page.clab.api.global.common.notificationSetting.adapter.out.webhook; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.concurrent.CompletableFuture; +import page.clab.api.global.common.notificationSetting.application.port.out.WebhookClient; +import page.clab.api.global.common.notificationSetting.domain.AlertType; + +/** + * {@code AbstractWebhookClient}는 Discord 및 Slack Webhook 클라이언트의 공통 인터페이스를 정의하는 추상 클래스입니다. + */ +public abstract class AbstractWebhookClient implements WebhookClient { + + @Override + public abstract CompletableFuture sendMessage(String webhookUrl, AlertType alertType, + HttpServletRequest request, + Object additionalData); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java new file mode 100644 index 000000000..b6bfedf51 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java @@ -0,0 +1,25 @@ +package page.clab.api.global.common.notificationSetting.adapter.out.webhook; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.application.port.out.NotificationSender; +import page.clab.api.global.common.notificationSetting.domain.PlatformType; + +@Component +@RequiredArgsConstructor +public class DiscordNotificationSender implements NotificationSender { + + private final DiscordWebhookClient discordWebhookClient; + + @Override + public String getPlatformName() { + return PlatformType.DISCORD.getName(); + } + + @Override + public void sendNotification(NotificationEvent event, String webhookUrl) { + discordWebhookClient.sendMessage(webhookUrl, event.getAlertType(), event.getRequest(), + event.getAdditionalData()); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java new file mode 100644 index 000000000..fb1c443d3 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java @@ -0,0 +1,362 @@ +package page.clab.api.global.common.notificationSetting.adapter.out.webhook; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto; +import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.service.WebhookCommonService; +import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType; +import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; +import page.clab.api.global.util.HttpReqResUtil; + +/** + * {@code DiscordWebhookClient}는 다양한 알림 유형에 따라 Discord 메시지를 구성하고 전송하는 클래스입니다. + * + *

주요 기능:

+ *
    + *
  • {@link #sendMessage(String, AlertType, HttpServletRequest, Object)}: Discord에 알림 메시지를 비동기적으로 전송
  • + *
  • {@link #createEmbeds(AlertType, HttpServletRequest, Object)}: 알림 유형에 따라 Discord 메시지 임베드 생성
  • + *
  • 다양한 알림 유형에 맞는 메시지 형식을 생성하는 전용 메서드
  • + *
+ * + *

Discord Webhook API를 사용하여 웹훅 URL을 통해 메시지를 전송하며, 메시지 전송 실패 시 로그에 오류를 기록합니다.

+ * + *

AlertType을 기반으로 여러 도메인에서 발생하는 이벤트를 Discord를 통해 모니터링할 수 있도록 지원하며, + * Discord 알림은 주로 서버 이벤트, 보안 경고, 신규 신청, 관리자 로그인 등의 이벤트를 다룹니다.

+ * + * @see HttpClient + * @see HttpRequest + * @see HttpResponse + */ +@Component +@Slf4j +public class DiscordWebhookClient extends AbstractWebhookClient { + + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + private final NotificationConfigProperties.CommonProperties commonProperties; + private final Environment environment; + private final WebhookCommonService webhookCommonService; + + public DiscordWebhookClient( + NotificationConfigProperties notificationConfigProperties, + ObjectMapper objectMapper, + Environment environment, + WebhookCommonService webhookCommonService + ) { + this.httpClient = HttpClient.newHttpClient(); + this.objectMapper = objectMapper; + this.commonProperties = notificationConfigProperties.getCommon(); + this.environment = environment; + this.webhookCommonService = webhookCommonService; + } + + /** + * Discord에 알림 메시지를 비동기적으로 전송합니다. + * + * @param webhookUrl 메시지를 보낼 Discord 웹훅 URL + * @param alertType 알림 유형을 나타내는 {@link AlertType} + * @param request HttpServletRequest 객체, 클라이언트 요청 정보 + * @param additionalData 추가 데이터 + * @return 메시지 전송 성공 여부를 나타내는 CompletableFuture + */ + public CompletableFuture sendMessage(String webhookUrl, AlertType alertType, + HttpServletRequest request, Object additionalData) { + Map payload = createPayload(alertType, request, additionalData); + + return CompletableFuture.supplyAsync(() -> { + try { + String jsonPayload = objectMapper.writeValueAsString(payload); + + HttpRequest httpRequest = HttpRequest.newBuilder() + .uri(URI.create(webhookUrl)) + .header("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) + .build(); + + HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == HttpStatus.NO_CONTENT.value()) { + return true; + } else { + log.error("Discord notification failed: {}", response.body()); + return false; + } + } catch (IOException | InterruptedException e) { + log.error("Failed to send Discord message: {}", e.getMessage(), e); + return false; + } + }); + } + + /** + * 알림 유형과 요청 정보, 추가 데이터를 사용하여 Discord 메시지 페이로드를 생성합니다. + * + * @param alertType 알림 유형 + * @param request 클라이언트 요청 정보 + * @param additionalData 추가 데이터 + * @return 생성된 페이로드 맵 + */ + public Map createPayload(AlertType alertType, HttpServletRequest request, Object additionalData) { + List> embeds = createEmbeds(alertType, request, additionalData); + + Map payload = new HashMap<>(); + payload.put("embeds", embeds); + + return payload; + } + + /** + * 특정 알림 유형과 요청 정보 및 추가 데이터를 사용하여 Discord 메시지의 임베드를 생성합니다. + * + * @param alertType 알림 유형 + * @param request HttpServletRequest 객체 + * @param additionalData 추가 데이터 + * @return 생성된 임베드 목록 + */ + public List> createEmbeds(AlertType alertType, HttpServletRequest request, + Object additionalData) { + switch (alertType) { + case SecurityAlertType securityAlertType -> { + return createSecurityAlertEmbeds(request, alertType, additionalData.toString()); + } + case GeneralAlertType generalAlertType -> { + return createGeneralAlertEmbeds(generalAlertType, request, additionalData); + } + case ExecutivesAlertType executivesAlertType -> { + return createExecutivesAlertEmbeds(executivesAlertType, additionalData); + } + case null, default -> { + log.error("Unknown alert type: {}", alertType); + return Collections.emptyList(); + } + } + } + + private List> createGeneralAlertEmbeds(GeneralAlertType alertType, HttpServletRequest request, + Object additionalData) { + switch (alertType) { + case ADMIN_LOGIN: + if (additionalData instanceof MemberLoginInfoDto) { + return createAdminLoginEmbeds(request, (MemberLoginInfoDto) additionalData); + } + break; + case SERVER_START: + return createServerStartEmbeds(); + case SERVER_ERROR: + if (additionalData instanceof Exception) { + return createErrorEmbeds(request, (Exception) additionalData); + } + break; + default: + log.error("Unknown general alert type: {}", alertType); + } + return Collections.emptyList(); + } + + private List> createExecutivesAlertEmbeds(ExecutivesAlertType alertType, + Object additionalData) { + switch (alertType) { + case NEW_APPLICATION: + if (additionalData instanceof ApplicationRequestDto) { + return createApplicationEmbeds((ApplicationRequestDto) additionalData); + } + break; + case NEW_BOARD: + if (additionalData instanceof BoardNotificationInfo) { + return createBoardEmbeds((BoardNotificationInfo) additionalData); + } + break; + case NEW_MEMBERSHIP_FEE: + if (additionalData instanceof MembershipFeeNotificationInfo) { + return createMembershipFeeEmbeds((MembershipFeeNotificationInfo) additionalData); + } + break; + case NEW_BOOK_LOAN_REQUEST: + if (additionalData instanceof BookLoanRecordNotificationInfo) { + return createBookLoanRecordEmbeds((BookLoanRecordNotificationInfo) additionalData); + } + break; + default: + log.error("Unknown executives alert type: {}", alertType); + } + return Collections.emptyList(); + } + + private List> createErrorEmbeds(HttpServletRequest request, Exception e) { + String httpMethod = request.getMethod(); + String fullUrl = webhookCommonService.getFullUrl(request); + String username = webhookCommonService.getUsername(request); + String errorMessage = webhookCommonService.extractMessageAfterException(e); + String stackTrace = webhookCommonService.getStackTraceSummary(e); + + log.error("Server Error: {}", errorMessage); + + Map embed = new HashMap<>(); + embed.put("title", ":firecracker: Server Error"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("User", username, true), + createField("Endpoint", "[" + httpMethod + "] " + fullUrl, true), + createField("Error Message", errorMessage, false), + createField("Stack Trace", "```" + stackTrace + "```", false) + )); + + return Collections.singletonList(embed); + } + + private List> createSecurityAlertEmbeds(HttpServletRequest request, AlertType alertType, + String additionalMessage) { + String clientIp = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + String fullUrl = webhookCommonService.getFullUrl(request); + String username = webhookCommonService.getUsername(request); + String location = webhookCommonService.getLocation(request); + + Map embed = new HashMap<>(); + embed.put("title", ":imp: " + alertType.getTitle()); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("User", username, true), + createField("IP Address", clientIp, true), + createField("Location", location, true), + createField("Endpoint", fullUrl, true), + createField("Details", alertType.getDefaultMessage() + "\n" + additionalMessage, false) + )); + + return Collections.singletonList(embed); + } + + private List> createAdminLoginEmbeds(HttpServletRequest request, + MemberLoginInfoDto loginMember) { + String clientIp = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + String location = webhookCommonService.getLocation(request); + + Map embed = new HashMap<>(); + embed.put("title", ":mechanic: " + loginMember.getRole().getDescription() + " Login"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("User", loginMember.getMemberId() + " " + loginMember.getMemberName(), true), + createField("IP Address", clientIp, true), + createField("Location", location, true) + )); + + return Collections.singletonList(embed); + } + + private List> createApplicationEmbeds(ApplicationRequestDto requestDto) { + Map embed = new HashMap<>(); + embed.put("title", ":sparkles: 동아리 지원"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("구분", requestDto.getApplicationType().getDescription(), true), + createField("학번", requestDto.getStudentId(), true), + createField("이름", requestDto.getName(), true), + createField("학년", requestDto.getGrade() + "학년", true), + createField("관심 분야", requestDto.getInterests(), false) + )); + + if (requestDto.getGithubUrl() != null && !requestDto.getGithubUrl().isEmpty()) { + embed.put("description", "[Github](" + requestDto.getGithubUrl() + ")"); + } + + return Collections.singletonList(embed); + } + + private List> createBoardEmbeds(BoardNotificationInfo board) { + Map embed = new HashMap<>(); + embed.put("title", ":writing_hand: 새 게시글"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("제목", board.getTitle(), true), + createField("분류", board.getCategory(), true), + createField("작성자", board.getUsername(), true) + )); + + return Collections.singletonList(embed); + } + + private List> createMembershipFeeEmbeds(MembershipFeeNotificationInfo data) { + String username = data.getMemberId() + " " + data.getMemberName(); + + Map embed = new HashMap<>(); + embed.put("title", ":dollar: 회비 신청"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("신청자", username, true), + createField("분류", data.getCategory(), true), + createField("금액", data.getAmount() + "원", true), + createField("Content", data.getContent(), false) + )); + + return Collections.singletonList(embed); + } + + private List> createBookLoanRecordEmbeds(BookLoanRecordNotificationInfo data) { + String username = data.getMemberId() + " " + data.getMemberName(); + + Map embed = new HashMap<>(); + embed.put("title", ":books: 도서 대여 신청"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("도서명", data.getBookTitle(), true), + createField("분류", data.getCategory(), true), + createField("신청자", username, true), + createField("상태", data.isAvailable() ? "대여 가능" : "대여 중", true) + )); + + return Collections.singletonList(embed); + } + + private List> createServerStartEmbeds() { + String osInfo = webhookCommonService.getOperatingSystemInfo(); + String jdkVersion = webhookCommonService.getJavaRuntimeVersion(); + double cpuUsage = webhookCommonService.getCpuUsage(); + String memoryUsage = webhookCommonService.getMemoryUsage(); + + Map embed = new HashMap<>(); + embed.put("title", ":battery: Server Started"); + embed.put("color", commonProperties.getColorAsInt()); + embed.put("fields", Arrays.asList( + createField("Environment", environment.getProperty("spring.profiles.active"), true), + createField("OS", osInfo, true), + createField("JDK Version", jdkVersion, true), + createField("CPU Usage", String.format("%.2f%%", cpuUsage), true), + createField("Memory Usage", memoryUsage, true) + )); + + embed.put("description", + "[Web](" + commonProperties.getWebUrl() + ") | [API Docs](" + commonProperties.getApiUrl() + ")"); + + return Collections.singletonList(embed); + } + + private Map createField(String name, String value, boolean inline) { + Map field = new HashMap<>(); + field.put("name", name); + field.put("value", value); + field.put("inline", inline); + return field; + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackNotificationSender.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackNotificationSender.java new file mode 100644 index 000000000..3a993dc57 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackNotificationSender.java @@ -0,0 +1,25 @@ +package page.clab.api.global.common.notificationSetting.adapter.out.webhook; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.application.port.out.NotificationSender; +import page.clab.api.global.common.notificationSetting.domain.PlatformType; + +@Component +@RequiredArgsConstructor +public class SlackNotificationSender implements NotificationSender { + + private final SlackWebhookClient slackWebhookClient; + + @Override + public String getPlatformName() { + return PlatformType.SLACK.getName(); + } + + @Override + public void sendNotification(NotificationEvent event, String webhookUrl) { + slackWebhookClient.sendMessage(webhookUrl, event.getAlertType(), event.getRequest(), + event.getAdditionalData()); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackWebhookClient.java new file mode 100644 index 000000000..291bfd047 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/SlackWebhookClient.java @@ -0,0 +1,338 @@ +package page.clab.api.global.common.notificationSetting.adapter.out.webhook; + +import static com.slack.api.model.block.Blocks.actions; +import static com.slack.api.model.block.Blocks.section; +import static com.slack.api.model.block.composition.BlockCompositions.markdownText; +import static com.slack.api.model.block.composition.BlockCompositions.plainText; +import static com.slack.api.model.block.element.BlockElements.asElements; +import static com.slack.api.model.block.element.BlockElements.button; + +import com.slack.api.Slack; +import com.slack.api.model.Attachment; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.webhook.Payload; +import com.slack.api.webhook.WebhookResponse; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto; +import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto; +import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo; +import page.clab.api.global.common.notificationSetting.application.service.WebhookCommonService; +import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType; +import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType; +import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType; +import page.clab.api.global.util.HttpReqResUtil; + +/** + * {@code SlackWebhookClient}는 다양한 알림 유형에 따라 Slack 메시지를 구성하고 전송하는 클래스입니다. + * + *

주요 기능:

+ *
    + *
  • {@link #sendMessage(String, AlertType, HttpServletRequest, Object)}: Slack에 알림 메시지를 비동기적으로 전송
  • + *
  • {@link #createBlocks(AlertType, HttpServletRequest, Object)}: 알림 유형에 따라 Slack 메시지 블록 생성
  • + *
  • 다양한 알림 유형에 맞는 메시지 형식을 생성하는 전용 메서드
  • + *
+ * + *

Slack API와 통합하여 웹훅 URL을 통해 메시지를 전송하며, 메시지 전송 실패 시 로그에 오류를 기록합니다.

+ * + *

AlertType을 기반으로 여러 도메인에서 발생하는 이벤트를 Slack을 통해 모니터링할 수 있도록 지원하며, + * Slack 알림은 주로 서버 이벤트, 보안 경고, 신규 신청, 관리자 로그인 등의 이벤트를 다룹니다.

+ * + * @see Slack + * @see Payload + * @see LayoutBlock + */ +@Component +@Slf4j +public class SlackWebhookClient extends AbstractWebhookClient { + + private final Slack slack; + private final NotificationConfigProperties.CommonProperties commonProperties; + private final Environment environment; + private final WebhookCommonService webhookCommonService; + + public SlackWebhookClient( + NotificationConfigProperties notificationConfigProperties, + Environment environment, + WebhookCommonService webhookCommonService + ) { + this.slack = Slack.getInstance(); + this.commonProperties = notificationConfigProperties.getCommon(); + this.environment = environment; + this.webhookCommonService = webhookCommonService; + } + + /** + * Slack에 알림 메시지를 전송합니다. + * + *

주어진 webhookUrl과 alertType, HttpServletRequest 및 추가 데이터(additionalData)를 사용하여 알림 메시지를 + * 비동기적으로 Slack에 전송합니다.

+ * + * @param webhookUrl 메시지를 보낼 Slack 웹훅 URL + * @param alertType 알림 유형을 나타내는 {@link AlertType} + * @param request HttpServletRequest 객체, 클라이언트 요청 정보 + * @param additionalData 추가 데이터 + * @return 메시지 전송 성공 여부를 나타내는 CompletableFuture + */ + public CompletableFuture sendMessage(String webhookUrl, AlertType alertType, + HttpServletRequest request, Object additionalData) { + List blocks = createBlocks(alertType, request, additionalData); + + return CompletableFuture.supplyAsync(() -> { + Payload payload = Payload.builder() + .blocks(Collections.singletonList(blocks.getFirst())) + .attachments(Collections.singletonList( + Attachment.builder() + .color(commonProperties.getColor()) + .blocks(blocks.subList(1, blocks.size())) + .build() + )) + .build(); + + try { + WebhookResponse response = slack.send(webhookUrl, payload); + if (response.getCode() == HttpStatus.OK.value()) { + return true; + } else { + log.error("Slack notification failed: {}", response.getMessage()); + return false; + } + } catch (IOException e) { + log.error("Failed to send Slack message: {}", e.getMessage(), e); + return false; + } + }); + } + + /** + * 특정 알림 유형과 요청 정보 및 추가 데이터를 사용하여 Slack 메시지의 블록을 생성합니다. + * + *

AlertType에 따라 보안 경고, 일반 알림, 운영진 알림 등 다양한 형식의 메시지를 생성합니다.

+ * + * @param alertType 알림 유형 + * @param request HttpServletRequest 객체 + * @param additionalData 추가 데이터 + * @return 생성된 LayoutBlock 목록 + */ + public List createBlocks(AlertType alertType, HttpServletRequest request, Object additionalData) { + switch (alertType) { + case SecurityAlertType securityAlertType -> { + return createSecurityAlertBlocks(request, alertType, additionalData.toString()); + } + case GeneralAlertType generalAlertType -> { + return createGeneralAlertBlocks(generalAlertType, request, additionalData); + } + case ExecutivesAlertType executivesAlertType -> { + return createExecutivesAlertBlocks(executivesAlertType, additionalData); + } + case null, default -> { + log.error("Unknown alert type: {}", alertType); + return Collections.emptyList(); + } + } + } + + private List createGeneralAlertBlocks(GeneralAlertType alertType, HttpServletRequest request, + Object additionalData) { + switch (alertType) { + case ADMIN_LOGIN: + if (additionalData instanceof MemberLoginInfoDto) { + return createAdminLoginBlocks(request, (MemberLoginInfoDto) additionalData); + } + break; + case SERVER_START: + return createServerStartBlocks(); + case SERVER_ERROR: + if (additionalData instanceof Exception) { + return createErrorBlocks(request, (Exception) additionalData); + } + break; + default: + log.error("Unknown general alert type: {}", alertType); + } + return Collections.emptyList(); + } + + private List createExecutivesAlertBlocks(ExecutivesAlertType alertType, Object additionalData) { + switch (alertType) { + case NEW_APPLICATION: + if (additionalData instanceof ApplicationRequestDto) { + return createApplicationBlocks((ApplicationRequestDto) additionalData); + } + break; + case NEW_BOARD: + if (additionalData instanceof BoardNotificationInfo) { + return createBoardBlocks((BoardNotificationInfo) additionalData); + } + break; + case NEW_MEMBERSHIP_FEE: + if (additionalData instanceof MembershipFeeNotificationInfo) { + return createMembershipFeeBlocks((MembershipFeeNotificationInfo) additionalData); + } + break; + case NEW_BOOK_LOAN_REQUEST: + if (additionalData instanceof BookLoanRecordNotificationInfo) { + return createBookLoanRecordBlocks((BookLoanRecordNotificationInfo) additionalData); + } + break; + default: + log.error("Unknown executives alert type: {}", alertType); + } + return Collections.emptyList(); + } + + private List createErrorBlocks(HttpServletRequest request, Exception e) { + String httpMethod = request.getMethod(); + String fullUrl = webhookCommonService.getFullUrl(request); + String username = webhookCommonService.getUsername(request); + String errorMessage = webhookCommonService.extractMessageAfterException(e); + String stackTrace = webhookCommonService.getStackTraceSummary(e); + + log.error("Server Error: {}", errorMessage); + + return Arrays.asList( + section(s -> s.text(markdownText(":firecracker: *Server Error*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*User:*\n" + username), + markdownText("*Endpoint:*\n[" + httpMethod + "] " + fullUrl) + ))), + section(s -> s.text(markdownText("*Error Message:*\n" + errorMessage))), + section(s -> s.text(markdownText("*Stack Trace:*\n```" + stackTrace + "```"))) + ); + } + + private List createSecurityAlertBlocks(HttpServletRequest request, AlertType alertType, + String additionalMessage) { + String clientIp = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + String fullUrl = webhookCommonService.getFullUrl(request); + String username = webhookCommonService.getUsername(request); + String location = webhookCommonService.getLocation(request); + + return Arrays.asList( + section(s -> s.text(markdownText(":imp: *" + alertType.getTitle() + "*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*User:*\n" + username), + markdownText("*IP Address:*\n" + clientIp), + markdownText("*Location:*\n" + location), + markdownText("*Endpoint:*\n" + fullUrl) + ))), + section(s -> s.text( + markdownText("*Details:*\n" + alertType.getDefaultMessage() + "\n" + additionalMessage))) + ); + } + + private List createAdminLoginBlocks(HttpServletRequest request, MemberLoginInfoDto loginMember) { + String clientIp = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + String location = webhookCommonService.getLocation(request); + + return Arrays.asList( + section(s -> s.text(markdownText(":mechanic: *" + loginMember.getRole().getDescription() + " Login*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*User:*\n" + loginMember.getMemberId() + " " + loginMember.getMemberName()), + markdownText("*IP Address:*\n" + clientIp), + markdownText("*Location:*\n" + location) + ))) + ); + } + + private List createApplicationBlocks(ApplicationRequestDto requestDto) { + List blocks = new ArrayList<>(); + + blocks.add(section(s -> s.text(markdownText(":sparkles: *동아리 지원*")))); + blocks.add(section(s -> s.fields(Arrays.asList( + markdownText("*구분:*\n" + requestDto.getApplicationType().getDescription()), + markdownText("*학번:*\n" + requestDto.getStudentId()), + markdownText("*이름:*\n" + requestDto.getName()), + markdownText("*학년:*\n" + requestDto.getGrade() + "학년"), + markdownText("*관심 분야:*\n" + requestDto.getInterests()) + )))); + + if (requestDto.getGithubUrl() != null && !requestDto.getGithubUrl().isEmpty()) { + blocks.add(actions(a -> a.elements(asElements( + button(b -> b.text(plainText(pt -> pt.emoji(true).text("Github"))) + .url(requestDto.getGithubUrl()) + .actionId("click_github")) + )))); + } + + return blocks; + } + + private List createBoardBlocks(BoardNotificationInfo board) { + return Arrays.asList( + section(s -> s.text(markdownText(":writing_hand: *새 게시글*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*제목:*\n" + board.getTitle()), + markdownText("*분류:*\n" + board.getCategory()), + markdownText("*작성자:*\n" + board.getUsername()) + ))) + ); + } + + private List createMembershipFeeBlocks(MembershipFeeNotificationInfo data) { + String username = data.getMemberId() + " " + data.getMemberName(); + + return Arrays.asList( + section(s -> s.text(markdownText(":dollar: *회비 신청*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*신청자:*\n" + username), + markdownText("*분류:*\n" + data.getCategory()), + markdownText("*금액:*\n" + data.getAmount() + "원") + ))), + section(s -> s.text(markdownText("*Content:*\n" + data.getContent()))) + ); + } + + private List createBookLoanRecordBlocks(BookLoanRecordNotificationInfo data) { + String username = data.getMemberId() + " " + data.getMemberName(); + + return Arrays.asList( + section(s -> s.text(markdownText(":books: *도서 대여 신청*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*도서명:*\n" + data.getBookTitle()), + markdownText("*분류:*\n" + data.getCategory()), + markdownText("*신청자:*\n" + username), + markdownText("*상태:*\n" + (data.isAvailable() ? "대여 가능" : "대여 중")) + ))) + ); + } + + private List createServerStartBlocks() { + String osInfo = webhookCommonService.getOperatingSystemInfo(); + String jdkVersion = webhookCommonService.getJavaRuntimeVersion(); + double cpuUsage = webhookCommonService.getCpuUsage(); + String memoryUsage = webhookCommonService.getMemoryUsage(); + + return Arrays.asList( + section(s -> s.text(markdownText(":battery: *Server Started*"))), + section(s -> s.fields(Arrays.asList( + markdownText("*Environment:* \n" + environment.getProperty("spring.profiles.active")), + markdownText("*OS:* \n" + osInfo), + markdownText("*JDK Version:* \n" + jdkVersion), + markdownText("*CPU Usage:* \n" + String.format("%.2f%%", cpuUsage)), + markdownText("*Memory Usage:* \n" + memoryUsage) + ))), + actions(a -> a.elements(asElements( + button(b -> b.text(plainText(pt -> pt.emoji(true).text("Web"))) + .url(commonProperties.getWebUrl()) + .value("click_web")), + button(b -> b.text(plainText(pt -> pt.emoji(true).text("API Docs"))) + .url(commonProperties.getApiUrl()) + .value("click_apiDocs")) + ))) + ); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/mapper/NotificationSettingDtoMapper.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/mapper/NotificationSettingDtoMapper.java new file mode 100644 index 000000000..4b092100a --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/mapper/NotificationSettingDtoMapper.java @@ -0,0 +1,16 @@ +package page.clab.api.global.common.notificationSetting.application.dto.mapper; + +import org.springframework.stereotype.Component; +import page.clab.api.global.common.notificationSetting.application.dto.response.NotificationSettingResponseDto; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +@Component +public class NotificationSettingDtoMapper { + + public NotificationSettingResponseDto toDto(NotificationSetting setting) { + return NotificationSettingResponseDto.builder() + .alertType(setting.getAlertType().getTitle()) + .enabled(setting.isEnabled()) + .build(); + } +} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/SlackBoardInfo.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/BoardNotificationInfo.java similarity index 58% rename from src/main/java/page/clab/api/global/common/slack/domain/SlackBoardInfo.java rename to src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/BoardNotificationInfo.java index a12be3cb8..4cf0a3442 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SlackBoardInfo.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/BoardNotificationInfo.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.application.dto.notification; import lombok.Builder; import lombok.Getter; @@ -7,17 +7,18 @@ @Getter @Builder -public class SlackBoardInfo { +public class BoardNotificationInfo { private String title; private String category; private String username; - public static SlackBoardInfo create(Board board, MemberDetailedInfoDto memberInfo) { - return SlackBoardInfo.builder() + public static BoardNotificationInfo create(Board board, MemberDetailedInfoDto memberInfo) { + return BoardNotificationInfo.builder() .title(board.getTitle()) .category(board.getCategory().getDescription()) - .username(board.isWantAnonymous() ? board.getNickname() : memberInfo.getMemberId() + " " + memberInfo.getMemberName()) + .username(board.isWantAnonymous() ? board.getNickname() + : memberInfo.getMemberId() + " " + memberInfo.getMemberName()) .build(); } } diff --git a/src/main/java/page/clab/api/global/common/slack/domain/SlackBookLoanRecordInfo.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/BookLoanRecordNotificationInfo.java similarity index 69% rename from src/main/java/page/clab/api/global/common/slack/domain/SlackBookLoanRecordInfo.java rename to src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/BookLoanRecordNotificationInfo.java index 1f961750f..036f1de32 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SlackBookLoanRecordInfo.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/BookLoanRecordNotificationInfo.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.application.dto.notification; import lombok.Builder; import lombok.Getter; @@ -7,7 +7,7 @@ @Getter @Builder -public class SlackBookLoanRecordInfo { +public class BookLoanRecordNotificationInfo { private String memberId; private String memberName; @@ -15,8 +15,8 @@ public class SlackBookLoanRecordInfo { private String category; private boolean isAvailable; - public static SlackBookLoanRecordInfo create(Book book, MemberBorrowerInfoDto borrowerInfo) { - return SlackBookLoanRecordInfo.builder() + public static BookLoanRecordNotificationInfo create(Book book, MemberBorrowerInfoDto borrowerInfo) { + return BookLoanRecordNotificationInfo.builder() .memberId(borrowerInfo.getMemberId()) .memberName(borrowerInfo.getMemberName()) .bookTitle(book.getTitle()) diff --git a/src/main/java/page/clab/api/global/common/slack/domain/SlackMembershipFeeInfo.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/MembershipFeeNotificationInfo.java similarity index 69% rename from src/main/java/page/clab/api/global/common/slack/domain/SlackMembershipFeeInfo.java rename to src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/MembershipFeeNotificationInfo.java index 02461c71d..65b6ed8f3 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SlackMembershipFeeInfo.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/notification/MembershipFeeNotificationInfo.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.application.dto.notification; import lombok.Builder; import lombok.Getter; @@ -7,7 +7,7 @@ @Getter @Builder -public class SlackMembershipFeeInfo { +public class MembershipFeeNotificationInfo { private String memberId; private String memberName; @@ -15,8 +15,8 @@ public class SlackMembershipFeeInfo { private Long amount; private String content; - public static SlackMembershipFeeInfo create(MembershipFee membershipFee, MemberBasicInfoDto memberInfo) { - return SlackMembershipFeeInfo.builder() + public static MembershipFeeNotificationInfo create(MembershipFee membershipFee, MemberBasicInfoDto memberInfo) { + return MembershipFeeNotificationInfo.builder() .memberId(memberInfo.getMemberId()) .memberName(memberInfo.getMemberName()) .category(membershipFee.getCategory()) diff --git a/src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/request/NotificationSettingToggleRequestDto.java similarity index 78% rename from src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java rename to src/main/java/page/clab/api/global/common/notificationSetting/application/dto/request/NotificationSettingToggleRequestDto.java index fcd941924..172cc146d 100644 --- a/src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/request/NotificationSettingToggleRequestDto.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.dto.request; +package page.clab.api.global.common.notificationSetting.application.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; @@ -7,7 +7,7 @@ @Getter @Setter -public class NotificationSettingUpdateRequestDto { +public class NotificationSettingToggleRequestDto { @NotNull(message = "{notNull.notificationSetting.alertType}") @Schema(description = "알림 타입", example = "서버 시작") diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/response/NotificationSettingResponseDto.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/response/NotificationSettingResponseDto.java new file mode 100644 index 000000000..8eeaf2256 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/dto/response/NotificationSettingResponseDto.java @@ -0,0 +1,12 @@ +package page.clab.api.global.common.notificationSetting.application.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class NotificationSettingResponseDto { + + private String alertType; + private boolean enabled; +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/event/ApplicationStartupListener.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/event/ApplicationStartupListener.java new file mode 100644 index 000000000..0f9f08a6d --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/event/ApplicationStartupListener.java @@ -0,0 +1,23 @@ +package page.clab.api.global.common.notificationSetting.application.event; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType; + +@Component +public class ApplicationStartupListener { + + private final ApplicationEventPublisher eventPublisher; + + public ApplicationStartupListener(ApplicationEventPublisher eventPublisher) { + this.eventPublisher = eventPublisher; + } + + @EventListener(ContextRefreshedEvent.class) + public void onApplicationEvent(ContextRefreshedEvent event) { + eventPublisher.publishEvent( + new NotificationEvent(this, GeneralAlertType.SERVER_START, null, null)); + } +} diff --git a/src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationEvent.java similarity index 59% rename from src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java rename to src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationEvent.java index 59bfb5158..4493c8fa6 100644 --- a/src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationEvent.java @@ -1,21 +1,19 @@ -package page.clab.api.global.common.slack.event; +package page.clab.api.global.common.notificationSetting.application.event; import jakarta.servlet.http.HttpServletRequest; import lombok.Getter; import org.springframework.context.ApplicationEvent; -import page.clab.api.global.common.slack.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.AlertType; @Getter public class NotificationEvent extends ApplicationEvent { - private final String webhookUrl; private final AlertType alertType; private final HttpServletRequest request; private final Object additionalData; - public NotificationEvent(Object source, String webhookUrl, AlertType alertType, HttpServletRequest request, Object additionalData) { + public NotificationEvent(Object source, AlertType alertType, HttpServletRequest request, Object additionalData) { super(source); - this.webhookUrl = webhookUrl; this.alertType = alertType; this.request = request; this.additionalData = additionalData; diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationListener.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationListener.java new file mode 100644 index 000000000..56c2eda22 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/event/NotificationListener.java @@ -0,0 +1,95 @@ +package page.clab.api.global.common.notificationSetting.application.event; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import page.clab.api.global.common.notificationSetting.application.port.in.ManageNotificationSettingUseCase; +import page.clab.api.global.common.notificationSetting.application.port.out.NotificationSender; +import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties; +import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties.PlatformConfig; +import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties.PlatformMapping; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +@Component +@Slf4j +public class NotificationListener { + + private final ManageNotificationSettingUseCase manageNotificationSettingUseCase; + private final Map notificationSenders; + private final NotificationConfigProperties notificationConfigProperties; + + public NotificationListener( + ManageNotificationSettingUseCase manageNotificationSettingUseCase, + List notificationSenderList, + NotificationConfigProperties notificationConfigProperties) { + this.manageNotificationSettingUseCase = manageNotificationSettingUseCase; + this.notificationConfigProperties = notificationConfigProperties; + this.notificationSenders = notificationSenderList.stream() + .collect(Collectors.toMap(NotificationSender::getPlatformName, Function.identity())); + } + + @EventListener + public void handleNotificationEvent(NotificationEvent event) { + AlertType alertType = event.getAlertType(); + + NotificationSetting setting = manageNotificationSettingUseCase.getOrCreateDefaultSetting(alertType); + if (!setting.isEnabled()) { + return; + } + + List mappings = getMappingsForAlertType(alertType); + if (mappings.isEmpty()) { + return; + } + + mappings.forEach(mapping -> getWebhookUrl(mapping) + .ifPresent(webhookUrl -> sendNotification(mapping.getPlatform(), event, webhookUrl))); + } + + private List getMappingsForAlertType(AlertType alertType) { + String categoryName = alertType.getCategory().name(); + Map> categoryMappings = notificationConfigProperties.getCategoryMappings(); + + return Optional.ofNullable(categoryMappings.get(categoryName)) + .filter(list -> !list.isEmpty()) + .orElseGet(notificationConfigProperties::getDefaultMappings); + } + + private Optional getWebhookUrl(PlatformMapping mapping) { + String platform = mapping.getPlatform(); + String webhookKey = mapping.getWebhook(); + Map platforms = notificationConfigProperties.getPlatforms(); + + return Optional.ofNullable(platforms.get(platform)) + .map(platformConfig -> platformConfig.getWebhooks().get(webhookKey)) + .map(url -> { + log.debug("Found webhook URL for platform '{}', key '{}': {}", platform, webhookKey, url); + return url; + }) + .or(() -> { + log.warn("No webhook URL found for platform '{}', key '{}'", platform, webhookKey); + return Optional.empty(); + }); + } + + private void sendNotification(String platform, NotificationEvent event, String webhookUrl) { + NotificationSender sender = notificationSenders.get(platform); + if (sender == null) { + log.warn("No NotificationSender found for platform: {}", platform); + return; + } + + try { + sender.sendNotification(event, webhookUrl); + log.debug("Notification sent via platform: {}", platform); + } catch (Exception e) { + log.error("Failed to send notification via platform: {}", platform, e); + } + } +} diff --git a/src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/exception/AlertTypeNotFoundException.java similarity index 71% rename from src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java rename to src/main/java/page/clab/api/global/common/notificationSetting/application/exception/AlertTypeNotFoundException.java index c52c39796..94de58360 100644 --- a/src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/exception/AlertTypeNotFoundException.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.exception; +package page.clab.api.global.common.notificationSetting.application.exception; public class AlertTypeNotFoundException extends RuntimeException { diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/ManageNotificationSettingUseCase.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/ManageNotificationSettingUseCase.java new file mode 100644 index 000000000..338de0f31 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/ManageNotificationSettingUseCase.java @@ -0,0 +1,11 @@ +package page.clab.api.global.common.notificationSetting.application.port.in; + +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +public interface ManageNotificationSettingUseCase { + + void toggleNotificationSetting(String alertTypeName, boolean enabled); + + NotificationSetting getOrCreateDefaultSetting(AlertType alertType); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/RetrieveNotificationSettingUseCase.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/RetrieveNotificationSettingUseCase.java new file mode 100644 index 000000000..33bfde964 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/in/RetrieveNotificationSettingUseCase.java @@ -0,0 +1,9 @@ +package page.clab.api.global.common.notificationSetting.application.port.in; + +import java.util.List; +import page.clab.api.global.common.notificationSetting.application.dto.response.NotificationSettingResponseDto; + +public interface RetrieveNotificationSettingUseCase { + + List retrieveNotificationSettings(); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/NotificationSender.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/NotificationSender.java new file mode 100644 index 000000000..23b9f55c3 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/NotificationSender.java @@ -0,0 +1,10 @@ +package page.clab.api.global.common.notificationSetting.application.port.out; + +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; + +public interface NotificationSender { + + String getPlatformName(); + + void sendNotification(NotificationEvent event, String webhookUrl); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/RetrieveNotificationSettingPort.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/RetrieveNotificationSettingPort.java new file mode 100644 index 000000000..9b81d12b2 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/RetrieveNotificationSettingPort.java @@ -0,0 +1,13 @@ +package page.clab.api.global.common.notificationSetting.application.port.out; + +import java.util.List; +import java.util.Optional; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +public interface RetrieveNotificationSettingPort { + + List findAll(); + + Optional findByAlertType(AlertType alertType); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/UpdateNotificationSettingPort.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/UpdateNotificationSettingPort.java new file mode 100644 index 000000000..144205601 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/UpdateNotificationSettingPort.java @@ -0,0 +1,8 @@ +package page.clab.api.global.common.notificationSetting.application.port.out; + +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +public interface UpdateNotificationSettingPort { + + NotificationSetting save(NotificationSetting setting); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/WebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/WebhookClient.java new file mode 100644 index 000000000..c85e5776d --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/port/out/WebhookClient.java @@ -0,0 +1,23 @@ +package page.clab.api.global.common.notificationSetting.application.port.out; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.concurrent.CompletableFuture; +import page.clab.api.global.common.notificationSetting.domain.AlertType; + +/** + * WebhookClient는 외부 시스템(Discord, Slack 등)과 통신하기 위한 포트 인터페이스입니다. + */ +public interface WebhookClient { + + /** + * 특정 알림 유형과 요청 정보, 추가 데이터를 사용하여 메시지를 비동기적으로 전송합니다. + * + * @param webhookUrl 메시지를 보낼 Webhook URL + * @param alertType 알림 유형 + * @param request 클라이언트 요청 정보 + * @param additionalData 추가 데이터 + * @return 메시지 전송 성공 여부를 나타내는 CompletableFuture + */ + CompletableFuture sendMessage(String webhookUrl, AlertType alertType, HttpServletRequest request, + Object additionalData); +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/service/ManageNotificationSettingService.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/service/ManageNotificationSettingService.java new file mode 100644 index 000000000..d42337ddf --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/service/ManageNotificationSettingService.java @@ -0,0 +1,52 @@ +package page.clab.api.global.common.notificationSetting.application.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.global.common.notificationSetting.application.port.in.ManageNotificationSettingUseCase; +import page.clab.api.global.common.notificationSetting.application.port.out.RetrieveNotificationSettingPort; +import page.clab.api.global.common.notificationSetting.application.port.out.UpdateNotificationSettingPort; +import page.clab.api.global.common.notificationSetting.domain.AlertType; +import page.clab.api.global.common.notificationSetting.domain.AlertTypeResolver; +import page.clab.api.global.common.notificationSetting.domain.NotificationSetting; + +/** + * {@code UpdateNotificationSettingService}는 알림 설정을 업데이트하는 서비스입니다. + * + *

이 서비스는 주어진 알림 유형에 따라 활성화 또는 비활성화할 수 있는 설정을 업데이트할 수 있습니다. + * 또한, 기본 알림 설정이 존재하지 않으면 생성하여 제공합니다.

+ *

+ * 주요 기능: + *

    + *
  • {@link #toggleNotificationSetting(String, boolean)} - 주어진 알림 유형에 대해 알림 설정을 업데이트합니다.
  • + *
  • {@link #getOrCreateDefaultSetting(AlertType)} - 주어진 알림 유형에 대한 기본 알림 설정을 조회하거나, 존재하지 않으면 생성합니다.
  • + *
+ */ +@Service +@RequiredArgsConstructor +public class ManageNotificationSettingService implements ManageNotificationSettingUseCase { + + private final AlertTypeResolver alertTypeResolver; + private final RetrieveNotificationSettingPort retrieveNotificationSettingPort; + private final UpdateNotificationSettingPort updateNotificationSettingPort; + + @Transactional + @Override + public void toggleNotificationSetting(String alertTypeName, boolean enabled) { + AlertType alertType = alertTypeResolver.resolve(alertTypeName); + NotificationSetting setting = getOrCreateDefaultSetting(alertType); + setting.updateEnabled(enabled); + updateNotificationSettingPort.save(setting); + } + + @Transactional + public NotificationSetting getOrCreateDefaultSetting(AlertType alertType) { + return retrieveNotificationSettingPort.findByAlertType(alertType) + .orElseGet(() -> createAndSaveDefaultSetting(alertType)); + } + + private NotificationSetting createAndSaveDefaultSetting(AlertType alertType) { + NotificationSetting defaultSetting = NotificationSetting.createDefault(alertType); + return updateNotificationSettingPort.save(defaultSetting); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/service/RetrieveNotificationSettingService.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/service/RetrieveNotificationSettingService.java new file mode 100644 index 000000000..3d4d6bc32 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/service/RetrieveNotificationSettingService.java @@ -0,0 +1,36 @@ +package page.clab.api.global.common.notificationSetting.application.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.global.common.notificationSetting.application.dto.mapper.NotificationSettingDtoMapper; +import page.clab.api.global.common.notificationSetting.application.dto.response.NotificationSettingResponseDto; +import page.clab.api.global.common.notificationSetting.application.port.in.RetrieveNotificationSettingUseCase; +import page.clab.api.global.common.notificationSetting.application.port.out.RetrieveNotificationSettingPort; + +/** + * {@code RetrieveNotificationSettingService}는 알림 설정을 조회하는 서비스입니다. + * + *

이 서비스는 알림 설정의 전체 목록을 조회할 수 있는 기능을 제공합니다.

+ *

+ * 주요 기능: + *

    + *
  • {@link #retrieveNotificationSettings()} - 모든 알림 설정을 조회합니다.
  • + *
+ */ +@Service +@RequiredArgsConstructor +public class RetrieveNotificationSettingService implements RetrieveNotificationSettingUseCase { + + private final RetrieveNotificationSettingPort retrieveNotificationSettingPort; + private final NotificationSettingDtoMapper mapper; + + @Transactional(readOnly = true) + @Override + public List retrieveNotificationSettings() { + return retrieveNotificationSettingPort.findAll().stream() + .map(mapper::toDto) + .toList(); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/application/service/WebhookCommonService.java b/src/main/java/page/clab/api/global/common/notificationSetting/application/service/WebhookCommonService.java new file mode 100644 index 000000000..0cb574aa0 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/application/service/WebhookCommonService.java @@ -0,0 +1,87 @@ +package page.clab.api.global.common.notificationSetting.application.service; + +import io.ipinfo.api.model.IPResponse; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; +import jakarta.servlet.http.HttpServletRequest; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.lang.management.OperatingSystemMXBean; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +/** + * {@code WebhookCommonService}는 Webhook 클라이언트에서 공통으로 사용되는 로직을 제공합니다. + */ +@Service +@RequiredArgsConstructor +public class WebhookCommonService { + + private final AttributeStrategy attributeStrategy; + + public String getFullUrl(HttpServletRequest request) { + String requestUrl = request.getRequestURI(); + String queryString = request.getQueryString(); + return queryString == null ? requestUrl : requestUrl + "?" + queryString; + } + + public String extractMessageAfterException(Exception e) { + String errorMessage = Optional.ofNullable(e.getMessage()).orElse("No error message provided"); + String exceptionIndicator = "Exception:"; + int index = errorMessage.indexOf(exceptionIndicator); + return index == -1 ? errorMessage : errorMessage.substring(index + exceptionIndicator.length()).trim(); + } + + public String getStackTraceSummary(Exception e) { + return Arrays.stream(e.getStackTrace()) + .limit(10) + .map(StackTraceElement::toString) + .collect(Collectors.joining("\n")); + } + + public String getOperatingSystemInfo() { + String osName = System.getProperty("os.name"); + String osVersion = System.getProperty("os.version"); + return osName + " " + osVersion; + } + + public String getJavaRuntimeVersion() { + return System.getProperty("java.version"); + } + + public double getCpuUsage() { + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + int processors = osBean.getAvailableProcessors(); + double systemLoadAverage = osBean.getSystemLoadAverage(); + return (systemLoadAverage / processors) * 100; + } + + public String getMemoryUsage() { + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage memoryUsage = memoryMXBean.getHeapMemoryUsage(); + + long used = memoryUsage.getUsed() / (1024 * 1024); + long max = memoryUsage.getMax() / (1024 * 1024); + + return String.format("%dMB / %dMB (%.2f%%)", used, max, ((double) used / max) * 100); + } + + public String getUsername(HttpServletRequest request) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return Optional.ofNullable(request.getAttribute("member")) + .map(Object::toString) + .orElseGet(() -> Optional.ofNullable(auth) + .map(Authentication::getName) + .orElse("anonymous")); + } + + public String getLocation(HttpServletRequest request) { + IPResponse ipResponse = attributeStrategy.getAttribute(request); + return ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfig.java b/src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfig.java new file mode 100644 index 000000000..b398d8377 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfig.java @@ -0,0 +1,13 @@ +package page.clab.api.global.common.notificationSetting.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class NotificationConfig { + + @Bean + public NotificationConfigProperties notificationConfigProperties() { + return new NotificationConfigProperties(); + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfigProperties.java b/src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfigProperties.java new file mode 100644 index 000000000..a052ac17e --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/config/NotificationConfigProperties.java @@ -0,0 +1,45 @@ +package page.clab.api.global.common.notificationSetting.config; + +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "notification") +@Getter +@Setter +public class NotificationConfigProperties { + + private CommonProperties common; + private Map platforms; + private Map> categoryMappings; + private List defaultMappings; + + @Getter + @Setter + public static class CommonProperties { + private String webUrl; + private String apiUrl; + private String color; + + public int getColorAsInt() { + return Integer.parseInt(color.replaceFirst("^#", ""), 16); + } + } + + @Getter + @Setter + public static class PlatformConfig { + private Map webhooks; + } + + @Getter + @Setter + public static class PlatformMapping { + private String platform; + private String webhook; + } +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertCategory.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertCategory.java new file mode 100644 index 000000000..1bbbb3a72 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertCategory.java @@ -0,0 +1,8 @@ +package page.clab.api.global.common.notificationSetting.domain; + +public enum AlertCategory { + + GENERAL, + SECURITY, + EXECUTIVES +} diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertType.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertType.java new file mode 100644 index 000000000..8cb5326ad --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertType.java @@ -0,0 +1,10 @@ +package page.clab.api.global.common.notificationSetting.domain; + +public interface AlertType { + + String getTitle(); + + String getDefaultMessage(); + + AlertCategory getCategory(); +} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertTypeConverter.java similarity index 88% rename from src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java rename to src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertTypeConverter.java index 1755b529b..dfa397238 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertTypeConverter.java @@ -1,11 +1,10 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.domain; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; -import page.clab.api.global.common.slack.exception.AlertTypeNotFoundException; - import java.util.HashMap; import java.util.Map; +import page.clab.api.global.common.notificationSetting.application.exception.AlertTypeNotFoundException; @Converter(autoApply = true) public class AlertTypeConverter implements AttributeConverter { diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertTypeResolver.java similarity index 77% rename from src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java rename to src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertTypeResolver.java index f2d580962..7fe3cc2dd 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/AlertTypeResolver.java @@ -1,7 +1,7 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.domain; import org.springframework.stereotype.Service; -import page.clab.api.global.common.slack.exception.AlertTypeNotFoundException; +import page.clab.api.global.common.notificationSetting.application.exception.AlertTypeNotFoundException; @Service public class AlertTypeResolver { diff --git a/src/main/java/page/clab/api/global/common/slack/domain/ExecutivesAlertType.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/ExecutivesAlertType.java similarity index 54% rename from src/main/java/page/clab/api/global/common/slack/domain/ExecutivesAlertType.java rename to src/main/java/page/clab/api/global/common/notificationSetting/domain/ExecutivesAlertType.java index 703b2974f..cba124e18 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/ExecutivesAlertType.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/ExecutivesAlertType.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.domain; import lombok.AllArgsConstructor; import lombok.Getter; @@ -7,11 +7,12 @@ @AllArgsConstructor public enum ExecutivesAlertType implements AlertType { - NEW_APPLICATION("새 지원서", "New application has been submitted."), - NEW_BOARD("새 게시글", "New board has been posted."), - NEW_MEMBERSHIP_FEE("새 회비 신청", "New membership fee has been submitted."), - NEW_BOOK_LOAN_REQUEST("도서 대출 신청", "New book loan request has been submitted."); + NEW_APPLICATION("새 지원서", "New application has been submitted.", AlertCategory.EXECUTIVES), + NEW_BOARD("새 게시글", "New board has been posted.", AlertCategory.EXECUTIVES), + NEW_MEMBERSHIP_FEE("새 회비 신청", "New membership fee has been submitted.", AlertCategory.EXECUTIVES), + NEW_BOOK_LOAN_REQUEST("도서 대출 신청", "New book loan request has been submitted.", AlertCategory.EXECUTIVES); private final String title; private final String defaultMessage; + private final AlertCategory category; } diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/domain/GeneralAlertType.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/GeneralAlertType.java new file mode 100644 index 000000000..f8854c56e --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/GeneralAlertType.java @@ -0,0 +1,17 @@ +package page.clab.api.global.common.notificationSetting.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GeneralAlertType implements AlertType { + + ADMIN_LOGIN("관리자 로그인", "Admin login.", AlertCategory.GENERAL), + SERVER_START("서버 시작", "Server has been started.", AlertCategory.GENERAL), + SERVER_ERROR("서버 에러", "Server error occurred.", AlertCategory.GENERAL); + + private final String title; + private final String defaultMessage; + private final AlertCategory category; +} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/NotificationSetting.java similarity index 94% rename from src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java rename to src/main/java/page/clab/api/global/common/notificationSetting/domain/NotificationSetting.java index c352008a2..d9bd8f47f 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/NotificationSetting.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.domain; import jakarta.persistence.Convert; import jakarta.persistence.Entity; diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/domain/PlatformType.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/PlatformType.java new file mode 100644 index 000000000..5ed4c6d32 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/PlatformType.java @@ -0,0 +1,14 @@ +package page.clab.api.global.common.notificationSetting.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PlatformType { + + SLACK("slack"), + DISCORD("discord"); + + private final String name; +} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java b/src/main/java/page/clab/api/global/common/notificationSetting/domain/SecurityAlertType.java similarity index 58% rename from src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java rename to src/main/java/page/clab/api/global/common/notificationSetting/domain/SecurityAlertType.java index 4aa4d1b59..5a6fb8800 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java +++ b/src/main/java/page/clab/api/global/common/notificationSetting/domain/SecurityAlertType.java @@ -1,4 +1,4 @@ -package page.clab.api.global.common.slack.domain; +package page.clab.api.global.common.notificationSetting.domain; import lombok.AllArgsConstructor; import lombok.Getter; @@ -7,20 +7,21 @@ @AllArgsConstructor public enum SecurityAlertType implements AlertType { - ABNORMAL_ACCESS("비정상적인 접근", "Unexpected access pattern detected."), - REPEATED_LOGIN_FAILURES("지속된 로그인 실패", "Multiple consecutive failed login attempts."), - DUPLICATE_LOGIN("중복 로그인", "Duplicate login attempt."), - API_DOCS_ACCESS("API 문서 접근", "API Documentation access attempt."), - ACTUATOR_ACCESS("Actuator 접근", "Actuator endpoint access attempt."), - UNAUTHORIZED_ACCESS("인가되지 않은 접근", "Unauthorized access attempt."), - BLACKLISTED_IP_ADDED("블랙리스트 IP 등록", "IP address has been added to the blacklist."), - BLACKLISTED_IP_REMOVED("블랙리스트 IP 해제", "IP address has been removed from the blacklist."), - ABNORMAL_ACCESS_IP_BLOCKED("비정상적인 접근 IP 차단", "Abnormal access IP has been blocked."), - ABNORMAL_ACCESS_IP_DELETED("비정상적인 접근 IP 삭제", "Abnormal access IP has been deleted."), - MEMBER_BANNED("멤버 밴 등록", "Member has been banned."), - MEMBER_UNBANNED("멤버 밴 해제", "Member has been unbanned."), - MEMBER_ROLE_CHANGED("멤버 권한 변경", "Member role has been changed."); + ABNORMAL_ACCESS("비정상적인 접근", "Unexpected access pattern detected.", AlertCategory.SECURITY), + REPEATED_LOGIN_FAILURES("지속된 로그인 실패", "Multiple consecutive failed login attempts.", AlertCategory.SECURITY), + DUPLICATE_LOGIN("중복 로그인", "Duplicate login attempt.", AlertCategory.SECURITY), + API_DOCS_ACCESS("API 문서 접근", "API Documentation access attempt.", AlertCategory.SECURITY), + ACTUATOR_ACCESS("Actuator 접근", "Actuator endpoint access attempt.", AlertCategory.SECURITY), + UNAUTHORIZED_ACCESS("인가되지 않은 접근", "Unauthorized access attempt.", AlertCategory.SECURITY), + BLACKLISTED_IP_ADDED("블랙리스트 IP 등록", "IP address has been added to the blacklist.", AlertCategory.SECURITY), + BLACKLISTED_IP_REMOVED("블랙리스트 IP 해제", "IP address has been removed from the blacklist.", AlertCategory.SECURITY), + ABNORMAL_ACCESS_IP_BLOCKED("비정상적인 접근 IP 차단", "Abnormal access IP has been blocked.", AlertCategory.SECURITY), + ABNORMAL_ACCESS_IP_DELETED("비정상적인 접근 IP 삭제", "Abnormal access IP has been deleted.", AlertCategory.SECURITY), + MEMBER_BANNED("멤버 밴 등록", "Member has been banned.", AlertCategory.SECURITY), + MEMBER_UNBANNED("멤버 밴 해제", "Member has been unbanned.", AlertCategory.SECURITY), + MEMBER_ROLE_CHANGED("멤버 권한 변경", "Member role has been changed.", AlertCategory.SECURITY); private final String title; private final String defaultMessage; + private final AlertCategory category; } diff --git a/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java b/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java deleted file mode 100644 index f242289d1..000000000 --- a/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java +++ /dev/null @@ -1,45 +0,0 @@ -package page.clab.api.global.common.slack.api; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import page.clab.api.global.common.dto.ApiResponse; -import page.clab.api.global.common.slack.application.NotificationSettingService; -import page.clab.api.global.common.slack.dto.request.NotificationSettingUpdateRequestDto; -import page.clab.api.global.common.slack.dto.response.NotificationSettingResponseDto; - -import java.util.List; - -@RestController -@RequestMapping("/api/v1/notification-settings") -@RequiredArgsConstructor -@Tag(name = "Notification Setting", description = "알림 설정") -public class NotificationSettingController { - - private final NotificationSettingService notificationSettingService; - - @Operation(summary = "[S] 슬랙 알림 조회", description = "ROLE_SUPER 이상의 권한이 필요함") - @PreAuthorize("hasRole('SUPER')") - @GetMapping("") - public ApiResponse> getNotificationSettings() { - List notificationSettings = notificationSettingService.getNotificationSettings(); - return ApiResponse.success(notificationSettings); - } - - @Operation(summary = "[S] 슬랙 알림 설정 변경", description = "ROLE_SUPER 이상의 권한이 필요함") - @PreAuthorize("hasRole('SUPER')") - @PutMapping("") - public ApiResponse updateNotificationSetting( - @Valid @RequestBody NotificationSettingUpdateRequestDto requestDto - ) { - notificationSettingService.updateNotificationSetting(requestDto.getAlertType(), requestDto.isEnabled()); - return ApiResponse.success(); - } -} diff --git a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java deleted file mode 100644 index 3d38bc026..000000000 --- a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java +++ /dev/null @@ -1,46 +0,0 @@ -package page.clab.api.global.common.slack.application; - -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import page.clab.api.global.common.slack.dao.NotificationSettingRepository; -import page.clab.api.global.common.slack.domain.AlertType; -import page.clab.api.global.common.slack.domain.AlertTypeResolver; -import page.clab.api.global.common.slack.domain.NotificationSetting; -import page.clab.api.global.common.slack.dto.response.NotificationSettingResponseDto; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class NotificationSettingService { - - private final AlertTypeResolver alertTypeResolver; - private final NotificationSettingRepository settingRepository; - - @Transactional(readOnly = true) - public List getNotificationSettings() { - return settingRepository.findAll().stream() - .map(NotificationSettingResponseDto::toDto) - .toList(); - } - - @Transactional - public void updateNotificationSetting(String alertTypeName, boolean enabled) { - AlertType alertType = alertTypeResolver.resolve(alertTypeName); - NotificationSetting setting = getOrCreateDefaultSetting(alertType); - setting.updateEnabled(enabled); - settingRepository.save(setting); - } - - @Transactional - public NotificationSetting getOrCreateDefaultSetting(AlertType alertType) { - return settingRepository.findByAlertType(alertType) - .orElseGet(() -> createAndSaveDefaultSetting(alertType)); - } - - private NotificationSetting createAndSaveDefaultSetting(AlertType alertType) { - NotificationSetting defaultSetting = NotificationSetting.createDefault(alertType); - return settingRepository.save(defaultSetting); - } -} diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java deleted file mode 100644 index 7e9054fcc..000000000 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java +++ /dev/null @@ -1,64 +0,0 @@ -package page.clab.api.global.common.slack.application; - -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; -import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto; -import page.clab.api.global.common.slack.domain.ExecutivesAlertType; -import page.clab.api.global.common.slack.domain.GeneralAlertType; -import page.clab.api.global.common.slack.domain.SecurityAlertType; -import page.clab.api.global.common.slack.domain.SlackBoardInfo; -import page.clab.api.global.common.slack.domain.SlackBookLoanRecordInfo; -import page.clab.api.global.common.slack.domain.SlackMembershipFeeInfo; -import page.clab.api.global.common.slack.event.NotificationEvent; -import page.clab.api.global.config.SlackConfig; - -@Service -public class SlackService { - - private final ApplicationEventPublisher eventPublisher; - private final String coreTeamWebhookUrl; - private final String executivesWebhookUrl; - - public SlackService(ApplicationEventPublisher eventPublisher, SlackConfig slackConfig) { - this.eventPublisher = eventPublisher; - this.coreTeamWebhookUrl = slackConfig.getCoreTeamWebhookUrl(); - this.executivesWebhookUrl = slackConfig.getExecutivesWebhookUrl(); - } - - public void sendServerErrorNotification(HttpServletRequest request, Exception e) { - eventPublisher.publishEvent(new NotificationEvent(this, coreTeamWebhookUrl, GeneralAlertType.SERVER_ERROR, request, e)); - } - - public void sendSecurityAlertNotification(HttpServletRequest request, SecurityAlertType alertType, String additionalMessage) { - eventPublisher.publishEvent(new NotificationEvent(this, coreTeamWebhookUrl, alertType, request, additionalMessage)); - } - - public void sendAdminLoginNotification(HttpServletRequest request, MemberLoginInfoDto loginMember) { - eventPublisher.publishEvent(new NotificationEvent(this, coreTeamWebhookUrl, GeneralAlertType.ADMIN_LOGIN, request, loginMember)); - } - - public void sendNewApplicationNotification(ApplicationRequestDto applicationRequestDto) { - eventPublisher.publishEvent(new NotificationEvent(this, executivesWebhookUrl, ExecutivesAlertType.NEW_APPLICATION, null, applicationRequestDto)); - } - - public void sendNewBoardNotification(SlackBoardInfo board) { - eventPublisher.publishEvent(new NotificationEvent(this, executivesWebhookUrl, ExecutivesAlertType.NEW_BOARD, null, board)); - } - - public void sendNewMembershipFeeNotification(SlackMembershipFeeInfo membershipFee) { - eventPublisher.publishEvent(new NotificationEvent(this, executivesWebhookUrl, ExecutivesAlertType.NEW_MEMBERSHIP_FEE, null, membershipFee)); - } - - public void sendNewBookLoanRequestNotification(SlackBookLoanRecordInfo bookLoanRecord) { - eventPublisher.publishEvent(new NotificationEvent(this, executivesWebhookUrl, ExecutivesAlertType.NEW_BOOK_LOAN_REQUEST, null, bookLoanRecord)); - } - - @EventListener(ContextRefreshedEvent.class) - public void sendServerStartNotification() { - eventPublisher.publishEvent(new NotificationEvent(this, coreTeamWebhookUrl, GeneralAlertType.SERVER_START, null, null)); - } -} diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java deleted file mode 100644 index 95b846eb4..000000000 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java +++ /dev/null @@ -1,327 +0,0 @@ -package page.clab.api.global.common.slack.application; - -import com.slack.api.Slack; -import com.slack.api.model.Attachment; -import static com.slack.api.model.block.Blocks.actions; -import static com.slack.api.model.block.Blocks.section; -import com.slack.api.model.block.LayoutBlock; -import static com.slack.api.model.block.composition.BlockCompositions.markdownText; -import static com.slack.api.model.block.composition.BlockCompositions.plainText; -import static com.slack.api.model.block.element.BlockElements.asElements; -import static com.slack.api.model.block.element.BlockElements.button; -import com.slack.api.webhook.Payload; -import com.slack.api.webhook.WebhookResponse; -import io.ipinfo.api.model.IPResponse; -import io.ipinfo.spring.strategies.attribute.AttributeStrategy; -import jakarta.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; -import org.springframework.core.env.Environment; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto; -import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto; -import page.clab.api.global.common.slack.domain.AlertType; -import page.clab.api.global.common.slack.domain.ExecutivesAlertType; -import page.clab.api.global.common.slack.domain.GeneralAlertType; -import page.clab.api.global.common.slack.domain.SecurityAlertType; -import page.clab.api.global.common.slack.domain.SlackBoardInfo; -import page.clab.api.global.common.slack.domain.SlackBookLoanRecordInfo; -import page.clab.api.global.common.slack.domain.SlackMembershipFeeInfo; -import page.clab.api.global.config.SlackConfig; -import page.clab.api.global.util.HttpReqResUtil; - -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.lang.management.MemoryUsage; -import java.lang.management.OperatingSystemMXBean; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -@Component -@Slf4j -public class SlackServiceHelper { - - private final Slack slack; - private final String webUrl; - private final String apiUrl; - private final String color; - private final Environment environment; - private final AttributeStrategy attributeStrategy; - - public SlackServiceHelper(SlackConfig slackConfig, Environment environment, AttributeStrategy attributeStrategy) { - this.slack = slackConfig.slack(); - this.webUrl = slackConfig.getWebUrl(); - this.apiUrl = slackConfig.getApiUrl(); - this.color = slackConfig.getColor(); - this.environment = environment; - this.attributeStrategy = attributeStrategy; - } - - public CompletableFuture sendSlackMessage(String webhookUrl, AlertType alertType, HttpServletRequest request, Object additionalData) { - List blocks = createBlocks(alertType, request, additionalData); - return CompletableFuture.supplyAsync(() -> { - Payload payload = Payload.builder() - .blocks(List.of(blocks.getFirst())) - .attachments(Collections.singletonList( - Attachment.builder() - .color(color) - .blocks(blocks.subList(1, blocks.size())) - .build() - )).build(); - try { - WebhookResponse response = slack.send(webhookUrl, payload); - if (response.getCode() == 200) { - return true; - } else { - log.error("Slack notification failed: {}", response.getMessage()); - return false; - } - } catch (IOException e) { - log.error("Failed to send Slack message: {}", e.getMessage(), e); - return false; - } - }); - } - - public List createBlocks(AlertType alertType, HttpServletRequest request, Object additionalData) { - if (alertType instanceof SecurityAlertType) { - return createSecurityAlertBlocks(request, alertType, additionalData.toString()); - } else if (alertType instanceof GeneralAlertType) { - switch ((GeneralAlertType) alertType) { - case ADMIN_LOGIN: - if (additionalData instanceof MemberLoginInfoDto) { - return createAdminLoginBlocks(request, (MemberLoginInfoDto) additionalData); - } - break; - case SERVER_START: - return createServerStartBlocks(); - case SERVER_ERROR: - if (additionalData instanceof Exception) { - return createErrorBlocks(request, (Exception) additionalData); - } - break; - default: - log.error("Unknown alert type: {}", alertType); - return List.of(); - } - } else if (alertType instanceof ExecutivesAlertType) { - switch ((ExecutivesAlertType) alertType) { - case NEW_APPLICATION: - if (additionalData instanceof ApplicationRequestDto) { - return createApplicationBlocks((ApplicationRequestDto) additionalData); - } - break; - case NEW_BOARD: - if (additionalData instanceof SlackBoardInfo) { - return createBoardBlocks((SlackBoardInfo) additionalData); - } - break; - case NEW_MEMBERSHIP_FEE: - if (additionalData instanceof SlackMembershipFeeInfo) { - return createMembershipFeeBlocks((SlackMembershipFeeInfo) additionalData); - } - break; - case NEW_BOOK_LOAN_REQUEST: - if (additionalData instanceof SlackBookLoanRecordInfo) { - return createBookLoanRecordBlocks((SlackBookLoanRecordInfo) additionalData); - } - break; - default: - log.error("Unknown alert type: {}", alertType); - return List.of(); - } - } - return List.of(); - } - - private List createErrorBlocks(HttpServletRequest request, Exception e) { - String httpMethod = request.getMethod(); - String requestUrl = request.getRequestURI(); - String queryString = request.getQueryString(); - String fullUrl = queryString == null ? requestUrl : requestUrl + "?" + queryString; - String username = getUsername(request); - - String errorMessage = e.getMessage() == null ? "No error message provided" : e.getMessage(); - String detailedMessage = extractMessageAfterException(errorMessage); - log.error("Server Error: {}", detailedMessage); - return Arrays.asList( - section(section -> section.text(markdownText(":firecracker: *Server Error*"))), - section(section -> section.fields(Arrays.asList( - markdownText("*User:*\n" + username), - markdownText("*Endpoint:*\n[" + httpMethod + "] " + fullUrl) - ))), - section(section -> section.text(markdownText("*Error Message:*\n" + detailedMessage))), - section(section -> section.text(markdownText("*Stack Trace:*\n```" + getStackTraceSummary(e) + "```"))) - ); - } - - private List createSecurityAlertBlocks(HttpServletRequest request, AlertType alertType, String additionalMessage) { - String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - String requestUrl = request.getRequestURI(); - String queryString = request.getQueryString(); - String fullUrl = queryString == null ? requestUrl : requestUrl + "?" + queryString; - String username = getUsername(request); - String location = getLocation(request); - - return Arrays.asList( - section(section -> section.text(markdownText(String.format(":imp: *%s*", alertType.getTitle())))), - section(section -> section.fields(Arrays.asList( - markdownText("*User:*\n" + username), - markdownText("*IP Address:*\n" + clientIpAddress), - markdownText("*Location:*\n" + location), - markdownText("*Endpoint:*\n" + fullUrl) - ))), - section(section -> section.text(markdownText("*Details:*\n" + alertType.getDefaultMessage() + "\n" + additionalMessage))) - ); - } - - private List createAdminLoginBlocks(HttpServletRequest request, MemberLoginInfoDto loginMember) { - String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - String location = getLocation(request); - - return Arrays.asList( - section(section -> section.text(markdownText(String.format(":mechanic: *%s Login*", loginMember.getRole().getDescription())))), - section(section -> section.fields(Arrays.asList( - markdownText("*User:*\n" + loginMember.getMemberId() + " " + loginMember.getMemberName()), - markdownText("*IP Address:*\n" + clientIpAddress), - markdownText("*Location:*\n" + location) - ))) - ); - } - - private List createApplicationBlocks(ApplicationRequestDto requestDto) { - List blocks = new ArrayList<>(); - - blocks.add(section(section -> section.text(markdownText(":sparkles: *동아리 지원*")))); - blocks.add(section(section -> section.fields(Arrays.asList( - markdownText("*구분:*\n" + requestDto.getApplicationType().getDescription()), - markdownText("*학번:*\n" + requestDto.getStudentId()), - markdownText("*이름:*\n" + requestDto.getName()), - markdownText("*학년:*\n" + requestDto.getGrade() + "학년"), - markdownText("*관심 분야:*\n" + requestDto.getInterests()) - )))); - - if (requestDto.getGithubUrl() != null && !requestDto.getGithubUrl().isEmpty()) { - blocks.add(actions(actions -> actions.elements(asElements( - button(b -> b.text(plainText(pt -> pt.emoji(true).text("Github"))) - .url(requestDto.getGithubUrl()) - .actionId("click_github")) - )))); - } - return blocks; - } - - private List createBoardBlocks(SlackBoardInfo board) { - List blocks = new ArrayList<>(); - - blocks.add(section(section -> section.text(markdownText(":writing_hand: *새 게시글*")))); - blocks.add(section(section -> section.fields(Arrays.asList( - markdownText("*제목:*\n" + board.getTitle()), - markdownText("*분류:*\n" + board.getCategory()), - markdownText("*작성자:*\n" + board.getUsername()) - )))); - return blocks; - } - - private List createMembershipFeeBlocks(SlackMembershipFeeInfo additionalData) { - String username = additionalData.getMemberId() + " " + additionalData.getMemberName(); - - return Arrays.asList( - section(section -> section.text(markdownText(":dollar: *회비 신청*"))), - section(section -> section.fields(Arrays.asList( - markdownText("*신청자:*\n" + username), - markdownText("*분류:*\n" + additionalData.getCategory()), - markdownText("*금액:*\n" + additionalData.getAmount() + "원") - ))), - section(section -> section.text(markdownText("*Content:*\n" + additionalData.getContent()))) - ); - } - - private List createBookLoanRecordBlocks(SlackBookLoanRecordInfo additionalData) { - String username = additionalData.getMemberId() + " " + additionalData.getMemberName(); - - return Arrays.asList( - section(section -> section.text(markdownText(":books: *도서 대여 신청*"))), - section(section -> section.fields(Arrays.asList( - markdownText("*도서명:*\n" + additionalData.getBookTitle()), - markdownText("*분류:*\n" + additionalData.getCategory()), - markdownText("*신청자:*\n" + username), - markdownText("*상태:*\n" + (additionalData.isAvailable() ? "대여 가능" : "대여 중")) - ))) - ); - } - - private List createServerStartBlocks() { - String osInfo = System.getProperty("os.name") + " " + System.getProperty("os.version"); - String jdkVersion = System.getProperty("java.version"); - - OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - int availableProcessors = osBean.getAvailableProcessors(); - double systemLoadAverage = osBean.getSystemLoadAverage(); - double cpuUsage = ((systemLoadAverage / availableProcessors) * 100); - - MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); - String memoryInfo = formatMemoryUsage(heapMemoryUsage); - - return Arrays.asList( - section(section -> section.text(markdownText("*:battery: Server Started*"))), - section(section -> section.fields(Arrays.asList( - markdownText("*Environment:* \n" + environment.getProperty("spring.profiles.active")), - markdownText("*OS:* \n" + osInfo), - markdownText("*JDK Version:* \n" + jdkVersion), - markdownText("*CPU Usage:* \n" + String.format("%.2f%%", cpuUsage)), - markdownText("*Memory Usage:* \n" + memoryInfo) - ))), - actions(actions -> actions.elements(asElements( - button(b -> b.text(plainText(pt -> pt.emoji(true).text("Web"))) - .url(webUrl) - .value("click_web")), - button(b -> b.text(plainText(pt -> pt.emoji(true).text("Swagger"))) - .url(apiUrl) - .value("click_swagger")) - ))) - ); - } - - private String extractMessageAfterException(String message) { - String exceptionIndicator = "Exception:"; - int exceptionIndex = message.indexOf(exceptionIndicator); - return exceptionIndex == -1 ? message : message.substring(exceptionIndex + exceptionIndicator.length()).trim(); - } - - private String getStackTraceSummary(Exception e) { - return Arrays.stream(e.getStackTrace()) - .limit(10) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); - } - - private String formatMemoryUsage(MemoryUsage memoryUsage) { - long usedMemory = memoryUsage.getUsed() / (1024 * 1024); - long maxMemory = memoryUsage.getMax() / (1024 * 1024); - return String.format("%dMB / %dMB (%.2f%%)", usedMemory, maxMemory, ((double) usedMemory / maxMemory) * 100); - } - - private @NotNull String getUsername(HttpServletRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return Optional.ofNullable(request.getAttribute("member")) - .map(Object::toString) - .orElseGet(() -> Optional.ofNullable(authentication) - .map(Authentication::getName) - .orElse("anonymous")); - } - - private @NotNull String getLocation(HttpServletRequest request) { - IPResponse ipResponse = attributeStrategy.getAttribute(request); - return ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); - } -} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/AlertType.java deleted file mode 100644 index 99c52fd8f..000000000 --- a/src/main/java/page/clab/api/global/common/slack/domain/AlertType.java +++ /dev/null @@ -1,8 +0,0 @@ -package page.clab.api.global.common.slack.domain; - -public interface AlertType { - - String getTitle(); - - String getDefaultMessage(); -} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java deleted file mode 100644 index 4b564bde7..000000000 --- a/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java +++ /dev/null @@ -1,16 +0,0 @@ -package page.clab.api.global.common.slack.domain; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum GeneralAlertType implements AlertType { - - ADMIN_LOGIN("관리자 로그인", "Admin login."), - SERVER_START("서버 시작", "Server has been started."), - SERVER_ERROR("서버 에러", "Server error occurred."); - - private final String title; - private final String defaultMessage; -} diff --git a/src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java b/src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java deleted file mode 100644 index bfa8b67a8..000000000 --- a/src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package page.clab.api.global.common.slack.dto.response; - -import lombok.Builder; -import lombok.Getter; -import page.clab.api.global.common.slack.domain.NotificationSetting; - -@Getter -@Builder -public class NotificationSettingResponseDto { - - private String alertType; - private boolean enabled; - - public static NotificationSettingResponseDto toDto(NotificationSetting setting) { - return NotificationSettingResponseDto.builder() - .alertType(setting.getAlertType().getTitle()) - .enabled(setting.isEnabled()) - .build(); - } -} diff --git a/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java b/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java deleted file mode 100644 index e8463a03a..000000000 --- a/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java +++ /dev/null @@ -1,28 +0,0 @@ -package page.clab.api.global.common.slack.listener; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; -import page.clab.api.global.common.slack.application.NotificationSettingService; -import page.clab.api.global.common.slack.application.SlackServiceHelper; -import page.clab.api.global.common.slack.domain.AlertType; -import page.clab.api.global.common.slack.domain.NotificationSetting; -import page.clab.api.global.common.slack.event.NotificationEvent; - -@Component -@RequiredArgsConstructor -public class NotificationListener { - - private final NotificationSettingService settingService; - private final SlackServiceHelper slackServiceHelper; - - @EventListener - public void handleNotificationEvent(NotificationEvent event) { - AlertType alertType = event.getAlertType(); - NotificationSetting setting = settingService.getOrCreateDefaultSetting(alertType); - - if (setting.isEnabled()) { - slackServiceHelper.sendSlackMessage(event.getWebhookUrl(), alertType, event.getRequest(), event.getAdditionalData()); - } - } -} diff --git a/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java b/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java index d4b137634..0af2c02e3 100644 --- a/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java +++ b/src/main/java/page/clab/api/global/common/verification/application/VerificationService.java @@ -11,6 +11,21 @@ import java.security.SecureRandom; +/** + * {@code VerificationService}는 회원 인증 코드 생성, 저장, 검증 및 삭제 기능을 제공하는 서비스입니다. + * + *

인증 코드는 사용자의 이메일 또는 전화번호 인증을 위한 임시 코드로, + * 생성된 코드는 데이터베이스에 저장되고, 유효성 검사를 통해 해당 사용자의 인증을 검증합니다.

+ * + * 주요 기능: + *
    + *
  • {@link #getVerificationCode(String)} - 주어진 코드로 데이터베이스에서 인증 코드를 조회합니다.
  • + *
  • {@link #saveVerificationCode(String, String)} - 주어진 회원 ID와 인증 코드를 데이터베이스에 저장합니다.
  • + *
  • {@link #deleteVerificationCode(String)} - 주어진 인증 코드를 데이터베이스에서 삭제합니다.
  • + *
  • {@link #validateVerificationCode(VerificationRequestDto, Member)} - 요청된 코드와 사용자를 검증합니다.
  • + *
  • {@link #generateVerificationCode()} - 보안 난수를 기반으로 새로운 인증 코드를 생성합니다.
  • + *
+ */ @Service @RequiredArgsConstructor public class VerificationService { diff --git a/src/main/java/page/clab/api/global/config/SecurityConfig.java b/src/main/java/page/clab/api/global/config/SecurityConfig.java index 0002f24be..d31e1ce21 100644 --- a/src/main/java/page/clab/api/global/config/SecurityConfig.java +++ b/src/main/java/page/clab/api/global/config/SecurityConfig.java @@ -2,10 +2,12 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -34,14 +36,11 @@ import page.clab.api.global.auth.jwt.JwtTokenProvider; import page.clab.api.global.auth.util.IpWhitelistValidator; import page.clab.api.global.common.file.application.FileService; -import page.clab.api.global.common.slack.application.SlackService; import page.clab.api.global.filter.IPinfoSpringFilter; import page.clab.api.global.util.ApiLogger; import page.clab.api.global.util.HttpReqResUtil; import page.clab.api.global.util.ResponseUtil; -import java.io.IOException; - @Configuration @EnableWebSecurity @EnableMethodSecurity @@ -54,7 +53,7 @@ public class SecurityConfig { private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase; private final ExternalRegisterBlacklistIpUseCase externalRegisterBlacklistIpUseCase; private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase; - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; private final IpWhitelistValidator ipWhitelistValidator; private final WhitelistAccountProperties whitelistAccountProperties; private final WhitelistPatternsProperties whitelistPatternsProperties; @@ -92,15 +91,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( - new InvalidEndpointAccessFilter(slackService, fileURL, externalRegisterBlacklistIpUseCase, externalRetrieveBlacklistIpUseCase), + new InvalidEndpointAccessFilter(fileURL, externalRegisterBlacklistIpUseCase, + externalRetrieveBlacklistIpUseCase, eventPublisher), UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( - new CustomBasicAuthenticationFilter(authenticationManager, ipWhitelistValidator, slackService, externalCheckIpBlockedUseCase, externalRetrieveBlacklistIpUseCase), + new CustomBasicAuthenticationFilter(authenticationManager, ipWhitelistValidator, + externalCheckIpBlockedUseCase, externalRetrieveBlacklistIpUseCase, eventPublisher), UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( - new JwtAuthenticationFilter(slackService, jwtTokenProvider, externalManageRedisTokenUseCase, externalCheckIpBlockedUseCase, externalRetrieveBlacklistIpUseCase), + new JwtAuthenticationFilter(jwtTokenProvider, eventPublisher, externalManageRedisTokenUseCase, + externalCheckIpBlockedUseCase, externalRetrieveBlacklistIpUseCase), UsernamePasswordAuthenticationFilter.class ) // .addFilterBefore( @@ -120,11 +122,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(SecurityConstants.PERMIT_ALL).permitAll() .requestMatchers(HttpMethod.GET, SecurityConstants.PERMIT_ALL_API_ENDPOINTS_GET).permitAll() .requestMatchers(HttpMethod.POST, SecurityConstants.PERMIT_ALL_API_ENDPOINTS_POST).permitAll() - .requestMatchers(whitelistPatternsProperties.getWhitelistPatterns()).hasRole(whitelistAccountProperties.getRole()) + .requestMatchers(whitelistPatternsProperties.getWhitelistPatterns()) + .hasRole(whitelistAccountProperties.getRole()) .anyRequest().authenticated(); } - private void handleException(HttpServletRequest request, HttpServletResponse response, Exception exception) throws IOException { + private void handleException(HttpServletRequest request, HttpServletResponse response, Exception exception) + throws IOException { String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); String message; int statusCode; diff --git a/src/main/java/page/clab/api/global/config/SlackConfig.java b/src/main/java/page/clab/api/global/config/SlackConfig.java deleted file mode 100644 index 31f1f6d63..000000000 --- a/src/main/java/page/clab/api/global/config/SlackConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -package page.clab.api.global.config; - -import com.slack.api.Slack; -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Setter -@Getter -@Configuration -@ConfigurationProperties(prefix = "slack") -public class SlackConfig { - - private String coreTeamWebhookUrl; - private String executivesWebhookUrl; - private String webUrl; - private String apiUrl; - private String color; - - @Bean - public Slack slack() { - return Slack.getInstance(); - } -} diff --git a/src/main/java/page/clab/api/global/filter/IPinfoSpringFilter.java b/src/main/java/page/clab/api/global/filter/IPinfoSpringFilter.java index e3d9128b1..704982677 100644 --- a/src/main/java/page/clab/api/global/filter/IPinfoSpringFilter.java +++ b/src/main/java/page/clab/api/global/filter/IPinfoSpringFilter.java @@ -18,6 +18,13 @@ import java.io.IOException; import java.util.Objects; +/** + * {@code IPinfoSpringFilter}는 IP 정보 조회 및 관리 기능을 제공하는 필터로, + * 클라이언트의 IP 주소를 기반으로 IP 정보 조회 API를 통해 세부 정보를 가져와 요청에 저장합니다. + * + *

이 필터는 요청이 IP 정보가 필요한 경우에만 IP 정보를 조회하며, + * 요청 객체에 IP 정보를 속성으로 저장하여 이후 처리 로직에서 사용할 수 있도록 합니다.

+ */ @Slf4j public class IPinfoSpringFilter implements Filter { diff --git a/src/main/java/page/clab/api/global/filter/PostResponseFilter.java b/src/main/java/page/clab/api/global/filter/PostResponseFilter.java new file mode 100644 index 000000000..20bed42a4 --- /dev/null +++ b/src/main/java/page/clab/api/global/filter/PostResponseFilter.java @@ -0,0 +1,38 @@ +package page.clab.api.global.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.annotation.WebFilter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * {@code PostResponseFilter}는 특정 API 요청에 대해 POST 요청의 응답 상태 코드를 수정하는 서블릿 필터입니다. + * + *

이 필터는 모든 API URL 패턴("/api/*")에 대해 등록되며, Spring 컴포넌트로 작동합니다. + * 만약 POST 요청의 응답 상태 코드가 200(OK)일 경우, 이를 201(Created)로 변경하여 리소스 생성에 대한 명확한 상태 코드를 제공합니다.

+ */ +@WebFilter("/api/*") +@Component +public class PostResponseFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + // POST 요청이고 상태 코드가 200인 경우 상태 코드를 201로 변경 + if (HttpMethod.POST.name().equalsIgnoreCase(httpRequest.getMethod()) && httpResponse.getStatus() == HttpServletResponse.SC_OK) { + httpResponse.setStatus(HttpServletResponse.SC_CREATED); + } + + chain.doFilter(request, response); + } +} diff --git a/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java b/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java index ac27a3774..ac269d054 100644 --- a/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/page/clab/api/global/handler/GlobalExceptionHandler.java @@ -7,9 +7,14 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.ConstraintViolationException; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.concurrent.CompletionException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.query.sqm.UnknownPathException; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -75,7 +80,8 @@ import page.clab.api.global.common.file.exception.FileUploadFailException; import page.clab.api.global.common.file.exception.InvalidFileAttributeException; import page.clab.api.global.common.file.exception.InvalidPathVariableException; -import page.clab.api.global.common.slack.application.SlackService; +import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent; +import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType; import page.clab.api.global.exception.CustomOptimisticLockingFailureException; import page.clab.api.global.exception.DecryptionException; import page.clab.api.global.exception.EncryptionException; @@ -87,17 +93,12 @@ import page.clab.api.global.exception.PermissionDeniedException; import page.clab.api.global.exception.SortingArgumentException; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.NoSuchElementException; -import java.util.concurrent.CompletionException; - @RestControllerAdvice(basePackages = "page.clab.api") @RequiredArgsConstructor @Slf4j public class GlobalExceptionHandler { - private final SlackService slackService; + private final ApplicationEventPublisher eventPublisher; @ExceptionHandler({ InvalidInformationException.class, @@ -231,7 +232,8 @@ public ErrorResponse conflictException(HttpServletResponse response, Exception.class }) public ApiResponse serverException(HttpServletRequest request, HttpServletResponse response, Exception e) { - slackService.sendServerErrorNotification(request, e); + eventPublisher.publishEvent( + new NotificationEvent(this, GeneralAlertType.SERVER_ERROR, request, e)); log.warn(e.getMessage()); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return ApiResponse.failure(); diff --git a/src/main/java/page/clab/api/global/util/ApiLogger.java b/src/main/java/page/clab/api/global/util/ApiLogger.java index 70da7f6c9..3e0ce7d14 100644 --- a/src/main/java/page/clab/api/global/util/ApiLogger.java +++ b/src/main/java/page/clab/api/global/util/ApiLogger.java @@ -7,6 +7,16 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +/** + * {@code ApiLogger}는 API 요청 및 응답 로깅을 담당하는 유틸리티 클래스입니다. + * 클라이언트 IP 주소, 사용자 정보, 요청 URL, HTTP 메서드, 상태 코드 등의 정보를 포함하여 로그를 출력합니다. + * + *

주요 기능: + *

    + *
  • {@link #logRequest(HttpServletRequest, HttpServletResponse, String, String)} - 요청과 응답에 대한 기본 정보 로깅
  • + *
  • {@link #logRequestDuration(HttpServletRequest, HttpServletResponse, Exception)} - 요청의 수행 시간을 포함한 추가 정보를 로깅
  • + *
+ */ @Component @Slf4j public class ApiLogger { diff --git a/src/main/java/page/clab/api/global/util/ColumnValidator.java b/src/main/java/page/clab/api/global/util/ColumnValidator.java index 7180426cb..1b88e2f0b 100644 --- a/src/main/java/page/clab/api/global/util/ColumnValidator.java +++ b/src/main/java/page/clab/api/global/util/ColumnValidator.java @@ -5,6 +5,15 @@ import java.lang.reflect.Field; import java.util.Arrays; +/** + * {@code ColumnValidator}는 주어진 클래스에 특정 필드(컬럼)가 존재하는지 검증하는 유틸리티 클래스입니다. + * 리플렉션을 사용하여 클래스의 모든 필드를 조회하고, 입력된 필드 이름과 일치하는 필드가 있는지 확인합니다. + * + *

주요 기능: + *

    + *
  • {@link #isValidColumn(Class, String)} - 클래스에 특정 필드가 존재하는지 검증
  • + *
+ */ @Component public class ColumnValidator { diff --git a/src/main/java/page/clab/api/global/util/EmojiUtils.java b/src/main/java/page/clab/api/global/util/EmojiUtils.java index 692a987b4..4c451c663 100644 --- a/src/main/java/page/clab/api/global/util/EmojiUtils.java +++ b/src/main/java/page/clab/api/global/util/EmojiUtils.java @@ -5,6 +5,15 @@ import java.text.BreakIterator; +/** + * {@code EmojiUtils}는 주어진 텍스트가 이모지인지 여부를 확인하는 유틸리티 클래스입니다. + * ICU 라이브러리를 사용하여 텍스트 내 코드 포인트가 이모지 속성에 해당하는지 검증합니다. + * + *

주요 기능: + *

    + *
  • {@link #isEmoji(String)} - 주어진 문자열이 이모지인지 여부를 확인
  • + *
+ */ public class EmojiUtils { public static boolean isEmoji(String text) { diff --git a/src/main/java/page/clab/api/global/util/EncryptionUtil.java b/src/main/java/page/clab/api/global/util/EncryptionUtil.java index d4d5565bb..c4219389c 100644 --- a/src/main/java/page/clab/api/global/util/EncryptionUtil.java +++ b/src/main/java/page/clab/api/global/util/EncryptionUtil.java @@ -15,6 +15,16 @@ import java.util.Arrays; import java.util.Base64; +/** + * {@code EncryptionUtil}은 AES/GCM 암호화 및 복호화 작업을 수행하는 유틸리티 클래스입니다. + * 암호화와 복호화에 필요한 키와 IV(초기화 벡터)를 생성하고, AES 암호화 표준을 사용하여 문자열을 암호화하고 복호화하는 기능을 제공합니다. + * + *

주요 기능: + *

    + *
  • {@link #encrypt(String)} - 문자열을 암호화합니다.
  • + *
  • {@link #decrypt(String)} - 암호화된 문자열을 복호화합니다.
  • + *
+ */ public class EncryptionUtil { private final String secretKey; diff --git a/src/main/java/page/clab/api/global/util/FileSystemUtil.java b/src/main/java/page/clab/api/global/util/FileSystemUtil.java index 113ff933e..05a7df80d 100644 --- a/src/main/java/page/clab/api/global/util/FileSystemUtil.java +++ b/src/main/java/page/clab/api/global/util/FileSystemUtil.java @@ -5,6 +5,17 @@ import java.util.Arrays; import java.util.List; +/** + * {@code FileSystemUtil}은 파일 시스템 관련 작업을 지원하는 유틸리티 클래스입니다. + * 디렉토리의 크기를 계산하고, 디렉토리에 포함된 파일 목록을 반환하며, 크기 문자열을 바이트 단위로 변환하는 기능을 제공합니다. + * + *

주요 기능: + *

    + *
  • {@link #calculateDirectorySize(File)} - 디렉토리의 총 크기를 계산합니다.
  • + *
  • {@link #getFilesInDirectory(File)} - 디렉토리 내 모든 파일 목록을 반환합니다.
  • + *
  • {@link #convertToBytes(String)} - 주어진 크기 문자열을 바이트 단위로 변환합니다.
  • + *
+ */ public class FileSystemUtil { public static long calculateDirectorySize(File directory) { diff --git a/src/main/java/page/clab/api/global/util/FileUtil.java b/src/main/java/page/clab/api/global/util/FileUtil.java index 1e1798b96..a00be629a 100644 --- a/src/main/java/page/clab/api/global/util/FileUtil.java +++ b/src/main/java/page/clab/api/global/util/FileUtil.java @@ -16,6 +16,20 @@ import java.util.Set; import java.util.UUID; +/** + * {@code FileUtil}은 파일 및 디렉토리와 관련된 유틸리티 기능을 제공합니다. + * 파일 경로 검증, 파일 권한 설정, 고유 파일명 생성, 디렉토리 생성 등의 기능을 수행합니다. + * + *

주요 기능: + *

    + *
  • {@link #validateFilePath(String, String)} - 파일 경로가 기본 디렉토리 내에 있는지 확인합니다.
  • + *
  • {@link #validateFileExists(Path)} - 파일 존재 여부를 검증합니다.
  • + *
  • {@link #makeFileName(String)} - 고유한 파일명을 생성합니다.
  • + *
  • {@link #ensureParentDirectoryExists(File, String)} - 디렉토리가 존재하지 않으면 생성합니다.
  • + *
  • {@link #validateFileAttributes(String, Set)} - 파일명과 확장자를 검증합니다.
  • + *
  • {@link #setFilePermissions(File, String, String)} - 파일의 읽기 전용 권한을 설정합니다.
  • + *
+ */ public class FileUtil { /** diff --git a/src/main/java/page/clab/api/global/util/HtmlCharacterEscapes.java b/src/main/java/page/clab/api/global/util/HtmlCharacterEscapes.java index 60a4966d0..1bc53051e 100644 --- a/src/main/java/page/clab/api/global/util/HtmlCharacterEscapes.java +++ b/src/main/java/page/clab/api/global/util/HtmlCharacterEscapes.java @@ -5,6 +5,26 @@ import com.fasterxml.jackson.core.io.SerializedString; import org.apache.commons.text.StringEscapeUtils; +/** + * {@code HtmlCharacterEscapes}는 HTML 특수 문자를 JSON에서 안전하게 사용할 수 있도록 + * 이스케이프 처리를 담당하는 클래스입니다. + * + *

특히 JSON 내에서 HTML 관련 특수 문자가 안전하게 사용될 수 있도록 다음과 같은 문자들을 + * 커스텀 이스케이프 처리합니다: + *

    + *
  • < (less than)
  • + *
  • > (greater than)
  • + *
  • " (double quote)
  • + *
  • ( (left parenthesis)
  • + *
  • ) (right parenthesis)
  • + *
  • # (hash symbol)
  • + *
  • ' (single quote)
  • + *
+ *

+ * + *

이 클래스는 또한 이모지나 Zero Width Joiner와 같은 특수 유니코드 문자를 + * UTF-16 형태로 이스케이프 처리하여 JSON 내에서 표현할 수 있도록 합니다.

+ */ public class HtmlCharacterEscapes extends CharacterEscapes { private static final char ZERO_WIDTH_JOINER = 0x200D; diff --git a/src/main/java/page/clab/api/global/util/HttpReqResUtil.java b/src/main/java/page/clab/api/global/util/HttpReqResUtil.java index 4631fdb4c..f768881a0 100644 --- a/src/main/java/page/clab/api/global/util/HttpReqResUtil.java +++ b/src/main/java/page/clab/api/global/util/HttpReqResUtil.java @@ -7,6 +7,10 @@ import java.util.Arrays; +/** + * {@code HttpReqResUtil}은 HTTP 요청 및 응답에 관련된 유틸리티 메서드를 제공하는 클래스입니다. + * 이 클래스는 클라이언트 IP 주소를 가져오거나, IP 주소가 비공개 IP 대역인지 확인하는 등의 기능을 포함합니다. + */ public class HttpReqResUtil { private static final IPRequest.IpAddressMatcher[] IpAddressMatcherList = { diff --git a/src/main/java/page/clab/api/global/util/IPInfoUtil.java b/src/main/java/page/clab/api/global/util/IPInfoUtil.java index dcb65d22b..4904b93cf 100644 --- a/src/main/java/page/clab/api/global/util/IPInfoUtil.java +++ b/src/main/java/page/clab/api/global/util/IPInfoUtil.java @@ -11,6 +11,10 @@ import page.clab.api.global.common.dto.IPInfoResponse; import page.clab.api.global.config.IPInfoConfig; +/** + * {@code IPInfoUtil} 클래스는 IP 주소에 대한 정보를 가져오기 위한 유틸리티 메서드를 제공합니다. + * IP 정보를 가져오기 위해 IPInfo API를 호출하고, IP 기반 위치 데이터를 요청으로부터 가져옵니다. + */ @Component @Slf4j public class IPInfoUtil { diff --git a/src/main/java/page/clab/api/global/util/ImageUtil.java b/src/main/java/page/clab/api/global/util/ImageUtil.java index 35921ced1..99345069f 100644 --- a/src/main/java/page/clab/api/global/util/ImageUtil.java +++ b/src/main/java/page/clab/api/global/util/ImageUtil.java @@ -25,6 +25,10 @@ import java.nio.file.Path; import java.util.Iterator; +/** + * {@code ImageUtil}은 이미지 파일의 처리와 관련된 유틸리티 메서드를 제공합니다. + * 이미지의 방향을 조정하거나, 압축을 수행하는 등의 기능을 포함합니다. + */ @Slf4j public class ImageUtil { diff --git a/src/main/java/page/clab/api/global/util/IpAddressUtil.java b/src/main/java/page/clab/api/global/util/IpAddressUtil.java index 52dc5d73d..c12820052 100644 --- a/src/main/java/page/clab/api/global/util/IpAddressUtil.java +++ b/src/main/java/page/clab/api/global/util/IpAddressUtil.java @@ -2,6 +2,9 @@ import org.springframework.security.web.util.matcher.IpAddressMatcher; +/** + * {@code IpAddressUtil} 클래스는 IP 주소의 범위를 확인하는 유틸리티 메서드를 제공합니다. + */ public class IpAddressUtil { /** diff --git a/src/main/java/page/clab/api/global/util/LogSanitizerUtil.java b/src/main/java/page/clab/api/global/util/LogSanitizerUtil.java index 47478e038..a804d8fc7 100644 --- a/src/main/java/page/clab/api/global/util/LogSanitizerUtil.java +++ b/src/main/java/page/clab/api/global/util/LogSanitizerUtil.java @@ -1,5 +1,9 @@ package page.clab.api.global.util; +/** + * {@code LogSanitizerUtil} 클래스는 로그에 기록하기 전 문자열을 안전하게 변환하기 위한 유틸리티 메서드를 제공합니다. + * 이 클래스는 잠재적으로 해로운 특수 문자를 제거하여 로그의 안전성을 보장합니다. + */ public class LogSanitizerUtil { /** diff --git a/src/main/java/page/clab/api/global/util/OrderSpecifierUtil.java b/src/main/java/page/clab/api/global/util/OrderSpecifierUtil.java index 3f83d93c9..0843a03ce 100644 --- a/src/main/java/page/clab/api/global/util/OrderSpecifierUtil.java +++ b/src/main/java/page/clab/api/global/util/OrderSpecifierUtil.java @@ -8,6 +8,11 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; +/** + * {@code OrderSpecifierUtil} 클래스는 QueryDSL을 사용하여 정렬 조건을 동적으로 생성하는 유틸리티 메서드를 제공합니다. + * 이 클래스는 {@link Pageable} 객체에서 제공된 정렬 정보를 바탕으로 {@link OrderSpecifier} 배열을 생성하여, + * QueryDSL 쿼리에 활용할 수 있도록 합니다. + */ @Component public class OrderSpecifierUtil { diff --git a/src/main/java/page/clab/api/global/util/PageableUtils.java b/src/main/java/page/clab/api/global/util/PageableUtils.java index 6b7fc9338..4cdbb72dc 100644 --- a/src/main/java/page/clab/api/global/util/PageableUtils.java +++ b/src/main/java/page/clab/api/global/util/PageableUtils.java @@ -11,6 +11,10 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +/** + * {@code PageableUtils} 클래스는 정렬 및 페이지네이션 정보를 기반으로 {@link Pageable} 객체를 생성하는 유틸리티 클래스입니다. + * 이 클래스는 주어진 정렬 기준이 유효한지 검증하고, 유효하지 않은 경우 예외를 던집니다. + */ @Component public class PageableUtils { diff --git a/src/main/java/page/clab/api/global/util/PaginationUtils.java b/src/main/java/page/clab/api/global/util/PaginationUtils.java index 6604eeb4e..c7b106660 100644 --- a/src/main/java/page/clab/api/global/util/PaginationUtils.java +++ b/src/main/java/page/clab/api/global/util/PaginationUtils.java @@ -8,6 +8,9 @@ import java.util.Comparator; import java.util.List; +/** + * {@code PaginationUtils} 클래스는 정렬 및 페이지네이션 기능을 통해 리스트에 대한 데이터를 처리하는 유틸리티 클래스입니다. + */ @Component public class PaginationUtils { diff --git a/src/main/java/page/clab/api/global/util/QRCodeUtil.java b/src/main/java/page/clab/api/global/util/QRCodeUtil.java index e95361de1..a8bd0728e 100644 --- a/src/main/java/page/clab/api/global/util/QRCodeUtil.java +++ b/src/main/java/page/clab/api/global/util/QRCodeUtil.java @@ -9,6 +9,10 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +/** + * {@code QRCodeUtil} 클래스는 QR 코드 이미지를 생성하는 유틸리티 클래스입니다. + * 주어진 데이터를 인코딩하여 QR 코드 이미지를 생성하고, 바이트 배열로 반환합니다. + */ public class QRCodeUtil { private static final int SIZE = 200; diff --git a/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java b/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java index a6afb248f..3aac38e38 100644 --- a/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java +++ b/src/main/java/page/clab/api/global/util/RandomNicknameUtil.java @@ -3,6 +3,10 @@ import java.util.Arrays; import java.util.List; +/** + * {@code RandomNicknameUtil} 클래스는 임의의 닉네임을 생성하는 유틸리티 클래스입니다. + * 다양한 형용사, 색상, 명사 리스트에서 무작위로 선택하여 조합된 닉네임을 반환합니다. + */ public class RandomNicknameUtil { private static final int ADJECTIVE_SIZE = 15; diff --git a/src/main/java/page/clab/api/global/util/ResponseUtil.java b/src/main/java/page/clab/api/global/util/ResponseUtil.java index 67d8cb265..ba0983303 100644 --- a/src/main/java/page/clab/api/global/util/ResponseUtil.java +++ b/src/main/java/page/clab/api/global/util/ResponseUtil.java @@ -5,6 +5,10 @@ import java.io.IOException; +/** + * {@code ResponseUtil} 클래스는 HTTP 응답에 대한 유틸리티 메서드를 제공합니다. + * 주로 오류 응답을 JSON 형식으로 전송하는 메서드를 포함하고 있습니다. + */ public class ResponseUtil { public static void sendErrorResponse(HttpServletResponse response, int status) throws IOException { diff --git a/src/main/java/page/clab/api/global/util/StringJsonConverter.java b/src/main/java/page/clab/api/global/util/StringJsonConverter.java index fcbc378ff..ba16c24eb 100644 --- a/src/main/java/page/clab/api/global/util/StringJsonConverter.java +++ b/src/main/java/page/clab/api/global/util/StringJsonConverter.java @@ -7,6 +7,11 @@ import java.util.List; +/** + * {@code StringJsonConverter} 클래스는 JPA 엔티티 필드를 JSON 문자열로 변환하거나 JSON 문자열을 리스트로 변환하는 역할을 수행하는 컨버터입니다. + *

+ * 데이터베이스와 애플리케이션 사이에서 {@code List} 타입을 JSON 문자열로 저장 및 변환하는 데 사용됩니다. + */ @Slf4j public class StringJsonConverter implements AttributeConverter, String> { diff --git a/src/main/java/page/clab/api/global/util/TempFileUtil.java b/src/main/java/page/clab/api/global/util/TempFileUtil.java index 7e8c0c09c..81b7ef29f 100644 --- a/src/main/java/page/clab/api/global/util/TempFileUtil.java +++ b/src/main/java/page/clab/api/global/util/TempFileUtil.java @@ -6,6 +6,11 @@ import java.nio.file.attribute.PosixFilePermission; import java.util.EnumSet; +/** + * {@code TempFileUtil} 클래스는 보안 설정이 적용된 임시 파일을 생성하는 유틸리티 클래스입니다. + *

+ * 임시 파일은 POSIX 파일 시스템(예: UNIX, 리눅스 계열)에서 파일 소유자만 접근할 수 있도록 권한을 설정하여 보안을 강화합니다. + */ public class TempFileUtil { /** diff --git a/src/main/java/page/clab/api/global/util/WhitelistPathMatcher.java b/src/main/java/page/clab/api/global/util/WhitelistPathMatcher.java index d6ed4e77d..ab1c2b148 100644 --- a/src/main/java/page/clab/api/global/util/WhitelistPathMatcher.java +++ b/src/main/java/page/clab/api/global/util/WhitelistPathMatcher.java @@ -6,6 +6,12 @@ import java.util.regex.Pattern; +/** + * {@code WhitelistPathMatcher} 클래스는 지정된 경로가 화이트리스트에 포함되는지 확인하는 유틸리티 클래스입니다. + *

+ * 이 클래스는 Swagger 및 Actuator와 같은 특정 요청 경로에 대한 화이트리스트 패턴을 관리하고, 주어진 경로가 + * 해당 패턴들과 일치하는지 검사하는 메서드를 제공합니다. + */ @Component public class WhitelistPathMatcher implements InitializingBean { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 5ae527332..68c4e082c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -195,13 +195,49 @@ resource: ipinfo: access-token: ${IPINFO_ACCESS_TOKEN} # Register at https://ipinfo.io/ -# Slack webhook configuration -slack: - core-team-webhook-url: ${SLACK_WEBHOOK_URL} # Create a Slack channel and get a webhook URL - executives-webhook-url: ${SLACK_WEBHOOK_URL} # Create a Slack channel and get a webhook URL - web-url: ${WEB_URL} # Your web URL - api-url: ${API_URL} # Your API docs URL - color: "#FF968A" # Slack message color +# Messaging configuration +notification: + common: + web-url: "${WEB_URL}" # Your web URL + api-url: "${API_URL}" # Your API documentation URL + color: "#FF968A" # Message color used in notifications + platforms: + slack: + webhooks: + # Replace the placeholders with your actual Slack webhook URLs + core-team: "${SLACK_CORE_TEAM_WEBHOOK_URL}" # Slack webhook URL for core team notifications + executives: "${SLACK_EXECUTIVES_WEBHOOK_URL}" # Slack webhook URL for executive team notifications + discord: + webhooks: + # Replace the placeholders with your actual Discord webhook URLs + release: "${DISCORD_RELEASE_WEBHOOK_URL}" # Discord webhook URL for release notifications + notifications: "${DISCORD_NOTIFICATIONS_WEBHOOK_URL}" # Discord webhook URL for general notifications + executives: "${DISCORD_EXECUTIVES_WEBHOOK_URL}" # Discord webhook URL for executive team notifications + # The category-mappings section defines how notifications are routed based on their category. + # The category names should match those specified in "page.clab.api.global.common.notificationSetting.domain.AlertCategory". + # By specifying multiple platforms and webhooks under each category, you can configure messages to be sent to multiple platforms and multiple webhooks simultaneously. + category-mappings: + GENERAL: + - platform: slack + webhook: core-team + - platform: discord + webhook: notifications + SECURITY: + - platform: slack + webhook: core-team + - platform: discord + webhook: notifications + EXECUTIVES: + - platform: slack + webhook: executives + - platform: discord + webhook: executives + # If a notification category is not explicitly mapped in category-mappings, it will use the default-mappings to determine where to send messages. + default-mappings: + - platform: slack + webhook: core-team + - platform: discord + webhook: notifications # Configure Swagger UI and generate OpenAPI documentation springdoc: