diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 201ad5570..e0d720750 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @Gyuhyeok99 @nayonsoso @wibaek
+* @Gyuhyeok99 @nayonsoso @wibaek @whqtker @lsy1307
diff --git a/.github/workflows/stage-cd.yml b/.github/workflows/dev-cd.yml
similarity index 51%
rename from .github/workflows/stage-cd.yml
rename to .github/workflows/dev-cd.yml
index 41ff68b37..4269f9d46 100644
--- a/.github/workflows/stage-cd.yml
+++ b/.github/workflows/dev-cd.yml
@@ -1,8 +1,8 @@
-name: "[STAGE] Build Gradle and Deploy"
+name: "[DEV] Build Gradle and Deploy"
on:
push:
- branches: [ "release" ]
+ branches: [ "develop" ]
workflow_dispatch:
jobs:
@@ -38,38 +38,47 @@ jobs:
- name: Copy jar file to remote
uses: appleboy/scp-action@master
with:
- host: ${{ secrets.STAGE_HOST }}
- username: ${{ secrets.STAGE_USERNAME }}
- key: ${{ secrets.STAGE_PRIVATE_KEY }}
+ host: ${{ secrets.DEV_HOST }}
+ username: ${{ secrets.DEV_USERNAME }}
+ key: ${{ secrets.DEV_PRIVATE_KEY }}
source: "./build/libs/*.jar"
- target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/"
+ target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/"
- name: Copy docker file to remote
uses: appleboy/scp-action@master
with:
- host: ${{ secrets.STAGE_HOST }}
- username: ${{ secrets.STAGE_USERNAME }}
- key: ${{ secrets.STAGE_PRIVATE_KEY }}
+ host: ${{ secrets.DEV_HOST }}
+ username: ${{ secrets.DEV_USERNAME }}
+ key: ${{ secrets.DEV_PRIVATE_KEY }}
source: "./Dockerfile"
- target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/"
+ target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/"
- name: Copy docker compose file to remote
uses: appleboy/scp-action@master
with:
- host: ${{ secrets.STAGE_HOST }}
- username: ${{ secrets.STAGE_USERNAME }}
- key: ${{ secrets.STAGE_PRIVATE_KEY }}
- source: "./docker-compose.stage.yml"
- target: "/home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage/"
+ host: ${{ secrets.DEV_HOST }}
+ username: ${{ secrets.DEV_USERNAME }}
+ key: ${{ secrets.DEV_PRIVATE_KEY }}
+ source: "./docker-compose.dev.yml"
+ target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/"
+
+ - name: Copy alloy config file to remote
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.DEV_HOST }}
+ username: ${{ secrets.DEV_USERNAME }}
+ key: ${{ secrets.DEV_PRIVATE_KEY }}
+ source: "./docs/infra-config/config.alloy"
+ target: "/home/${{ secrets.DEV_USERNAME }}/solid-connection-dev/"
- name: Run docker compose
uses: appleboy/ssh-action@master
with:
- host: ${{ secrets.STAGE_HOST }}
- username: ${{ secrets.STAGE_USERNAME }}
- key: ${{ secrets.STAGE_PRIVATE_KEY }}
+ host: ${{ secrets.DEV_HOST }}
+ username: ${{ secrets.DEV_USERNAME }}
+ key: ${{ secrets.DEV_PRIVATE_KEY }}
script_stop: true
script: |
- cd /home/${{ secrets.STAGE_USERNAME }}/solid-connect-stage
+ cd /home/${{ secrets.DEV_USERNAME }}/solid-connection-dev
docker compose down
- docker compose -f docker-compose.stage.yml up -d --build
+ docker compose -f docker-compose.dev.yml up -d --build
diff --git a/.github/workflows/prod-cd.yml b/.github/workflows/prod-cd.yml
index 9030c80f5..714aede30 100644
--- a/.github/workflows/prod-cd.yml
+++ b/.github/workflows/prod-cd.yml
@@ -61,7 +61,16 @@ jobs:
key: ${{ secrets.PRIVATE_KEY }}
source: "./docker-compose.prod.yml"
target: "/home/${{ secrets.USERNAME }}/solid-connect-server/"
-
+
+ - name: Copy alloy config file to remote
+ uses: appleboy/scp-action@master
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USERNAME }}
+ key: ${{ secrets.PRIVATE_KEY }}
+ source: "./docs/infra-config/config.alloy"
+ target: "/home/${{ secrets.USERNAME }}/solid-connect-server/"
+
- name: Run docker compose
uses: appleboy/ssh-action@master
with:
diff --git a/.gitignore b/.gitignore
index 9f59fa8d9..d5df4047a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
+logs/
### STS ###
.apt_generated
diff --git a/README.md b/README.md
index a07f6d77b..6cd3ba1a3 100644
Binary files a/README.md and b/README.md differ
diff --git a/build.gradle b/build.gradle
index b4267f41e..91cc2e77d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,6 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.4'
+ id 'org.flywaydb.flyway' version '9.16.3'
}
group = 'com.example'
@@ -31,6 +32,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'
+ runtimeOnly 'com.h2database:h2'
// QueryDSL
implementation 'io.github.openfeign.querydsl:querydsl-jpa:6.11'
@@ -58,10 +60,14 @@ dependencies {
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mysql'
+ testImplementation 'org.projectlombok:lombok'
+ testAnnotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.awaitility:awaitility:4.2.0'
// Etc
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
+ implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
tasks.named('test', Test) {
@@ -72,3 +78,21 @@ tasks.named('test', Test) {
sourceSets {
main.java.srcDirs += ['build/generated/sources/annotationProcessor/java/main']
}
+
+// build 단계에서 flyway 검증
+flyway {
+ url = 'jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1'
+ user = 'sa'
+ password = ''
+ locations = ['filesystem:src/main/resources/db/migration']
+ validateMigrationNaming = true
+ ignoreMigrationPatterns = ['*:pending']
+}
+
+tasks.named('build') {
+ dependsOn 'flywayValidate'
+}
+
+tasks.named('bootJar') {
+ dependsOn 'flywayValidate'
+}
diff --git a/docker-compose.stage.yml b/docker-compose.dev.yml
similarity index 57%
rename from docker-compose.stage.yml
rename to docker-compose.dev.yml
index 3a97a6411..a37ef4c55 100644
--- a/docker-compose.stage.yml
+++ b/docker-compose.dev.yml
@@ -19,15 +19,28 @@ services:
- redis
network_mode: host
- solid-connection-stage:
+ solid-connection-dev:
build:
context: .
dockerfile: Dockerfile
- container_name: solid-connection-stage
+ container_name: solid-connection-dev
ports:
- "8080:8080"
environment:
- - SPRING_PROFILES_ACTIVE=stage
+ - SPRING_PROFILES_ACTIVE=dev
+ volumes:
+ - ./logs:/var/log/spring
depends_on:
- redis
network_mode: host
+
+ alloy:
+ image: grafana/alloy:latest
+ container_name: alloy
+ ports:
+ - "12345:12345"
+ volumes:
+ - ./logs:/var/log/spring
+ - ./docs/config.alloy:/etc/alloy/config.alloy:ro
+ environment:
+ - ALLOY_ENV=dev
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 9517a07aa..30b0c9fc1 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -28,5 +28,18 @@ services:
- SPRING_PROFILES_ACTIVE=prod
- SPRING_DATA_REDIS_HOST=redis
- SPRING_DATA_REDIS_PORT=6379
+ volumes:
+ - ./logs:/var/log/spring
depends_on:
- redis
+
+ alloy:
+ image: grafana/alloy:latest
+ container_name: alloy
+ ports:
+ - "12345:12345"
+ volumes:
+ - ./logs:/var/log/spring
+ - ./docs/config.alloy:/etc/alloy/config.alloy:ro
+ environment:
+ - ALLOY_ENV=production
diff --git a/docs/code-style/solid-connection-intellij-scheme.xml b/docs/code-style/solid-connection-intellij-scheme.xml
new file mode 100644
index 000000000..3bcaefe90
--- /dev/null
+++ b/docs/code-style/solid-connection-intellij-scheme.xml
@@ -0,0 +1,595 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:.*Style
+
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:layout_width
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_height
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_weight
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_margin
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_marginTop
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_marginBottom
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_marginStart
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_marginEnd
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_marginLeft
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_marginRight
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:layout_.*
+
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:padding
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:paddingTop
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:paddingBottom
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:paddingStart
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:paddingEnd
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:paddingLeft
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:paddingRight
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*
+ http://schemas.android.com/apk/res/android
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+ http://schemas.android.com/apk/res-auto
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+ http://schemas.android.com/tools
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/infra-config/config.alloy b/docs/infra-config/config.alloy
new file mode 100644
index 000000000..bd3aedf9a
--- /dev/null
+++ b/docs/infra-config/config.alloy
@@ -0,0 +1,37 @@
+livedebugging {
+ enabled = true
+}
+
+logging {
+ level = "info"
+ format = "logfmt"
+}
+
+local.file_match "spring_logs" {
+ path_targets = [{ __path__ = "/var/log/spring/*.log" }] // 서비스 로그 파일 경로
+}
+
+loki.source.file "spring_source" {
+ targets = local.file_match.spring_logs.targets // 위에서 정의한 로그 파일 경로 사용
+ forward_to = [loki.process.spring_labels.receiver] // 읽은 로그를 처리 단계로 전달
+}
+
+loki.process "spring_labels" {
+ forward_to = [loki.write.grafana_loki.receiver] // 처리된 로그를 Loki로 전송
+
+ stage.static_labels {
+ values = {
+ service = "backend",
+ env = sys.env("ALLOY_ENV"),
+ }
+ }
+}
+
+loki.write "grafana_loki" {
+ endpoint {
+ url = "http://monitor.solid-connection.com:3100/loki/api/v1/push"
+ tenant_id = "fake" // Loki 테넌트 ID (싱글 테넌시이기에 fake로 설정)
+ batch_wait = "1s" // 로그 배치 전송 대기 시간
+ batch_size = "1MB" // 로그 배치 크기
+ }
+}
diff --git a/docs/nginx.conf b/docs/infra-config/nginx.conf
similarity index 100%
rename from docs/nginx.conf
rename to docs/infra-config/nginx.conf
diff --git a/local_compose_down.sh b/docs/script/local_compose_down.sh
similarity index 63%
rename from local_compose_down.sh
rename to docs/script/local_compose_down.sh
index 32792e490..ceb9df3e1 100755
--- a/local_compose_down.sh
+++ b/docs/script/local_compose_down.sh
@@ -2,8 +2,10 @@
set -e
+BASE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
+
echo "Stopping all docker containers..."
-docker compose -f docker-compose.local.yml down
+docker compose -f "${BASE_DIR}/docker-compose.local.yml" down
echo "Pruning unused Docker images..."
docker image prune -f
diff --git a/docs/script/local_compose_up.sh b/docs/script/local_compose_up.sh
new file mode 100755
index 000000000..df36cb0ea
--- /dev/null
+++ b/docs/script/local_compose_up.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# 명령이 0이 아닌 종료값을 가질때 즉시 종료
+set -e
+
+BASE_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
+
+if [ ! -d "${BASE_DIR}/mysql_data_local" ]; then
+ echo "mysql_data_local 디렉토리가 없습니다. ${BASE_DIR}/mysql_data_local 디렉토리를 생성합니다."
+ mkdir -p "${BASE_DIR}/mysql_data_local"
+fi
+
+if [ ! -d "${BASE_DIR}/redis_data_local" ]; then
+ echo "redis_data_local 디렉토리가 없습니다. ${BASE_DIR}/redis_data_local 디렉토리를 생성합니다."
+ mkdir -p "${BASE_DIR}/redis_data_local"
+fi
+
+echo "Starting all docker containers..."
+docker compose -f "${BASE_DIR}/docker-compose.local.yml" up -d
+
+echo "Pruning unused Docker images..."
+docker image prune -f
+
+echo "Containers are up and running."
+docker compose ps -a
diff --git a/local_compose_up.sh b/local_compose_up.sh
deleted file mode 100755
index 400861e57..000000000
--- a/local_compose_up.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/bash
-
-# 명령이 0이 아닌 종료값을 가질때 즉시 종료
-set -e
-
-if [ ! -d "mysql_data_local" ]; then
- echo "mysql_data_local 디렉토리가 없습니다. 디렉토리를 생성합니다."
- mkdir -p mysql_data_local
-fi
-
-if [ ! -d "redis_data_local" ]; then
- echo "redis_data_local 디렉토리가 없습니다. 디렉토리를 생성합니다."
- mkdir -p redis_data_local
-fi
-
-echo "Starting all docker containers..."
-docker compose -f docker-compose.local.yml up -d
-
-echo "Pruning unused Docker images..."
-docker image prune -f
-
-echo "Containers are up and running."
-docker compose ps -a
diff --git a/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java
index 47bac37e1..d2869e4d3 100644
--- a/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java
+++ b/src/main/java/com/example/solidconnection/admin/controller/AdminScoreController.java
@@ -9,7 +9,7 @@
import com.example.solidconnection.admin.dto.ScoreSearchCondition;
import com.example.solidconnection.admin.service.AdminGpaScoreService;
import com.example.solidconnection.admin.service.AdminLanguageTestScoreService;
-import com.example.solidconnection.custom.response.PageResponse;
+import com.example.solidconnection.common.response.PageResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java
index 564bc724b..36d66820d 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/GpaResponse.java
@@ -5,4 +5,5 @@ public record GpaResponse(
double gpaCriteria,
String gpaReportUrl
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java
index 5f37e823b..524142feb 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreResponse.java
@@ -1,7 +1,7 @@
package com.example.solidconnection.admin.dto;
+import com.example.solidconnection.common.VerifyStatus;
import com.example.solidconnection.score.domain.GpaScore;
-import com.example.solidconnection.type.VerifyStatus;
public record GpaScoreResponse(
long id,
@@ -10,6 +10,7 @@ public record GpaScoreResponse(
VerifyStatus verifyStatus,
String rejectedReason
) {
+
public static GpaScoreResponse from(GpaScore gpaScore) {
return new GpaScoreResponse(
gpaScore.getId(),
diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java
index 2da39fb88..30bf26469 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreSearchResponse.java
@@ -4,4 +4,5 @@ public record GpaScoreSearchResponse(
GpaScoreStatusResponse gpaScoreStatusResponse,
SiteUserResponse siteUserResponse
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java
index 49afbd4ed..00937eb2d 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreStatusResponse.java
@@ -1,7 +1,6 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.type.VerifyStatus;
-
+import com.example.solidconnection.common.VerifyStatus;
import java.time.ZonedDateTime;
public record GpaScoreStatusResponse(
@@ -12,4 +11,5 @@ public record GpaScoreStatusResponse(
ZonedDateTime createdAt,
ZonedDateTime updatedAt
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java
index b7fe58c71..85d0d2472 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/GpaScoreUpdateRequest.java
@@ -1,7 +1,7 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired;
-import com.example.solidconnection.type.VerifyStatus;
+import com.example.solidconnection.application.dto.validation.RejectedReasonRequired;
+import com.example.solidconnection.common.VerifyStatus;
import jakarta.validation.constraints.NotNull;
@RejectedReasonRequired
@@ -18,4 +18,5 @@ public record GpaScoreUpdateRequest(
String rejectedReason
) implements ScoreUpdateRequest {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java
index c91fc68c3..2740a03f3 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestResponse.java
@@ -1,10 +1,11 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.type.LanguageTestType;
+import com.example.solidconnection.university.domain.LanguageTestType;
public record LanguageTestResponse(
LanguageTestType languageTestType,
String languageTestScore,
String languageTestReportUrl
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java
index aee435c9c..b8c2d2b73 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreResponse.java
@@ -1,8 +1,8 @@
package com.example.solidconnection.admin.dto;
+import com.example.solidconnection.common.VerifyStatus;
import com.example.solidconnection.score.domain.LanguageTestScore;
-import com.example.solidconnection.type.LanguageTestType;
-import com.example.solidconnection.type.VerifyStatus;
+import com.example.solidconnection.university.domain.LanguageTestType;
public record LanguageTestScoreResponse(
long id,
@@ -11,6 +11,7 @@ public record LanguageTestScoreResponse(
VerifyStatus verifyStatus,
String rejectedReason
) {
+
public static LanguageTestScoreResponse from(LanguageTestScore languageTestScore) {
return new LanguageTestScoreResponse(
languageTestScore.getId(),
diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java
index 0e1830f66..ba4e27a7d 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreSearchResponse.java
@@ -4,4 +4,5 @@ public record LanguageTestScoreSearchResponse(
LanguageTestScoreStatusResponse languageTestScoreStatusResponse,
SiteUserResponse siteUserResponse
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java
index c852b5b2a..b2bd29fb9 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreStatusResponse.java
@@ -1,7 +1,6 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.type.VerifyStatus;
-
+import com.example.solidconnection.common.VerifyStatus;
import java.time.ZonedDateTime;
public record LanguageTestScoreStatusResponse(
@@ -12,4 +11,5 @@ public record LanguageTestScoreStatusResponse(
ZonedDateTime createdAt,
ZonedDateTime updatedAt
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java
index 3e76e0c93..150cd02b7 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/LanguageTestScoreUpdateRequest.java
@@ -1,8 +1,8 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired;
-import com.example.solidconnection.type.LanguageTestType;
-import com.example.solidconnection.type.VerifyStatus;
+import com.example.solidconnection.application.dto.validation.RejectedReasonRequired;
+import com.example.solidconnection.common.VerifyStatus;
+import com.example.solidconnection.university.domain.LanguageTestType;
import jakarta.validation.constraints.NotNull;
@RejectedReasonRequired
@@ -19,4 +19,5 @@ public record LanguageTestScoreUpdateRequest(
String rejectedReason
) implements ScoreUpdateRequest {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java b/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java
index 2e94628e6..2efdb476b 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/ScoreSearchCondition.java
@@ -1,11 +1,11 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.type.VerifyStatus;
-
+import com.example.solidconnection.common.VerifyStatus;
import java.time.LocalDate;
public record ScoreSearchCondition(
VerifyStatus verifyStatus,
String nickname,
LocalDate createdAt) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java b/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java
index 184f76100..8e04b06de 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/ScoreUpdateRequest.java
@@ -1,8 +1,10 @@
package com.example.solidconnection.admin.dto;
-import com.example.solidconnection.type.VerifyStatus;
+import com.example.solidconnection.common.VerifyStatus;
public interface ScoreUpdateRequest {
+
VerifyStatus verifyStatus();
+
String rejectedReason();
}
diff --git a/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java b/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java
index 1b62f262f..0b6d23816 100644
--- a/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java
+++ b/src/main/java/com/example/solidconnection/admin/dto/SiteUserResponse.java
@@ -5,4 +5,5 @@ public record SiteUserResponse(
String nickname,
String profileImageUrl
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java b/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java
index c761ff485..717f2a963 100644
--- a/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java
+++ b/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java
@@ -1,22 +1,22 @@
package com.example.solidconnection.admin.service;
+import static com.example.solidconnection.common.exception.ErrorCode.GPA_SCORE_NOT_FOUND;
+
import com.example.solidconnection.admin.dto.GpaScoreResponse;
import com.example.solidconnection.admin.dto.GpaScoreSearchResponse;
import com.example.solidconnection.admin.dto.GpaScoreUpdateRequest;
import com.example.solidconnection.admin.dto.ScoreSearchCondition;
import com.example.solidconnection.application.domain.Gpa;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.common.VerifyStatus;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.score.domain.GpaScore;
import com.example.solidconnection.score.repository.GpaScoreRepository;
-import com.example.solidconnection.type.VerifyStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import static com.example.solidconnection.custom.exception.ErrorCode.GPA_SCORE_NOT_FOUND;
-
@RequiredArgsConstructor
@Service
public class AdminGpaScoreService {
diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java b/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java
index 380ef02c6..0aa4a859d 100644
--- a/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java
+++ b/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java
@@ -1,22 +1,22 @@
package com.example.solidconnection.admin.service;
+import static com.example.solidconnection.common.exception.ErrorCode.LANGUAGE_TEST_SCORE_NOT_FOUND;
+
import com.example.solidconnection.admin.dto.LanguageTestScoreResponse;
import com.example.solidconnection.admin.dto.LanguageTestScoreSearchResponse;
import com.example.solidconnection.admin.dto.LanguageTestScoreUpdateRequest;
import com.example.solidconnection.admin.dto.ScoreSearchCondition;
import com.example.solidconnection.application.domain.LanguageTest;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.common.VerifyStatus;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.score.domain.LanguageTestScore;
import com.example.solidconnection.score.repository.LanguageTestScoreRepository;
-import com.example.solidconnection.type.VerifyStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import static com.example.solidconnection.custom.exception.ErrorCode.LANGUAGE_TEST_SCORE_NOT_FOUND;
-
@RequiredArgsConstructor
@Service
public class AdminLanguageTestScoreService {
diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java
index 36c7d6af2..07571d060 100644
--- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java
+++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java
@@ -5,9 +5,9 @@
import com.example.solidconnection.application.dto.ApplyRequest;
import com.example.solidconnection.application.service.ApplicationQueryService;
import com.example.solidconnection.application.service.ApplicationSubmissionService;
-import com.example.solidconnection.custom.resolver.AuthorizedUser;
-import com.example.solidconnection.custom.security.annotation.RequireAdminAccess;
-import com.example.solidconnection.siteuser.domain.SiteUser;
+import com.example.solidconnection.common.resolver.AuthorizedUser;
+import com.example.solidconnection.security.annotation.RequireRoleAccess;
+import com.example.solidconnection.siteuser.domain.Role;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@@ -30,34 +30,34 @@ public class ApplicationController {
// 지원서 제출하기 api
@PostMapping
public ResponseEntity apply(
- @AuthorizedUser SiteUser siteUser,
+ @AuthorizedUser long siteUserId,
@Valid @RequestBody ApplyRequest applyRequest
) {
- ApplicationSubmissionResponse applicationSubmissionResponse = applicationSubmissionService.apply(siteUser, applyRequest);
+ ApplicationSubmissionResponse applicationSubmissionResponse = applicationSubmissionService.apply(siteUserId, applyRequest);
return ResponseEntity
.status(HttpStatus.OK)
.body(applicationSubmissionResponse);
}
- @RequireAdminAccess
+ @RequireRoleAccess(roles = {Role.ADMIN})
@GetMapping
public ResponseEntity getApplicants(
- @AuthorizedUser SiteUser siteUser,
+ @AuthorizedUser long siteUserId,
@RequestParam(required = false, defaultValue = "") String region,
@RequestParam(required = false, defaultValue = "") String keyword
) {
- applicationQueryService.validateSiteUserCanViewApplicants(siteUser);
- ApplicationsResponse result = applicationQueryService.getApplicants(siteUser, region, keyword);
+ applicationQueryService.validateSiteUserCanViewApplicants(siteUserId);
+ ApplicationsResponse result = applicationQueryService.getApplicants(siteUserId, region, keyword);
return ResponseEntity
.ok(result);
}
@GetMapping("/competitors")
public ResponseEntity getApplicantsForUserCompetitors(
- @AuthorizedUser SiteUser siteUser
+ @AuthorizedUser long siteUserId
) {
- applicationQueryService.validateSiteUserCanViewApplicants(siteUser);
- ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(siteUser);
+ applicationQueryService.validateSiteUserCanViewApplicants(siteUserId);
+ ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(siteUserId);
return ResponseEntity
.ok(result);
}
diff --git a/src/main/java/com/example/solidconnection/application/domain/Application.java b/src/main/java/com/example/solidconnection/application/domain/Application.java
index 6caa75331..72eb0068b 100644
--- a/src/main/java/com/example/solidconnection/application/domain/Application.java
+++ b/src/main/java/com/example/solidconnection/application/domain/Application.java
@@ -1,31 +1,40 @@
package com.example.solidconnection.application.domain;
+import static com.example.solidconnection.common.VerifyStatus.PENDING;
+
+import com.example.solidconnection.common.VerifyStatus;
import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.type.VerifyStatus;
-import com.example.solidconnection.university.domain.UniversityInfoForApply;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
-import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
-import static com.example.solidconnection.type.VerifyStatus.PENDING;
-
@Getter
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
@DynamicUpdate
@DynamicInsert
@Entity
+@Table(indexes = {
+ @Index(name = "idx_app_user_term_delete",
+ columnList = "site_user_id, term, is_delete"),
+ @Index(name = "idx_app_first_choice_search",
+ columnList = "verify_status, term, is_delete, first_choice_university_info_for_apply_id"),
+ @Index(name = "idx_app_second_choice_search",
+ columnList = "verify_status, term, is_delete, second_choice_university_info_for_apply_id"),
+ @Index(name = "idx_app_third_choice_search",
+ columnList = "verify_status, term, is_delete, third_choice_university_info_for_apply_id")
+})
public class Application {
@Id
@@ -39,40 +48,40 @@ public class Application {
private LanguageTest languageTest;
@Setter
- @Column(columnDefinition = "varchar(50) not null default 'PENDING'")
+ @Column(name = "verify_status", nullable = false)
@Enumerated(EnumType.STRING)
- private VerifyStatus verifyStatus;
+ private VerifyStatus verifyStatus = VerifyStatus.PENDING;
- @Column(length = 100)
+ @Column(length = 100, name = "nickname_for_apply")
private String nicknameForApply;
- @Column(columnDefinition = "int not null default 1")
+ @Column(columnDefinition = "int not null default 1", name = "update_count")
private Integer updateCount;
- @Column(length = 50, nullable = false)
+ @Column(length = 50, nullable = false, name = "term")
private String term;
- @Column
+ @Column(name = "is_delete")
private boolean isDelete = false;
- @ManyToOne(fetch = FetchType.LAZY)
- private UniversityInfoForApply firstChoiceUniversity;
+ @Column(nullable = false, name = "first_choice_university_info_for_apply_id")
+ private long firstChoiceUnivApplyInfoId;
- @ManyToOne(fetch = FetchType.LAZY)
- private UniversityInfoForApply secondChoiceUniversity;
+ @Column(name = "second_choice_university_info_for_apply_id")
+ private Long secondChoiceUnivApplyInfoId;
- @ManyToOne(fetch = FetchType.LAZY)
- private UniversityInfoForApply thirdChoiceUniversity;
+ @Column(name = "third_choice_university_info_for_apply_id")
+ private Long thirdChoiceUnivApplyInfoId;
- @ManyToOne(fetch = FetchType.LAZY)
- private SiteUser siteUser;
+ @Column(name = "site_user_id")
+ private long siteUserId;
public Application(
SiteUser siteUser,
Gpa gpa,
LanguageTest languageTest,
String term) {
- this.siteUser = siteUser;
+ this.siteUserId = siteUser.getId();
this.gpa = gpa;
this.languageTest = languageTest;
this.term = term;
@@ -86,18 +95,18 @@ public Application(
LanguageTest languageTest,
String term,
Integer updateCount,
- UniversityInfoForApply firstChoiceUniversity,
- UniversityInfoForApply secondChoiceUniversity,
- UniversityInfoForApply thirdChoiceUniversity,
+ long firstChoiceUnivApplyInfoId,
+ Long secondChoiceUnivApplyInfoId,
+ Long thirdChoiceUnivApplyInfoId,
String nicknameForApply) {
- this.siteUser = siteUser;
+ this.siteUserId = siteUser.getId();
this.gpa = gpa;
this.languageTest = languageTest;
this.term = term;
this.updateCount = updateCount;
- this.firstChoiceUniversity = firstChoiceUniversity;
- this.secondChoiceUniversity = secondChoiceUniversity;
- this.thirdChoiceUniversity = thirdChoiceUniversity;
+ this.firstChoiceUnivApplyInfoId = firstChoiceUnivApplyInfoId;
+ this.secondChoiceUnivApplyInfoId = secondChoiceUnivApplyInfoId;
+ this.thirdChoiceUnivApplyInfoId = thirdChoiceUnivApplyInfoId;
this.nicknameForApply = nicknameForApply;
this.verifyStatus = PENDING;
}
@@ -107,18 +116,18 @@ public Application(
Gpa gpa,
LanguageTest languageTest,
String term,
- UniversityInfoForApply firstChoiceUniversity,
- UniversityInfoForApply secondChoiceUniversity,
- UniversityInfoForApply thirdChoiceUniversity,
+ long firstChoiceUnivApplyInfoId,
+ Long secondChoiceUnivApplyInfoId,
+ Long thirdChoiceUnivApplyInfoId,
String nicknameForApply) {
- this.siteUser = siteUser;
+ this.siteUserId = siteUser.getId();
this.gpa = gpa;
this.languageTest = languageTest;
this.term = term;
this.updateCount = 1;
- this.firstChoiceUniversity = firstChoiceUniversity;
- this.secondChoiceUniversity = secondChoiceUniversity;
- this.thirdChoiceUniversity = thirdChoiceUniversity;
+ this.firstChoiceUnivApplyInfoId = firstChoiceUnivApplyInfoId;
+ this.secondChoiceUnivApplyInfoId = secondChoiceUnivApplyInfoId;
+ this.thirdChoiceUnivApplyInfoId = thirdChoiceUnivApplyInfoId;
this.nicknameForApply = nicknameForApply;
this.verifyStatus = PENDING;
}
@@ -126,18 +135,4 @@ public Application(
public void setIsDeleteTrue() {
this.isDelete = true;
}
-
- public void updateUniversityChoice(
- UniversityInfoForApply firstChoiceUniversity,
- UniversityInfoForApply secondChoiceUniversity,
- UniversityInfoForApply thirdChoiceUniversity,
- String nicknameForApply) {
- if (this.firstChoiceUniversity != null) {
- this.updateCount++;
- }
- this.firstChoiceUniversity = firstChoiceUniversity;
- this.secondChoiceUniversity = secondChoiceUniversity;
- this.thirdChoiceUniversity = thirdChoiceUniversity;
- this.nicknameForApply = nicknameForApply;
- }
}
diff --git a/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java b/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java
index 4295372d4..4a0544ea7 100644
--- a/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java
+++ b/src/main/java/com/example/solidconnection/application/domain/LanguageTest.java
@@ -1,6 +1,6 @@
package com.example.solidconnection.application.domain;
-import com.example.solidconnection.type.LanguageTestType;
+import com.example.solidconnection.university.domain.LanguageTestType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EnumType;
diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java
index 9835491b1..c6161a134 100644
--- a/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java
+++ b/src/main/java/com/example/solidconnection/application/dto/ApplicantResponse.java
@@ -1,7 +1,7 @@
package com.example.solidconnection.application.dto;
import com.example.solidconnection.application.domain.Application;
-import com.example.solidconnection.type.LanguageTestType;
+import com.example.solidconnection.university.domain.LanguageTestType;
public record ApplicantResponse(
String nicknameForApply,
diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicantsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicantsResponse.java
new file mode 100644
index 000000000..fdb9c357c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/application/dto/ApplicantsResponse.java
@@ -0,0 +1,30 @@
+package com.example.solidconnection.application.dto;
+
+import com.example.solidconnection.application.domain.Application;
+import com.example.solidconnection.siteuser.domain.SiteUser;
+import com.example.solidconnection.university.domain.UnivApplyInfo;
+import java.util.List;
+import java.util.Objects;
+
+public record ApplicantsResponse(
+ String koreanName,
+ int studentCapacity,
+ String region,
+ String country,
+ List applicants) {
+
+ public static ApplicantsResponse of(UnivApplyInfo univApplyInfo, List applications, SiteUser siteUser) {
+ return new ApplicantsResponse(
+ univApplyInfo.getKoreanName(),
+ univApplyInfo.getStudentCapacity(),
+ univApplyInfo.getUniversity().getRegion().getKoreanName(),
+ univApplyInfo.getUniversity().getCountry().getKoreanName(),
+ applications.stream()
+ .map(application -> ApplicantResponse.of(application, isUsers(application, siteUser)))
+ .toList());
+ }
+
+ private static boolean isUsers(Application application, SiteUser siteUser) {
+ return Objects.equals(application.getSiteUserId(), siteUser.getId());
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java
index e90994c37..a2636c135 100644
--- a/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java
+++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationSubmissionResponse.java
@@ -5,6 +5,7 @@
public record ApplicationSubmissionResponse(
int applyCount
) {
+
public static ApplicationSubmissionResponse from(Application application) {
return new ApplicationSubmissionResponse(application.getUpdateCount());
}
diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java
index a3429c1ef..657c7c2c3 100644
--- a/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java
+++ b/src/main/java/com/example/solidconnection/application/dto/ApplicationsResponse.java
@@ -3,7 +3,8 @@
import java.util.List;
public record ApplicationsResponse(
- List firstChoice,
- List secondChoice,
- List thirdChoice) {
+ List firstChoice,
+ List secondChoice,
+ List thirdChoice) {
+
}
diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java
index 7c4da1c99..c50252c00 100644
--- a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java
+++ b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java
@@ -1,5 +1,6 @@
package com.example.solidconnection.application.dto;
+import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@@ -12,6 +13,8 @@ public record ApplyRequest(
Long languageTestScoreId,
@Valid
- UniversityChoiceRequest universityChoiceRequest
+ @JsonProperty("universityChoiceRequest")
+ UnivApplyInfoChoiceRequest univApplyInfoChoiceRequest
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/application/dto/UnivApplyInfoChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UnivApplyInfoChoiceRequest.java
new file mode 100644
index 000000000..449b1ca2c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/application/dto/UnivApplyInfoChoiceRequest.java
@@ -0,0 +1,18 @@
+package com.example.solidconnection.application.dto;
+
+import com.example.solidconnection.university.dto.validation.ValidUnivApplyInfoChoice;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@ValidUnivApplyInfoChoice
+public record UnivApplyInfoChoiceRequest(
+
+ @JsonProperty("firstChoiceUniversityId")
+ Long firstChoiceUnivApplyInfoId,
+
+ @JsonProperty("secondChoiceUniversityId")
+ Long secondChoiceUnivApplyInfoId,
+
+ @JsonProperty("thirdChoiceUniversityId")
+ Long thirdChoiceUnivApplyInfoId) {
+
+}
diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java b/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java
deleted file mode 100644
index 1d3415003..000000000
--- a/src/main/java/com/example/solidconnection/application/dto/UniversityApplicantsResponse.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.example.solidconnection.application.dto;
-
-import com.example.solidconnection.university.domain.UniversityInfoForApply;
-
-import java.util.List;
-
-public record UniversityApplicantsResponse(
- String koreanName,
- int studentCapacity,
- String region,
- String country,
- List applicants) {
-
- public static UniversityApplicantsResponse of(UniversityInfoForApply universityInfoForApply, List applicant) {
- return new UniversityApplicantsResponse(
- universityInfoForApply.getKoreanName(),
- universityInfoForApply.getStudentCapacity(),
- universityInfoForApply.getUniversity().getRegion().getKoreanName(),
- universityInfoForApply.getUniversity().getCountry().getKoreanName(),
- applicant);
- }
-}
diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java
deleted file mode 100644
index d219dbc2e..000000000
--- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.example.solidconnection.application.dto;
-
-import com.example.solidconnection.custom.validation.annotation.ValidUniversityChoice;
-
-@ValidUniversityChoice
-public record UniversityChoiceRequest(
- Long firstChoiceUniversityId,
- Long secondChoiceUniversityId,
- Long thirdChoiceUniversityId) {
-}
diff --git a/src/main/java/com/example/solidconnection/custom/validation/annotation/RejectedReasonRequired.java b/src/main/java/com/example/solidconnection/application/dto/validation/RejectedReasonRequired.java
similarity index 78%
rename from src/main/java/com/example/solidconnection/custom/validation/annotation/RejectedReasonRequired.java
rename to src/main/java/com/example/solidconnection/application/dto/validation/RejectedReasonRequired.java
index 4ae4a6618..7f00291ba 100644
--- a/src/main/java/com/example/solidconnection/custom/validation/annotation/RejectedReasonRequired.java
+++ b/src/main/java/com/example/solidconnection/application/dto/validation/RejectedReasonRequired.java
@@ -1,9 +1,7 @@
-package com.example.solidconnection.custom.validation.annotation;
+package com.example.solidconnection.application.dto.validation;
-import com.example.solidconnection.custom.validation.validator.RejectedReasonValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
-
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -15,6 +13,8 @@
public @interface RejectedReasonRequired {
String message() default "거절 사유 입력값이 올바르지 않습니다.";
+
Class>[] groups() default {};
+
Class extends Payload>[] payload() default {};
}
diff --git a/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java b/src/main/java/com/example/solidconnection/application/dto/validation/RejectedReasonValidator.java
similarity index 82%
rename from src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java
rename to src/main/java/com/example/solidconnection/application/dto/validation/RejectedReasonValidator.java
index c975a0a0a..85e6ecc78 100644
--- a/src/main/java/com/example/solidconnection/custom/validation/validator/RejectedReasonValidator.java
+++ b/src/main/java/com/example/solidconnection/application/dto/validation/RejectedReasonValidator.java
@@ -1,14 +1,13 @@
-package com.example.solidconnection.custom.validation.validator;
+package com.example.solidconnection.application.dto.validation;
+
+import static com.example.solidconnection.common.exception.ErrorCode.REJECTED_REASON_REQUIRED;
import com.example.solidconnection.admin.dto.ScoreUpdateRequest;
-import com.example.solidconnection.custom.validation.annotation.RejectedReasonRequired;
-import com.example.solidconnection.type.VerifyStatus;
+import com.example.solidconnection.common.VerifyStatus;
import io.micrometer.common.util.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
-import static com.example.solidconnection.custom.exception.ErrorCode.REJECTED_REASON_REQUIRED;
-
public class RejectedReasonValidator implements ConstraintValidator {
private static final String REJECTED_REASON = "rejectedReason";
diff --git a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
index 5aeb972bf..3916f37db 100644
--- a/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
+++ b/src/main/java/com/example/solidconnection/application/repository/ApplicationRepository.java
@@ -1,44 +1,43 @@
package com.example.solidconnection.application.repository;
+import static com.example.solidconnection.common.exception.ErrorCode.APPLICATION_NOT_FOUND;
+
import com.example.solidconnection.application.domain.Application;
-import com.example.solidconnection.custom.exception.CustomException;
-import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.type.VerifyStatus;
-import com.example.solidconnection.university.domain.UniversityInfoForApply;
+import com.example.solidconnection.common.VerifyStatus;
+import com.example.solidconnection.common.exception.CustomException;
+import java.util.List;
+import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
-import org.springframework.stereotype.Repository;
-
-import java.util.List;
-import java.util.Optional;
-import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_FOUND;
-
-@Repository
public interface ApplicationRepository extends JpaRepository {
boolean existsByNicknameForApply(String nicknameForApply);
- List findAllByFirstChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(
- UniversityInfoForApply firstChoiceUniversity, VerifyStatus verifyStatus, String term);
-
- List findAllBySecondChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(
- UniversityInfoForApply secondChoiceUniversity, VerifyStatus verifyStatus, String term);
-
- List findAllByThirdChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(
- UniversityInfoForApply thirdChoiceUniversity, VerifyStatus verifyStatus, String term);
-
@Query("""
- SELECT a FROM Application a
- WHERE a.siteUser = :siteUser
- AND a.term = :term
- AND a.isDelete = false
- """)
- Optional findBySiteUserAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term);
+ SELECT a
+ FROM Application a
+ WHERE (a.firstChoiceUnivApplyInfoId IN :univApplyInfoIds
+ OR a.secondChoiceUnivApplyInfoId IN :univApplyInfoIds
+ OR a.thirdChoiceUnivApplyInfoId IN :univApplyInfoIds)
+ AND a.verifyStatus = :status
+ AND a.term = :term
+ AND a.isDelete = false
+ """)
+ List findAllByUnivApplyInfoIds(@Param("univApplyInfoIds") List univApplyInfoIds, @Param("status") VerifyStatus status, @Param("term") String term);
- default Application getApplicationBySiteUserAndTerm(SiteUser siteUser, String term) {
- return findBySiteUserAndTerm(siteUser, term)
+ @Query("""
+ SELECT a
+ FROM Application a
+ WHERE a.siteUserId = :siteUserId
+ AND a.term = :term
+ AND a.isDelete = false
+ """)
+ Optional findBySiteUserIdAndTerm(@Param("siteUserId") long siteUserId, @Param("term") String term);
+
+ default Application getApplicationBySiteUserIdAndTerm(long siteUserId, String term) {
+ return findBySiteUserIdAndTerm(siteUserId, term)
.orElseThrow(() -> new CustomException(APPLICATION_NOT_FOUND));
}
}
diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java
index 157c6adcd..7eba19de3 100644
--- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java
+++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java
@@ -1,134 +1,136 @@
package com.example.solidconnection.application.service;
+import static com.example.solidconnection.common.exception.ErrorCode.APPLICATION_NOT_APPROVED;
+import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
+
import com.example.solidconnection.application.domain.Application;
-import com.example.solidconnection.application.dto.ApplicantResponse;
+import com.example.solidconnection.application.dto.ApplicantsResponse;
import com.example.solidconnection.application.dto.ApplicationsResponse;
-import com.example.solidconnection.application.dto.UniversityApplicantsResponse;
import com.example.solidconnection.application.repository.ApplicationRepository;
-import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.common.VerifyStatus;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.type.VerifyStatus;
-import com.example.solidconnection.university.domain.University;
-import com.example.solidconnection.university.domain.UniversityInfoForApply;
-import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository;
-import com.example.solidconnection.university.repository.custom.UniversityFilterRepositoryImpl;
-import lombok.RequiredArgsConstructor;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.Arrays;
+import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import com.example.solidconnection.university.domain.UnivApplyInfo;
+import com.example.solidconnection.university.repository.UnivApplyInfoRepository;
+import com.example.solidconnection.university.repository.custom.UnivApplyInfoFilterRepositoryImpl;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.APPLICATION_NOT_APPROVED;
+import java.util.stream.Stream;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Service
public class ApplicationQueryService {
private final ApplicationRepository applicationRepository;
- private final UniversityInfoForApplyRepository universityInfoForApplyRepository;
- private final UniversityFilterRepositoryImpl universityFilterRepository;
+ private final UnivApplyInfoRepository univApplyInfoRepository;
+ private final UnivApplyInfoFilterRepositoryImpl universityFilterRepository;
+ private final SiteUserRepository siteUserRepository;
@Value("${university.term}")
public String term;
- /*
- * 다른 지원자들의 성적을 조회한다.
- * - 유저가 다른 지원자들을 볼 수 있는지 검증한다.
- * - 지역과 키워드를 통해 대학을 필터링한다.
- * - 지역은 영어 대문자로 받는다 e.g. ASIA
- * - 1지망, 2지망 지원자들을 조회한다.
- * */
+ // todo: 캐싱 정책 변경 시 수정 필요
@Transactional(readOnly = true)
- // todo: 임시로 단일 키로 캐시 적용. 추후 캐싱 전략 재검토 필요.
- @ThunderingHerdCaching(key = "applications:all", cacheManager = "customCacheManager", ttlSec = 86400)
- public ApplicationsResponse getApplicants(SiteUser siteUser, String regionCode, String keyword) {
- // 국가와 키워드와 지역을 통해 대학을 필터링한다.
- List universities
- = universityFilterRepository.findByRegionCodeAndKeywords(regionCode, List.of(keyword));
-
- // 1지망, 2지망, 3지망 지원자들을 조회한다.
- List firstChoiceApplicants = getFirstChoiceApplicants(universities, siteUser, term);
- List secondChoiceApplicants = getSecondChoiceApplicants(universities, siteUser, term);
- List thirdChoiceApplicants = getThirdChoiceApplicants(universities, siteUser, term);
- return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants);
+ public ApplicationsResponse getApplicants(long siteUserId, String regionCode, String keyword) {
+ // 1. 대학 지원 정보 필터링 (regionCode, keyword)
+ SiteUser siteUser = siteUserRepository.findById(siteUserId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ List univApplyInfos = universityFilterRepository.findAllByRegionCodeAndKeywords(regionCode, List.of(keyword));
+ if (univApplyInfos.isEmpty()) {
+ return new ApplicationsResponse(List.of(), List.of(), List.of());
+ }
+ // 2. 조건에 맞는 모든 Application 한 번에 조회
+ List univApplyInfoIds = univApplyInfos.stream()
+ .map(UnivApplyInfo::getId)
+ .toList();
+ List applications = applicationRepository.findAllByUnivApplyInfoIds(univApplyInfoIds, VerifyStatus.APPROVED, term);
+ // 3. 지원서 분류 및 DTO 변환
+ return classifyApplicationsByChoice(univApplyInfos, applications, siteUser);
}
@Transactional(readOnly = true)
- public ApplicationsResponse getApplicantsByUserApplications(SiteUser siteUser) {
- Application userLatestApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term);
- List userAppliedUniversities = Arrays.asList(
- Optional.ofNullable(userLatestApplication.getFirstChoiceUniversity())
- .map(UniversityInfoForApply::getUniversity)
- .orElse(null),
- Optional.ofNullable(userLatestApplication.getSecondChoiceUniversity())
- .map(UniversityInfoForApply::getUniversity)
- .orElse(null),
- Optional.ofNullable(userLatestApplication.getThirdChoiceUniversity())
- .map(UniversityInfoForApply::getUniversity)
- .orElse(null)
- ).stream()
+ public ApplicationsResponse getApplicantsByUserApplications(long siteUserId) {
+ SiteUser siteUser = siteUserRepository.findById(siteUserId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ Application userLatestApplication = applicationRepository.getApplicationBySiteUserIdAndTerm(siteUser.getId(), term);
+
+ List univApplyInfoIds = Stream.of(
+ userLatestApplication.getFirstChoiceUnivApplyInfoId(),
+ userLatestApplication.getSecondChoiceUnivApplyInfoId(),
+ userLatestApplication.getThirdChoiceUnivApplyInfoId()
+ )
.filter(Objects::nonNull)
.collect(Collectors.toList());
- List firstChoiceApplicants = getFirstChoiceApplicants(userAppliedUniversities, siteUser, term);
- List secondChoiceApplicants = getSecondChoiceApplicants(userAppliedUniversities, siteUser, term);
- List thirdChoiceApplicants = getThirdChoiceApplicants(userAppliedUniversities, siteUser, term);
- return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants);
- }
-
- // 학기별로 상태가 관리된다.
- // 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다.
- @Transactional(readOnly = true)
- public void validateSiteUserCanViewApplicants(SiteUser siteUser) {
- VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term).getVerifyStatus();
- if (verifyStatus != VerifyStatus.APPROVED) {
- throw new CustomException(APPLICATION_NOT_APPROVED);
+ if (univApplyInfoIds.isEmpty()) {
+ return new ApplicationsResponse(List.of(), List.of(), List.of());
}
- }
- private List getFirstChoiceApplicants(List universities, SiteUser siteUser, String term) {
- return getApplicantsByChoice(
- universities,
- siteUser,
- uia -> applicationRepository.findAllByFirstChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(uia, VerifyStatus.APPROVED, term)
- );
+ List applications = applicationRepository.findAllByUnivApplyInfoIds(univApplyInfoIds, VerifyStatus.APPROVED, term);
+ List univApplyInfos = univApplyInfoRepository.findAllByIds(univApplyInfoIds);
+
+ return classifyApplicationsByChoice(univApplyInfos, applications, siteUser);
}
- private List getSecondChoiceApplicants(List universities, SiteUser siteUser, String term) {
- return getApplicantsByChoice(
- universities,
- siteUser,
- uia -> applicationRepository.findAllBySecondChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(uia, VerifyStatus.APPROVED, term)
- );
+ private ApplicationsResponse classifyApplicationsByChoice(
+ List univApplyInfos,
+ List applications,
+ SiteUser siteUser) {
+ Map> firstChoiceMap = createChoiceMap(applications, Application::getFirstChoiceUnivApplyInfoId);
+ Map> secondChoiceMap = createChoiceMap(applications, Application::getSecondChoiceUnivApplyInfoId);
+ Map> thirdChoiceMap = createChoiceMap(applications, Application::getThirdChoiceUnivApplyInfoId);
+
+ List firstChoiceApplicants =
+ createUniversityApplicantsResponses(univApplyInfos, firstChoiceMap, siteUser);
+ List secondChoiceApplicants =
+ createUniversityApplicantsResponses(univApplyInfos, secondChoiceMap, siteUser);
+ List thirdChoiceApplicants =
+ createUniversityApplicantsResponses(univApplyInfos, thirdChoiceMap, siteUser);
+
+ return new ApplicationsResponse(firstChoiceApplicants, secondChoiceApplicants, thirdChoiceApplicants);
}
- private List getThirdChoiceApplicants(List universities, SiteUser siteUser, String term) {
- return getApplicantsByChoice(
- universities,
- siteUser,
- uia -> applicationRepository.findAllByThirdChoiceUniversityAndVerifyStatusAndTermAndIsDeleteFalse(uia, VerifyStatus.APPROVED, term)
- );
+ private Map> createChoiceMap(
+ List applications,
+ Function choiceIdExtractor) {
+ Map> choiceMap = new HashMap<>();
+
+ for (Application application : applications) {
+ Long choiceId = choiceIdExtractor.apply(application);
+ if (choiceId != null) {
+ choiceMap.computeIfAbsent(choiceId, k -> new ArrayList<>()).add(application);
+ }
+ }
+
+ return choiceMap;
}
- private List getApplicantsByChoice(
- List searchedUniversities,
- SiteUser siteUser,
- Function> findApplicationsByChoice) {
- return universityInfoForApplyRepository.findByUniversitiesAndTerm(searchedUniversities, term).stream()
- .map(universityInfoForApply -> UniversityApplicantsResponse.of(
- universityInfoForApply,
- findApplicationsByChoice.apply(universityInfoForApply).stream()
- .map(ap -> ApplicantResponse.of(
- ap,
- Objects.equals(siteUser.getId(), ap.getSiteUser().getId())))
- .toList()))
+ private List createUniversityApplicantsResponses(
+ List univApplyInfos,
+ Map> choiceMap,
+ SiteUser siteUser) {
+ return univApplyInfos.stream()
+ .map(uia -> ApplicantsResponse.of(uia, choiceMap.getOrDefault(uia.getId(), List.of()), siteUser))
.toList();
}
+
+ @Transactional(readOnly = true)
+ public void validateSiteUserCanViewApplicants(long siteUserId) {
+ SiteUser siteUser = siteUserRepository.findById(siteUserId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserIdAndTerm(siteUser.getId(), term).getVerifyStatus();
+ if (verifyStatus != VerifyStatus.APPROVED) {
+ throw new CustomException(APPLICATION_NOT_APPROVED);
+ }
+ }
}
diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java
index 432e93aff..4fd403d3a 100644
--- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java
+++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java
@@ -1,33 +1,31 @@
package com.example.solidconnection.application.service;
+import static com.example.solidconnection.common.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED;
+import static com.example.solidconnection.common.exception.ErrorCode.GPA_SCORE_NOT_FOUND;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_GPA_SCORE_STATUS;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS;
+import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
+
import com.example.solidconnection.application.domain.Application;
import com.example.solidconnection.application.dto.ApplicationSubmissionResponse;
import com.example.solidconnection.application.dto.ApplyRequest;
-import com.example.solidconnection.application.dto.UniversityChoiceRequest;
+import com.example.solidconnection.application.dto.UnivApplyInfoChoiceRequest;
import com.example.solidconnection.application.repository.ApplicationRepository;
-import com.example.solidconnection.cache.annotation.DefaultCacheOut;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.common.VerifyStatus;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.score.domain.GpaScore;
import com.example.solidconnection.score.domain.LanguageTestScore;
import com.example.solidconnection.score.repository.GpaScoreRepository;
import com.example.solidconnection.score.repository.LanguageTestScoreRepository;
import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.type.VerifyStatus;
-import com.example.solidconnection.university.domain.UniversityInfoForApply;
-import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository;
+import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.util.Optional;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED;
-import static com.example.solidconnection.custom.exception.ErrorCode.GPA_SCORE_NOT_FOUND;
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS;
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE;
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS;
-
@RequiredArgsConstructor
@Service
public class ApplicationSubmissionService {
@@ -35,9 +33,9 @@ public class ApplicationSubmissionService {
public static final int APPLICATION_UPDATE_COUNT_LIMIT = 3;
private final ApplicationRepository applicationRepository;
- private final UniversityInfoForApplyRepository universityInfoForApplyRepository;
private final GpaScoreRepository gpaScoreRepository;
private final LanguageTestScoreRepository languageTestScoreRepository;
+ private final SiteUserRepository siteUserRepository;
@Value("${university.term}")
private String term;
@@ -45,26 +43,18 @@ public class ApplicationSubmissionService {
// 학점 및 어학성적이 모두 유효한 경우에만 지원서 등록이 가능하다.
// 기존에 있던 status field 우선 APRROVED로 입력시킨다.
@Transactional
- // todo: 임시로 새로운 신청 생성 시 기존 캐싱 데이터를 삭제한다. 추후 수정 필요
- @DefaultCacheOut(
- key = {"applications:all"},
- cacheManager = "customCacheManager"
- )
- public ApplicationSubmissionResponse apply(SiteUser siteUser, ApplyRequest applyRequest) {
- UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest();
+ public ApplicationSubmissionResponse apply(long siteUserId, ApplyRequest applyRequest) {
+ SiteUser siteUser = siteUserRepository.findById(siteUserId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ UnivApplyInfoChoiceRequest univApplyInfoChoiceRequest = applyRequest.univApplyInfoChoiceRequest();
GpaScore gpaScore = getValidGpaScore(siteUser, applyRequest.gpaScoreId());
LanguageTestScore languageTestScore = getValidLanguageTestScore(siteUser, applyRequest.languageTestScoreId());
- UniversityInfoForApply firstChoiceUniversity = universityInfoForApplyRepository
- .getUniversityInfoForApplyByIdAndTerm(universityChoiceRequest.firstChoiceUniversityId(), term);
- UniversityInfoForApply secondChoiceUniversity = Optional.ofNullable(universityChoiceRequest.secondChoiceUniversityId())
- .map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term))
- .orElse(null);
- UniversityInfoForApply thirdChoiceUniversity = Optional.ofNullable(universityChoiceRequest.thirdChoiceUniversityId())
- .map(id -> universityInfoForApplyRepository.getUniversityInfoForApplyByIdAndTerm(id, term))
- .orElse(null);
+ long firstChoiceUnivApplyInfoId = univApplyInfoChoiceRequest.firstChoiceUnivApplyInfoId();
+ Long secondChoiceUnivApplyInfoId = univApplyInfoChoiceRequest.secondChoiceUnivApplyInfoId();
+ Long thirdChoiceUnivApplyInfoId = univApplyInfoChoiceRequest.thirdChoiceUnivApplyInfoId();
- Optional existingApplication = applicationRepository.findBySiteUserAndTerm(siteUser, term);
+ Optional existingApplication = applicationRepository.findBySiteUserIdAndTerm(siteUser.getId(), term);
int updateCount = existingApplication
.map(application -> {
validateUpdateLimitNotExceed(application);
@@ -72,15 +62,27 @@ public ApplicationSubmissionResponse apply(SiteUser siteUser, ApplyRequest apply
return application.getUpdateCount() + 1;
})
.orElse(1);
- Application newApplication = new Application(siteUser, gpaScore.getGpa(), languageTestScore.getLanguageTest(),
- term, updateCount, firstChoiceUniversity, secondChoiceUniversity, thirdChoiceUniversity, getRandomNickname());
+
+ Application newApplication = new Application(
+ siteUser,
+ gpaScore.getGpa(),
+ languageTestScore.getLanguageTest(),
+ term,
+ updateCount,
+ firstChoiceUnivApplyInfoId,
+ secondChoiceUnivApplyInfoId,
+ thirdChoiceUnivApplyInfoId,
+ getRandomNickname()
+ );
+
newApplication.setVerifyStatus(VerifyStatus.APPROVED);
applicationRepository.save(newApplication);
+
return ApplicationSubmissionResponse.from(newApplication);
}
private GpaScore getValidGpaScore(SiteUser siteUser, Long gpaScoreId) {
- GpaScore gpaScore = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)
+ GpaScore gpaScore = gpaScoreRepository.findGpaScoreBySiteUserIdAndId(siteUser.getId(), gpaScoreId)
.orElseThrow(() -> new CustomException(GPA_SCORE_NOT_FOUND));
if (gpaScore.getVerifyStatus() != VerifyStatus.APPROVED) {
throw new CustomException(INVALID_GPA_SCORE_STATUS);
@@ -90,7 +92,7 @@ private GpaScore getValidGpaScore(SiteUser siteUser, Long gpaScoreId) {
private LanguageTestScore getValidLanguageTestScore(SiteUser siteUser, Long languageTestScoreId) {
LanguageTestScore languageTestScore = languageTestScoreRepository
- .findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)
+ .findLanguageTestScoreBySiteUserIdAndId(siteUser.getId(), languageTestScoreId)
.orElseThrow(() -> new CustomException(INVALID_LANGUAGE_TEST_SCORE));
if (languageTestScore.getVerifyStatus() != VerifyStatus.APPROVED) {
throw new CustomException(INVALID_LANGUAGE_TEST_SCORE_STATUS);
diff --git a/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java b/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java
index d9243ce39..c9699d4dc 100644
--- a/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java
+++ b/src/main/java/com/example/solidconnection/application/service/NicknameCreator.java
@@ -1,10 +1,9 @@
package com.example.solidconnection.application.service;
-import lombok.NoArgsConstructor;
-
import java.util.List;
import java.util.Random;
import java.util.Set;
+import lombok.NoArgsConstructor;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
class NicknameCreator {
diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java
index aef1309af..48009cc82 100644
--- a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java
+++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java
@@ -1,10 +1,18 @@
package com.example.solidconnection.auth.client;
+import static com.example.solidconnection.common.exception.ErrorCode.APPLE_AUTHORIZATION_FAILED;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_APPLE_ID_TOKEN;
+
+import com.example.solidconnection.auth.client.config.AppleOAuthClientProperties;
import com.example.solidconnection.auth.dto.oauth.AppleTokenDto;
import com.example.solidconnection.auth.dto.oauth.AppleUserInfoDto;
-import com.example.solidconnection.config.client.AppleOAuthClientProperties;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto;
+import com.example.solidconnection.auth.service.oauth.OAuthClient;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.siteuser.domain.AuthType;
import io.jsonwebtoken.Jwts;
+import java.security.PublicKey;
+import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@@ -16,32 +24,32 @@
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
-import java.security.PublicKey;
-import java.util.Objects;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_AUTHORIZATION_FAILED;
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN;
-
/*
* 애플 인증을 위한 OAuth2 클라이언트
* https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens
* */
@Component
@RequiredArgsConstructor
-public class AppleOAuthClient {
+public class AppleOAuthClient implements OAuthClient {
private final RestTemplate restTemplate;
private final AppleOAuthClientProperties properties;
private final AppleOAuthClientSecretProvider clientSecretProvider;
private final ApplePublicKeyProvider publicKeyProvider;
- public AppleUserInfoDto processOAuth(String code) {
+ @Override
+ public AuthType getAuthType() {
+ return AuthType.APPLE;
+ }
+
+ @Override
+ public OAuthUserInfoDto getUserInfo(String code) {
String idToken = requestIdToken(code);
PublicKey applePublicKey = publicKeyProvider.getApplePublicKey(idToken);
return new AppleUserInfoDto(parseEmailFromToken(applePublicKey, idToken));
}
- public String requestIdToken(String code) {
+ private String requestIdToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap formData = buildFormData(code);
diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java
index 31228e5d3..a5a5cd315 100644
--- a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java
+++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java
@@ -1,22 +1,21 @@
package com.example.solidconnection.auth.client;
-import com.example.solidconnection.config.client.AppleOAuthClientProperties;
-import com.example.solidconnection.custom.exception.CustomException;
+import static com.example.solidconnection.common.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY;
+
+import com.example.solidconnection.auth.client.config.AppleOAuthClientProperties;
+import com.example.solidconnection.common.exception.CustomException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.annotation.PostConstruct;
-import lombok.RequiredArgsConstructor;
-import org.apache.tomcat.util.codec.binary.Base64;
-import org.springframework.stereotype.Component;
-
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY;
+import lombok.RequiredArgsConstructor;
+import org.apache.tomcat.util.codec.binary.Base64;
+import org.springframework.stereotype.Component;
/*
* 애플 OAuth 에 필요한 클라이언트 시크릿은 매번 동적으로 생성해야 한다.
diff --git a/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java b/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java
index 1cc708cc7..0b0aaa7d0 100644
--- a/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java
+++ b/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java
@@ -1,16 +1,16 @@
package com.example.solidconnection.auth.client;
-import com.example.solidconnection.config.client.AppleOAuthClientProperties;
-import com.example.solidconnection.custom.exception.CustomException;
+import static com.example.solidconnection.common.exception.ErrorCode.APPLE_ID_TOKEN_EXPIRED;
+import static com.example.solidconnection.common.exception.ErrorCode.APPLE_PUBLIC_KEY_NOT_FOUND;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_APPLE_ID_TOKEN;
+import static org.apache.tomcat.util.codec.binary.Base64.decodeBase64URLSafe;
+
+import com.example.solidconnection.auth.client.config.AppleOAuthClientProperties;
+import com.example.solidconnection.common.exception.CustomException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
-import lombok.RequiredArgsConstructor;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Component;
-import org.springframework.web.client.RestTemplate;
-
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
@@ -19,19 +19,18 @@
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_ID_TOKEN_EXPIRED;
-import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_PUBLIC_KEY_NOT_FOUND;
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN;
-import static org.apache.tomcat.util.codec.binary.Base64.decodeBase64URLSafe;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
/*
-* idToken 검증을 위해서 애플의 공개키를 가져온다.
-* - 애플 공개키는 주기적으로 바뀐다. 이를 효율적으로 관리하기 위해 캐싱한다.
-* - idToken 의 헤더에 있는 kid 값에 해당하는 키가 캐싱되어있으면 그것을 반환한다.
-* - 그렇지 않다면 공개키가 바뀌었다는 뜻이므로, JSON 형식의 공개키 목록을 받아오고 캐시를 갱신한다.
-* https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature
-* */
+ * idToken 검증을 위해서 애플의 공개키를 가져온다.
+ * - 애플 공개키는 주기적으로 바뀐다. 이를 효율적으로 관리하기 위해 캐싱한다.
+ * - idToken 의 헤더에 있는 kid 값에 해당하는 키가 캐싱되어있으면 그것을 반환한다.
+ * - 그렇지 않다면 공개키가 바뀌었다는 뜻이므로, JSON 형식의 공개키 목록을 받아오고 캐시를 갱신한다.
+ * https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature
+ * */
@Component
@RequiredArgsConstructor
public class ApplePublicKeyProvider {
@@ -62,9 +61,9 @@ public PublicKey getApplePublicKey(String idToken) {
}
/*
- * idToken 은 JWS 이므로, 원칙적으로는 서명까지 검증되어야 parsing 이 가능하다
- * 하지만 이 시점에서는 서명(=공개키)을 알 수 없으므로, Jwt 를 직접 인코딩하여 헤더를 가져온다.
- * */
+ * idToken 은 JWS 이므로, 원칙적으로는 서명까지 검증되어야 parsing 이 가능하다
+ * 하지만 이 시점에서는 서명(=공개키)을 알 수 없으므로, Jwt 를 직접 인코딩하여 헤더를 가져온다.
+ * */
private String getKeyIdFromTokenHeader(String idToken) throws JsonProcessingException {
String[] jwtParts = idToken.split("\\.");
if (jwtParts.length < 2) {
diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java
index 5d625cb7c..a25743f7d 100644
--- a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java
+++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java
@@ -1,9 +1,17 @@
package com.example.solidconnection.auth.client;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE;
+import static com.example.solidconnection.common.exception.ErrorCode.KAKAO_REDIRECT_URI_MISMATCH;
+import static com.example.solidconnection.common.exception.ErrorCode.KAKAO_USER_INFO_FAIL;
+
+import com.example.solidconnection.auth.client.config.KakaoOAuthClientProperties;
import com.example.solidconnection.auth.dto.oauth.KakaoTokenDto;
import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto;
-import com.example.solidconnection.config.client.KakaoOAuthClientProperties;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto;
+import com.example.solidconnection.auth.service.oauth.OAuthClient;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.siteuser.domain.AuthType;
+import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@@ -14,12 +22,6 @@
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
-import java.util.Objects;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_OR_EXPIRED_KAKAO_AUTH_CODE;
-import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_REDIRECT_URI_MISMATCH;
-import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_USER_INFO_FAIL;
-
/*
* 카카오 인증을 위한 OAuth2 클라이언트
* https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code
@@ -28,12 +30,18 @@
* */
@Component
@RequiredArgsConstructor
-public class KakaoOAuthClient {
+public class KakaoOAuthClient implements OAuthClient {
private final RestTemplate restTemplate;
private final KakaoOAuthClientProperties kakaoOAuthClientProperties;
- public KakaoUserInfoDto getUserInfo(String code) {
+ @Override
+ public AuthType getAuthType() {
+ return AuthType.KAKAO;
+ }
+
+ @Override
+ public OAuthUserInfoDto getUserInfo(String code) {
String kakaoAccessToken = getKakaoAccessToken(code);
return getKakaoUserInfo(kakaoAccessToken);
}
diff --git a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java b/src/main/java/com/example/solidconnection/auth/client/config/AppleOAuthClientProperties.java
similarity index 87%
rename from src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java
rename to src/main/java/com/example/solidconnection/auth/client/config/AppleOAuthClientProperties.java
index c04908583..3ff927a03 100644
--- a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java
+++ b/src/main/java/com/example/solidconnection/auth/client/config/AppleOAuthClientProperties.java
@@ -1,4 +1,4 @@
-package com.example.solidconnection.config.client;
+package com.example.solidconnection.auth.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -13,4 +13,5 @@ public record AppleOAuthClientProperties(
String keyId,
String secretKey
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java b/src/main/java/com/example/solidconnection/auth/client/config/KakaoOAuthClientProperties.java
similarity index 83%
rename from src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java
rename to src/main/java/com/example/solidconnection/auth/client/config/KakaoOAuthClientProperties.java
index 73b196d76..36dcdaec1 100644
--- a/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java
+++ b/src/main/java/com/example/solidconnection/auth/client/config/KakaoOAuthClientProperties.java
@@ -1,4 +1,4 @@
-package com.example.solidconnection.config.client;
+package com.example.solidconnection.auth.client.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -9,4 +9,5 @@ public record KakaoOAuthClientProperties(
String redirectUrl,
String clientId
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java
index 9c84e8d22..f5a30bb2f 100644
--- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java
+++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java
@@ -8,24 +8,23 @@
import com.example.solidconnection.auth.dto.SignUpRequest;
import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest;
import com.example.solidconnection.auth.dto.oauth.OAuthResponse;
+import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse;
import com.example.solidconnection.auth.service.AuthService;
-import com.example.solidconnection.auth.service.CommonSignUpTokenProvider;
import com.example.solidconnection.auth.service.EmailSignInService;
-import com.example.solidconnection.auth.service.EmailSignUpService;
import com.example.solidconnection.auth.service.EmailSignUpTokenProvider;
-import com.example.solidconnection.auth.service.oauth.AppleOAuthService;
-import com.example.solidconnection.auth.service.oauth.KakaoOAuthService;
-import com.example.solidconnection.auth.service.oauth.OAuthSignUpService;
-import com.example.solidconnection.custom.exception.CustomException;
-import com.example.solidconnection.custom.exception.ErrorCode;
-import com.example.solidconnection.custom.resolver.AuthorizedUser;
+import com.example.solidconnection.auth.service.SignUpService;
+import com.example.solidconnection.auth.service.oauth.OAuthService;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.common.exception.ErrorCode;
+import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.siteuser.domain.AuthType;
-import com.example.solidconnection.siteuser.domain.SiteUser;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
-import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -37,35 +36,43 @@
public class AuthController {
private final AuthService authService;
- private final OAuthSignUpService oAuthSignUpService;
- private final AppleOAuthService appleOAuthService;
- private final KakaoOAuthService kakaoOAuthService;
+ private final OAuthService oAuthService;
+ private final SignUpService signUpService;
private final EmailSignInService emailSignInService;
- private final EmailSignUpService emailSignUpService;
private final EmailSignUpTokenProvider emailSignUpTokenProvider;
- private final CommonSignUpTokenProvider commonSignUpTokenProvider;
+ private final RefreshTokenCookieManager refreshTokenCookieManager;
@PostMapping("/apple")
public ResponseEntity processAppleOAuth(
- @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest
+ @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest,
+ HttpServletResponse httpServletResponse
) {
- OAuthResponse oAuthResponse = appleOAuthService.processOAuth(oAuthCodeRequest);
+ OAuthResponse oAuthResponse = oAuthService.processOAuth(AuthType.APPLE, oAuthCodeRequest);
+ if (oAuthResponse instanceof OAuthSignInResponse signInResponse) {
+ refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken());
+ }
return ResponseEntity.ok(oAuthResponse);
}
@PostMapping("/kakao")
public ResponseEntity processKakaoOAuth(
- @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest
+ @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest,
+ HttpServletResponse httpServletResponse
) {
- OAuthResponse oAuthResponse = kakaoOAuthService.processOAuth(oAuthCodeRequest);
+ OAuthResponse oAuthResponse = oAuthService.processOAuth(AuthType.KAKAO, oAuthCodeRequest);
+ if (oAuthResponse instanceof OAuthSignInResponse signInResponse) {
+ refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken());
+ }
return ResponseEntity.ok(oAuthResponse);
}
@PostMapping("/email/sign-in")
public ResponseEntity signInWithEmail(
- @Valid @RequestBody EmailSignInRequest signInRequest
+ @Valid @RequestBody EmailSignInRequest signInRequest,
+ HttpServletResponse httpServletResponse
) {
SignInResponse signInResponse = emailSignInService.signIn(signInRequest);
+ refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken());
return ResponseEntity.ok(signInResponse);
}
@@ -74,8 +81,7 @@ public ResponseEntity signInWithEmail(
public ResponseEntity signUpWithEmail(
@Valid @RequestBody EmailSignUpTokenRequest signUpRequest
) {
- emailSignUpService.validateUniqueEmail(signUpRequest.email());
- String signUpToken = emailSignUpTokenProvider.generateAndSaveSignUpToken(signUpRequest);
+ String signUpToken = emailSignUpTokenProvider.issueEmailSignUpToken(signUpRequest);
return ResponseEntity.ok(new EmailSignUpTokenResponse(signUpToken));
}
@@ -83,44 +89,46 @@ public ResponseEntity signUpWithEmail(
public ResponseEntity signUp(
@Valid @RequestBody SignUpRequest signUpRequest
) {
- AuthType authType = commonSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken());
- if (AuthType.isEmail(authType)) {
- SignInResponse signInResponse = emailSignUpService.signUp(signUpRequest);
- return ResponseEntity.ok(signInResponse);
- }
- SignInResponse signInResponse = oAuthSignUpService.signUp(signUpRequest);
+ SignInResponse signInResponse = signUpService.signUp(signUpRequest);
return ResponseEntity.ok(signInResponse);
}
@PostMapping("/sign-out")
public ResponseEntity signOut(
- Authentication authentication
+ Authentication authentication,
+ HttpServletResponse httpServletResponse
) {
- String token = authentication.getCredentials().toString();
- if (token == null) {
- throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다.");
- }
- authService.signOut(token);
+ String accessToken = getAccessToken(authentication);
+ authService.signOut(accessToken);
+ refreshTokenCookieManager.deleteCookie(httpServletResponse);
return ResponseEntity.ok().build();
}
- @PatchMapping("/quit")
+ @DeleteMapping("/quit")
public ResponseEntity quit(
- @AuthorizedUser SiteUser siteUser
+ @AuthorizedUser long siteUserId,
+ Authentication authentication,
+ HttpServletResponse httpServletResponse
) {
- authService.quit(siteUser);
+ String accessToken = getAccessToken(authentication);
+ authService.quit(siteUserId, accessToken);
+ refreshTokenCookieManager.deleteCookie(httpServletResponse);
return ResponseEntity.ok().build();
}
@PostMapping("/reissue")
public ResponseEntity reissueToken(
- Authentication authentication
+ HttpServletRequest request
) {
- String token = authentication.getCredentials().toString();
- if (token == null) {
- throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "토큰이 없습니다.");
- }
- ReissueResponse reissueResponse = authService.reissue(token);
+ String refreshToken = refreshTokenCookieManager.getRefreshToken(request);
+ ReissueResponse reissueResponse = authService.reissue(refreshToken);
return ResponseEntity.ok(reissueResponse);
}
+
+ private String getAccessToken(Authentication authentication) {
+ if (authentication == null || !(authentication.getCredentials() instanceof String accessToken)) {
+ throw new CustomException(ErrorCode.AUTHENTICATION_FAILED, "엑세스 토큰이 없습니다.");
+ }
+ return accessToken;
+ }
}
diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java
new file mode 100644
index 000000000..7c6f4ec04
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java
@@ -0,0 +1,75 @@
+package com.example.solidconnection.auth.controller;
+
+import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_NOT_EXISTS;
+
+import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties;
+import com.example.solidconnection.auth.domain.TokenType;
+import com.example.solidconnection.common.exception.CustomException;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Arrays;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.web.server.Cookie.SameSite;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseCookie;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class RefreshTokenCookieManager {
+
+ private static final String COOKIE_NAME = "refreshToken";
+ private static final String PATH = "/";
+
+ private final RefreshTokenCookieProperties properties;
+
+ public void setCookie(HttpServletResponse response, String refreshToken) {
+ long maxAge = convertExpireTimeToCookieMaxAge(TokenType.REFRESH.getExpireTime());
+ setRefreshTokenCookie(response, refreshToken, maxAge);
+ }
+
+ private long convertExpireTimeToCookieMaxAge(long milliSeconds) {
+ // jwt의 expireTime 단위인 millisecond를 cookie의 maxAge 단위인 second로 변환
+ return milliSeconds / 1000;
+ }
+
+ public void deleteCookie(HttpServletResponse response) {
+ setRefreshTokenCookie(response, "", 0); // 쿠키 삭제를 위해 maxAge를 0으로 설정
+ }
+
+ private void setRefreshTokenCookie(
+ HttpServletResponse response, String refreshToken, long maxAge
+ ) {
+ ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken)
+ .httpOnly(true)
+ .secure(true)
+ .path(PATH)
+ .maxAge(maxAge)
+ .domain(properties.cookieDomain())
+ .sameSite(SameSite.LAX.attributeValue())
+ .build();
+ response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
+ }
+
+ public String getRefreshToken(HttpServletRequest request) {
+ // 쿠키가 없거나 비어있는 경우 예외 발생
+ Cookie[] cookies = request.getCookies();
+ if (cookies == null || cookies.length == 0) {
+ throw new CustomException(REFRESH_TOKEN_NOT_EXISTS);
+ }
+
+ // refreshToken 쿠키가 없는 경우 예외 발생
+ Cookie refreshTokenCookie = Arrays.stream(cookies)
+ .filter(cookie -> COOKIE_NAME.equals(cookie.getName()))
+ .findFirst()
+ .orElseThrow(() -> new CustomException(REFRESH_TOKEN_NOT_EXISTS));
+
+ // 쿠키 값이 비어있는 경우 예외 발생
+ String refreshToken = refreshTokenCookie.getValue();
+ if (refreshToken == null || refreshToken.isBlank()) {
+ throw new CustomException(REFRESH_TOKEN_NOT_EXISTS);
+ }
+ return refreshToken;
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/controller/config/RefreshTokenCookieProperties.java b/src/main/java/com/example/solidconnection/auth/controller/config/RefreshTokenCookieProperties.java
new file mode 100644
index 000000000..ce6588f14
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/controller/config/RefreshTokenCookieProperties.java
@@ -0,0 +1,10 @@
+package com.example.solidconnection.auth.controller.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "token.refresh")
+public record RefreshTokenCookieProperties(
+ String cookieDomain
+) {
+
+}
diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java
index caf1c7a9d..560b0e139 100644
--- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java
+++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java
@@ -5,16 +5,16 @@
@Getter
public enum TokenType {
- ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour
- REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days
+ ACCESS("ACCESS:", 1000L * 60 * 60), // 1hour
+ REFRESH("REFRESH:", 1000L * 60 * 60 * 24 * 90), // 90days
BLACKLIST("BLACKLIST:", ACCESS.expireTime),
- SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min
+ SIGN_UP("SIGN_UP:", 1000L * 60 * 10), // 10min
;
private final String prefix;
- private final int expireTime;
+ private final long expireTime;
- TokenType(String prefix, int expireTime) {
+ TokenType(String prefix, long expireTime) {
this.prefix = prefix;
this.expireTime = expireTime;
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java
index 306a8185a..3987bc508 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java
@@ -1,13 +1,18 @@
package com.example.solidconnection.auth.dto;
+import com.example.solidconnection.auth.dto.validation.Password;
+import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record EmailSignInRequest(
@NotBlank(message = "이메일을 입력해주세요.")
+ @Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,
+ @Password
@NotBlank(message = "비밀번호를 입력해주세요.")
String password
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java
index 92073b434..ffafa222d 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java
@@ -1,14 +1,18 @@
package com.example.solidconnection.auth.dto;
+import com.example.solidconnection.auth.dto.validation.Password;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record EmailSignUpTokenRequest(
- @Email(message = "이메일을 입력해주세요.")
+ @NotBlank(message = "이메일을 입력해주세요.")
+ @Email(message = "유효한 이메일 주소를 입력해주세요.")
String email,
+ @Password
@NotBlank(message = "비밀번호를 입력해주세요.")
String password
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java
index c8e983d0c..d056da89b 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java
@@ -3,4 +3,5 @@
public record EmailSignUpTokenResponse(
String signUpToken
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java
index 48b55e6cb..972470cca 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/ReissueResponse.java
@@ -1,5 +1,12 @@
package com.example.solidconnection.auth.dto;
+import com.example.solidconnection.auth.service.AccessToken;
+
public record ReissueResponse(
- String accessToken) {
+ String accessToken
+) {
+
+ public static ReissueResponse from(AccessToken accessToken) {
+ return new ReissueResponse(accessToken.token());
+ }
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java
index a4ae442e2..b01fdd369 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java
@@ -1,7 +1,14 @@
package com.example.solidconnection.auth.dto;
+import com.example.solidconnection.auth.service.AccessToken;
+import com.example.solidconnection.auth.service.RefreshToken;
+
public record SignInResponse(
String accessToken,
String refreshToken
) {
+
+ public static SignInResponse of(AccessToken accessToken, RefreshToken refreshToken) {
+ return new SignInResponse(accessToken.token(), refreshToken.token());
+ }
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java
index 9bf92a295..bafb9b4c8 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java
@@ -1,18 +1,21 @@
package com.example.solidconnection.auth.dto;
import com.example.solidconnection.siteuser.domain.AuthType;
+import com.example.solidconnection.siteuser.domain.ExchangeStatus;
+import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.type.PreparationStatus;
-import com.example.solidconnection.type.Role;
+import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
-
import java.util.List;
public record SignUpRequest(
String signUpToken,
List interestedRegions,
List interestedCountries,
- PreparationStatus preparationStatus,
+
+ @JsonProperty("preparationStatus")
+ ExchangeStatus exchangeStatus,
+
String profileImageUrl,
@NotBlank(message = "닉네임을 입력해주세요.")
@@ -23,7 +26,7 @@ public SiteUser toOAuthSiteUser(String email, AuthType authType) {
email,
this.nickname,
this.profileImageUrl,
- this.preparationStatus,
+ this.exchangeStatus,
Role.MENTEE,
authType
);
@@ -34,7 +37,7 @@ public SiteUser toEmailSiteUser(String email, String encodedPassword) {
email,
this.nickname,
this.profileImageUrl,
- this.preparationStatus,
+ this.exchangeStatus,
Role.MENTEE,
AuthType.EMAIL,
encodedPassword
diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java
index 6772cb2c2..019066c81 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java
@@ -7,4 +7,5 @@
public record AppleTokenDto(
@JsonProperty("id_token") String idToken
) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java
index 5c4363e51..5e95c2a5d 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java
@@ -1,10 +1,10 @@
package com.example.solidconnection.auth.dto.oauth;
/*
-* 애플로부터 사용자의 정보를 받아올 때 사용한다.
-* 카카오와 달리 애플은 더 엄격하게 사용자 정보를 관리하여, 이름이나 프로필 이미지 url 을 제공하지 않는다.
-* 따라서 닉네임, 프로필 정보는 null 을 반환한다.
-* */
+ * 애플로부터 사용자의 정보를 받아올 때 사용한다.
+ * 카카오와 달리 애플은 더 엄격하게 사용자 정보를 관리하여, 이름이나 프로필 이미지 url 을 제공하지 않는다.
+ * 따라서 닉네임, 프로필 정보는 null 을 반환한다.
+ * */
public record AppleUserInfoDto(String email) implements OAuthUserInfoDto {
@Override
diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java
index 6d4ccd10c..0663573b1 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java
@@ -7,4 +7,5 @@
public record KakaoTokenDto(
@JsonProperty("access_token") String accessToken,
@JsonProperty("refresh_token") String refreshToken) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java
index abbdb7802..5ecdf7cca 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java
@@ -6,4 +6,5 @@ public record OAuthCodeRequest(
@NotBlank(message = "인증 코드를 입력해주세요.")
String code) {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java
index ddbe121f7..5fbb0fd0d 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java
@@ -1,4 +1,5 @@
package com.example.solidconnection.auth.dto.oauth;
public interface OAuthResponse {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java
index 8ad429876..6ac121c46 100644
--- a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java
+++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java
@@ -4,4 +4,5 @@ public record OAuthSignInResponse(
boolean isRegistered,
String accessToken,
String refreshToken) implements OAuthResponse {
+
}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/validation/Password.java b/src/main/java/com/example/solidconnection/auth/dto/validation/Password.java
new file mode 100644
index 000000000..a896b5724
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/dto/validation/Password.java
@@ -0,0 +1,20 @@
+package com.example.solidconnection.auth.dto.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD, ElementType.RECORD_COMPONENT})
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = PasswordValidator.class)
+public @interface Password {
+
+ String message() default "비밀번호는 영문, 숫자, 특수문자를 포함한 8자리 이상이어야 합니다.";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+}
diff --git a/src/main/java/com/example/solidconnection/auth/dto/validation/PasswordValidator.java b/src/main/java/com/example/solidconnection/auth/dto/validation/PasswordValidator.java
new file mode 100644
index 000000000..77f8da213
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/dto/validation/PasswordValidator.java
@@ -0,0 +1,16 @@
+package com.example.solidconnection.auth.dto.validation;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+public class PasswordValidator implements ConstraintValidator {
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ if (value == null || value.isBlank()) {
+ return true;
+ }
+
+ return value.matches("^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-={}\\[\\]|:;\"'<>,.?/`~])\\S{8,}$");
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/AccessToken.java b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java
new file mode 100644
index 000000000..3456a2171
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/service/AccessToken.java
@@ -0,0 +1,14 @@
+package com.example.solidconnection.auth.service;
+
+import com.example.solidconnection.siteuser.domain.Role;
+
+public record AccessToken(
+ Subject subject,
+ Role role,
+ String token
+) {
+
+ public AccessToken(String subject, Role role, String token) {
+ this(new Subject(subject), role, token);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java
index 04bcadde7..01c162002 100644
--- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java
+++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java
@@ -1,56 +1,66 @@
package com.example.solidconnection.auth.service;
+import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
+import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
import com.example.solidconnection.auth.dto.ReissueResponse;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.auth.token.TokenBlackListService;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.SiteUser;
+import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.time.LocalDate;
-import java.util.Optional;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED;
-
@RequiredArgsConstructor
@Service
public class AuthService {
private final AuthTokenProvider authTokenProvider;
+ private final TokenBlackListService tokenBlackListService;
+ private final SiteUserRepository siteUserRepository;
/*
- * 로그아웃 한다.
+ * 로그아웃한다.
* - 엑세스 토큰을 블랙리스트에 추가한다.
+ * - 리프레시 토큰을 삭제한다.
* */
- public void signOut(String accessToken) {
- authTokenProvider.generateAndSaveBlackListToken(accessToken);
+ public void signOut(String token) {
+ SiteUser siteUser = authTokenProvider.parseSiteUser(token);
+ AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
+ authTokenProvider.deleteRefreshTokenByAccessToken(accessToken);
+ tokenBlackListService.addToBlacklist(accessToken);
}
/*
* 탈퇴한다.
* - 탈퇴한 시점의 다음날을 탈퇴일로 잡는다.
* - e.g. 2024-01-01 18:00 탈퇴 시, 2024-01-02 00:00 가 탈퇴일이 된다.
+ * - 로그아웃한다.
* */
@Transactional
- public void quit(SiteUser siteUser) {
+ public void quit(long siteUserId, String token) {
+ SiteUser siteUser = siteUserRepository.findById(siteUserId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
LocalDate tomorrow = LocalDate.now().plusDays(1);
siteUser.setQuitedAt(tomorrow);
+ signOut(token);
}
/*
* 액세스 토큰을 재발급한다.
- * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다.
- * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다.
+ * - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다.
+ * - 그렇지 않으면 예외를 발생시킨다.
* */
- public ReissueResponse reissue(String subject) {
- // 리프레시 토큰 만료 확인
- Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject);
- if (optionalRefreshToken.isEmpty()) {
+ public ReissueResponse reissue(String requestedRefreshToken) {
+ // 리프레시 토큰 확인
+ if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) {
throw new CustomException(REFRESH_TOKEN_EXPIRED);
}
// 액세스 토큰 재발급
- String newAccessToken = authTokenProvider.generateAccessToken(subject);
- return new ReissueResponse(newAccessToken);
+ SiteUser siteUser = authTokenProvider.parseSiteUser(requestedRefreshToken);
+ AccessToken newAccessToken = authTokenProvider.generateAccessToken(siteUser);
+ return ReissueResponse.from(newAccessToken);
}
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java
index da040a8d5..8e55f77d4 100644
--- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java
+++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java
@@ -1,53 +1,72 @@
package com.example.solidconnection.auth.service;
+import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
+
import com.example.solidconnection.auth.domain.TokenType;
-import com.example.solidconnection.config.security.JwtProperties;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
+import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import java.util.Map;
+import java.util.Objects;
+import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
-import java.util.Optional;
-
-import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration;
-
@Component
-public class AuthTokenProvider extends TokenProvider {
+@RequiredArgsConstructor
+public class AuthTokenProvider {
- public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) {
- super(jwtProperties, redisTemplate);
- }
+ private static final String ROLE_CLAIM_KEY = "role";
- public String generateAccessToken(SiteUser siteUser) {
- String subject = siteUser.getId().toString();
- return generateToken(subject, TokenType.ACCESS);
- }
+ private final RedisTemplate redisTemplate;
+ private final TokenProvider tokenProvider;
+ private final SiteUserRepository siteUserRepository;
- public String generateAccessToken(String subject) {
- return generateToken(subject, TokenType.ACCESS);
+ public AccessToken generateAccessToken(SiteUser siteUser) {
+ Subject subject = toSubject(siteUser);
+ Role role = siteUser.getRole();
+ String token = tokenProvider.generateToken(
+ subject.value(),
+ Map.of(ROLE_CLAIM_KEY, role.name()),
+ TokenType.ACCESS
+ );
+ return new AccessToken(subject, role, token);
}
- public String generateAndSaveRefreshToken(SiteUser siteUser) {
- String subject = siteUser.getId().toString();
- String refreshToken = generateToken(subject, TokenType.REFRESH);
- return saveToken(refreshToken, TokenType.REFRESH);
+ public RefreshToken generateAndSaveRefreshToken(SiteUser siteUser) {
+ Subject subject = toSubject(siteUser);
+ String token = tokenProvider.generateToken(subject.value(), TokenType.REFRESH);
+ tokenProvider.saveToken(token, TokenType.REFRESH);
+ return new RefreshToken(subject, token);
}
- public String generateAndSaveBlackListToken(String accessToken) {
- String blackListToken = generateToken(accessToken, TokenType.BLACKLIST);
- return saveToken(blackListToken, TokenType.BLACKLIST);
+ /*
+ * 유효한 리프레시 토큰인지 확인한다.
+ * - 요청된 토큰과 같은 subject 의 리프레시 토큰을 조회한다.
+ * - 조회된 리프레시 토큰과 요청된 토큰이 같은지 비교한다.
+ * */
+ public boolean isValidRefreshToken(String requestedRefreshToken) {
+ String subject = tokenProvider.parseSubject(requestedRefreshToken);
+ String refreshTokenKey = TokenType.REFRESH.addPrefix(subject);
+ String foundRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey);
+ return Objects.equals(requestedRefreshToken, foundRefreshToken);
}
- public Optional findRefreshToken(String subject) {
+ public void deleteRefreshTokenByAccessToken(AccessToken accessToken) {
+ String subject = accessToken.subject().value();
String refreshTokenKey = TokenType.REFRESH.addPrefix(subject);
- return Optional.ofNullable(redisTemplate.opsForValue().get(refreshTokenKey));
+ redisTemplate.delete(refreshTokenKey);
}
- public Optional findBlackListToken(String subject) {
- String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject);
- return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey));
+ public SiteUser parseSiteUser(String token) {
+ String subject = tokenProvider.parseSubject(token);
+ long siteUserId = Long.parseLong(subject);
+ return siteUserRepository.findById(siteUserId)
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
}
- public String getEmail(String token) {
- return parseSubjectIgnoringExpiration(token, jwtProperties.secret());
+ public Subject toSubject(SiteUser siteUser) {
+ return new Subject(siteUser.getId().toString());
}
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java
deleted file mode 100644
index 3d0eda53b..000000000
--- a/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.example.solidconnection.auth.service;
-
-import com.example.solidconnection.config.security.JwtProperties;
-import com.example.solidconnection.custom.exception.CustomException;
-import com.example.solidconnection.siteuser.domain.AuthType;
-import com.example.solidconnection.util.JwtUtils;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import static com.example.solidconnection.auth.service.EmailSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY;
-import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID;
-
-@Component
-@RequiredArgsConstructor
-public class CommonSignUpTokenProvider {
-
- private final JwtProperties jwtProperties;
-
- public AuthType parseAuthType(String signUpToken) {
- try {
- String authTypeStr = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()).get(AUTH_TYPE_CLAIM_KEY, String.class);
- return AuthType.valueOf(authTypeStr);
- } catch (Exception e) {
- throw new CustomException(SIGN_UP_TOKEN_INVALID);
- }
- }
-}
diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java
index 3e26309a5..4dac56586 100644
--- a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java
+++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java
@@ -1,22 +1,18 @@
package com.example.solidconnection.auth.service;
+import static com.example.solidconnection.common.exception.ErrorCode.SIGN_IN_FAILED;
+
import com.example.solidconnection.auth.dto.EmailSignInRequest;
import com.example.solidconnection.auth.dto.SignInResponse;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.AuthType;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
-import java.util.Optional;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND;
-
-/*
- * 보안을 위해 이메일과 비밀번호 중 무엇이 틀렸는지 구체적으로 응답하지 않는다.
- * */
@Service
@RequiredArgsConstructor
public class EmailSignInService {
@@ -25,19 +21,21 @@ public class EmailSignInService {
private final SiteUserRepository siteUserRepository;
private final PasswordEncoder passwordEncoder;
+ @Transactional(readOnly = true)
public SignInResponse signIn(EmailSignInRequest signInRequest) {
- Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(signInRequest.email(), AuthType.EMAIL);
- if (optionalSiteUser.isPresent()) {
- SiteUser siteUser = optionalSiteUser.get();
- validatePassword(signInRequest.password(), siteUser.getPassword());
- return signInService.signIn(siteUser);
- }
- throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요.");
+ SiteUser siteUser = getEmailMatchingUserOrThrow(signInRequest.email());
+ validatePassword(signInRequest.password(), siteUser.getPassword());
+ return signInService.signIn(siteUser);
+ }
+
+ private SiteUser getEmailMatchingUserOrThrow(String email) {
+ return siteUserRepository.findByEmailAndAuthType(email, AuthType.EMAIL)
+ .orElseThrow(() -> new CustomException(SIGN_IN_FAILED));
}
private void validatePassword(String rawPassword, String encodedPassword) {
if (!passwordEncoder.matches(rawPassword, encodedPassword)) {
- throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요.");
+ throw new CustomException(SIGN_IN_FAILED);
}
}
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java
deleted file mode 100644
index 37f6681ea..000000000
--- a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.example.solidconnection.auth.service;
-
-import com.example.solidconnection.auth.dto.SignUpRequest;
-import com.example.solidconnection.custom.exception.CustomException;
-import com.example.solidconnection.repositories.CountryRepository;
-import com.example.solidconnection.repositories.InterestedCountyRepository;
-import com.example.solidconnection.repositories.InterestedRegionRepository;
-import com.example.solidconnection.repositories.RegionRepository;
-import com.example.solidconnection.siteuser.domain.AuthType;
-import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.siteuser.repository.SiteUserRepository;
-import org.springframework.stereotype.Service;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED;
-
-@Service
-public class EmailSignUpService extends SignUpService {
-
- private final EmailSignUpTokenProvider emailSignUpTokenProvider;
-
- public EmailSignUpService(SignInService signInService, SiteUserRepository siteUserRepository,
- RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository,
- CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository,
- EmailSignUpTokenProvider emailSignUpTokenProvider) {
- super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountyRepository);
- this.emailSignUpTokenProvider = emailSignUpTokenProvider;
- }
-
- public void validateUniqueEmail(String email) {
- if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) {
- throw new CustomException(USER_ALREADY_EXISTED);
- }
- }
-
- @Override
- protected void validateSignUpToken(SignUpRequest signUpRequest) {
- emailSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken());
- }
-
- @Override
- protected void validateUserNotDuplicated(SignUpRequest signUpRequest) {
- String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken());
- if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) {
- throw new CustomException(USER_ALREADY_EXISTED);
- }
- }
-
- @Override
- protected SiteUser createSiteUser(SignUpRequest signUpRequest) {
- String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken());
- String encodedPassword = emailSignUpTokenProvider.parseEncodedPassword(signUpRequest.signUpToken());
- return signUpRequest.toEmailSiteUser(email, encodedPassword);
- }
-}
diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java
index 1c27a87bd..a3e2e5dc9 100644
--- a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java
+++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java
@@ -1,92 +1,32 @@
package com.example.solidconnection.auth.service;
-import com.example.solidconnection.auth.domain.TokenType;
import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest;
-import com.example.solidconnection.config.security.JwtProperties;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.common.exception.ErrorCode;
import com.example.solidconnection.siteuser.domain.AuthType;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.security.crypto.password.PasswordEncoder;
+import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
-
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID;
-import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER;
-import static com.example.solidconnection.util.JwtUtils.parseClaims;
-import static com.example.solidconnection.util.JwtUtils.parseSubject;
+import org.springframework.transaction.annotation.Transactional;
@Component
-public class EmailSignUpTokenProvider extends TokenProvider {
-
- static final String PASSWORD_CLAIM_KEY = "password";
- static final String AUTH_TYPE_CLAIM_KEY = "authType";
-
- private final PasswordEncoder passwordEncoder;
+@RequiredArgsConstructor
+public class EmailSignUpTokenProvider {
- public EmailSignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate,
- PasswordEncoder passwordEncoder) {
- super(jwtProperties, redisTemplate);
- this.passwordEncoder = passwordEncoder;
- }
+ private final SignUpTokenProvider signUpTokenProvider;
+ private final SiteUserRepository siteUserRepository;
+ private final PasswordTemporaryStorage passwordTemporaryStorage;
- public String generateAndSaveSignUpToken(EmailSignUpTokenRequest request) {
+ @Transactional(readOnly = true)
+ public String issueEmailSignUpToken(EmailSignUpTokenRequest request) {
String email = request.email();
String password = request.password();
- String encodedPassword = passwordEncoder.encode(password);
- Map emailSignUpClaims = new HashMap<>(Map.of(
- PASSWORD_CLAIM_KEY, encodedPassword,
- AUTH_TYPE_CLAIM_KEY, AuthType.EMAIL
- ));
- Claims claims = Jwts.claims(emailSignUpClaims).setSubject(email);
- Date now = new Date();
- Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime());
-
- String signUpToken = Jwts.builder()
- .setClaims(claims)
- .setIssuedAt(now)
- .setExpiration(expiredDate)
- .signWith(SignatureAlgorithm.HS512, jwtProperties.secret())
- .compact();
- return saveToken(signUpToken, TokenType.SIGN_UP);
- }
- public void validateSignUpToken(String token) {
- validateFormatAndExpiration(token);
- String email = parseEmail(token);
- validateIssuedByServer(email);
- }
-
- private void validateFormatAndExpiration(String token) {
- try {
- Claims claims = parseClaims(token, jwtProperties.secret());
- Objects.requireNonNull(claims.getSubject());
- String encodedPassword = claims.get(PASSWORD_CLAIM_KEY, String.class);
- Objects.requireNonNull(encodedPassword);
- } catch (Exception e) {
- throw new CustomException(SIGN_UP_TOKEN_INVALID);
- }
- }
-
- private void validateIssuedByServer(String email) {
- String key = TokenType.SIGN_UP.addPrefix(email);
- if (redisTemplate.opsForValue().get(key) == null) {
- throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER);
+ if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) {
+ throw new CustomException(ErrorCode.USER_ALREADY_EXISTED);
}
- }
-
- public String parseEmail(String token) {
- return parseSubject(token, jwtProperties.secret());
- }
- public String parseEncodedPassword(String token) {
- Claims claims = parseClaims(token, jwtProperties.secret());
- return claims.get(PASSWORD_CLAIM_KEY, String.class);
+ passwordTemporaryStorage.save(email, password);
+ return signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.EMAIL);
}
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java b/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java
new file mode 100644
index 000000000..adcb8bf68
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/service/PasswordTemporaryStorage.java
@@ -0,0 +1,46 @@
+package com.example.solidconnection.auth.service;
+
+import com.example.solidconnection.auth.domain.TokenType;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class PasswordTemporaryStorage {
+
+ private static final String KEY_PREFIX = "password:";
+
+ private final RedisTemplate redisTemplate;
+ private final PasswordEncoder passwordEncoder;
+
+ public void save(String email, String rawPassword) {
+ String encodedPassword = passwordEncoder.encode(rawPassword);
+ redisTemplate.opsForValue().set(
+ convertToKey(email),
+ encodedPassword,
+ TokenType.SIGN_UP.getExpireTime(),
+ TimeUnit.MILLISECONDS
+ );
+ }
+
+ public Optional findByEmail(String email) {
+ String encodedPassword = redisTemplate.opsForValue().get(convertToKey(email));
+ if (encodedPassword == null) {
+ return Optional.empty();
+ }
+ return Optional.of(encodedPassword);
+ }
+
+ public void deleteByEmail(String email) {
+ String key = convertToKey(email);
+ redisTemplate.delete(key);
+ }
+
+ private String convertToKey(String email) {
+ return KEY_PREFIX + email;
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java b/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java
new file mode 100644
index 000000000..2aac3ad8c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/service/RefreshToken.java
@@ -0,0 +1,11 @@
+package com.example.solidconnection.auth.service;
+
+public record RefreshToken(
+ Subject subject,
+ String token
+) {
+
+ RefreshToken(String subject, String token) {
+ this(new Subject(subject), token);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java
index 820d2e573..16ec4c484 100644
--- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java
+++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java
@@ -15,9 +15,9 @@ public class SignInService {
@Transactional
public SignInResponse signIn(SiteUser siteUser) {
resetQuitedAt(siteUser);
- String accessToken = authTokenProvider.generateAccessToken(siteUser);
- String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);
- return new SignInResponse(accessToken, refreshToken);
+ AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser);
+ RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser);
+ return SignInResponse.of(accessToken, refreshToken);
}
private void resetQuitedAt(SiteUser siteUser) {
diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java
index 319083658..d6feed9e1 100644
--- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java
+++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java
@@ -1,22 +1,22 @@
package com.example.solidconnection.auth.service;
+import static com.example.solidconnection.common.exception.ErrorCode.NICKNAME_ALREADY_EXISTED;
+import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID;
+import static com.example.solidconnection.common.exception.ErrorCode.USER_ALREADY_EXISTED;
+
import com.example.solidconnection.auth.dto.SignInResponse;
import com.example.solidconnection.auth.dto.SignUpRequest;
-import com.example.solidconnection.custom.exception.CustomException;
-import com.example.solidconnection.entity.InterestedCountry;
-import com.example.solidconnection.entity.InterestedRegion;
-import com.example.solidconnection.repositories.CountryRepository;
-import com.example.solidconnection.repositories.InterestedCountyRepository;
-import com.example.solidconnection.repositories.InterestedRegionRepository;
-import com.example.solidconnection.repositories.RegionRepository;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.location.country.service.InterestedCountryService;
+import com.example.solidconnection.location.region.service.InterestedRegionService;
+import com.example.solidconnection.siteuser.domain.AuthType;
+import com.example.solidconnection.siteuser.domain.Role;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import java.util.List;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED;
-
/*
* 우리 서버에서 인증되었음을 확인하기 위한 signUpToken 을 검증한다.
* - 사용자 정보를 DB에 저장한다.
@@ -24,67 +24,77 @@
* - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다.
* - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다.
* */
-public abstract class SignUpService {
+@Service
+@RequiredArgsConstructor
+public class SignUpService {
- protected final SignInService signInService;
- protected final SiteUserRepository siteUserRepository;
- protected final RegionRepository regionRepository;
- protected final InterestedRegionRepository interestedRegionRepository;
- protected final CountryRepository countryRepository;
- protected final InterestedCountyRepository interestedCountyRepository;
-
- protected SignUpService(SignInService signInService, SiteUserRepository siteUserRepository,
- RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository,
- CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository) {
- this.signInService = signInService;
- this.siteUserRepository = siteUserRepository;
- this.regionRepository = regionRepository;
- this.interestedRegionRepository = interestedRegionRepository;
- this.countryRepository = countryRepository;
- this.interestedCountyRepository = interestedCountyRepository;
- }
+ private final SignInService signInService;
+ private final SiteUserRepository siteUserRepository;
+ private final InterestedRegionService interestedRegionService;
+ private final InterestedCountryService interestedCountryService;
+ private final SignUpTokenProvider signUpTokenProvider;
+ private final PasswordTemporaryStorage passwordTemporaryStorage;
@Transactional
public SignInResponse signUp(SignUpRequest signUpRequest) {
// 검증
- validateSignUpToken(signUpRequest);
- validateUserNotDuplicated(signUpRequest);
- validateNicknameDuplicated(signUpRequest.nickname());
+ signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken());
+ String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken());
+ AuthType authType = signUpTokenProvider.parseAuthType(signUpRequest.signUpToken());
+ validateNicknameNotDuplicated(signUpRequest.nickname());
+ validateUserNotDuplicated(email, authType);
+
+ // 임시 저장된 비밀번호 가져오기
+ String password = getTemporarySavedPassword(email, authType);
// 사용자 저장
- SiteUser siteUser = siteUserRepository.save(createSiteUser(signUpRequest));
+ SiteUser siteUser = siteUserRepository.save(new SiteUser(
+ email,
+ signUpRequest.nickname(),
+ signUpRequest.profileImageUrl(),
+ signUpRequest.exchangeStatus(),
+ Role.MENTEE,
+ authType,
+ password
+ ));
// 관심 지역, 국가 저장
- saveInterestedRegion(signUpRequest, siteUser);
- saveInterestedCountry(signUpRequest, siteUser);
+ interestedRegionService.saveInterestedRegion(siteUser, signUpRequest.interestedRegions());
+ interestedCountryService.saveInterestedCountry(siteUser, signUpRequest.interestedCountries());
// 로그인
- return signInService.signIn(siteUser);
+ SignInResponse response = signInService.signIn(siteUser);
+
+ // 회원가입을 위해 저장한 데이터(SignUpToken, 비밀번호) 삭제
+ clearSignUpData(email, authType);
+
+ return response;
}
- private void validateNicknameDuplicated(String nickname) {
+ private void validateNicknameNotDuplicated(String nickname) {
if (siteUserRepository.existsByNickname(nickname)) {
throw new CustomException(NICKNAME_ALREADY_EXISTED);
}
}
- private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) {
- List interestedRegionNames = signUpRequest.interestedRegions();
- List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream()
- .map(region -> new InterestedRegion(savedSiteUser, region))
- .toList();
- interestedRegionRepository.saveAll(interestedRegions);
+ private void validateUserNotDuplicated(String email, AuthType authType) {
+ if (siteUserRepository.existsByEmailAndAuthType(email, authType)) {
+ throw new CustomException(USER_ALREADY_EXISTED);
+ }
}
- private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) {
- List interestedCountryNames = signUpRequest.interestedCountries();
- List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream()
- .map(country -> new InterestedCountry(savedSiteUser, country))
- .toList();
- interestedCountyRepository.saveAll(interestedCountries);
+ private String getTemporarySavedPassword(String email, AuthType authType) {
+ if (authType == AuthType.EMAIL) {
+ return passwordTemporaryStorage.findByEmail(email)
+ .orElseThrow(() -> new CustomException(SIGN_UP_TOKEN_INVALID));
+ }
+ return null;
}
- protected abstract void validateSignUpToken(SignUpRequest signUpRequest);
- protected abstract void validateUserNotDuplicated(SignUpRequest signUpRequest);
- protected abstract SiteUser createSiteUser(SignUpRequest signUpRequest);
+ private void clearSignUpData(String email, AuthType authType) {
+ if (authType == AuthType.EMAIL) {
+ passwordTemporaryStorage.deleteByEmail(email);
+ }
+ signUpTokenProvider.deleteByEmail(email);
+ }
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java
similarity index 65%
rename from src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java
rename to src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java
index c3a95dbe9..05480b10d 100644
--- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java
+++ b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java
@@ -1,34 +1,32 @@
-package com.example.solidconnection.auth.service.oauth;
+package com.example.solidconnection.auth.service;
+
+import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_INVALID;
+import static com.example.solidconnection.common.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER;
import com.example.solidconnection.auth.domain.TokenType;
-import com.example.solidconnection.auth.service.TokenProvider;
-import com.example.solidconnection.config.security.JwtProperties;
-import com.example.solidconnection.custom.exception.CustomException;
+import com.example.solidconnection.auth.token.config.JwtProperties;
+import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.siteuser.domain.AuthType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID;
-import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER;
-import static com.example.solidconnection.util.JwtUtils.parseClaims;
-import static com.example.solidconnection.util.JwtUtils.parseSubject;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
@Component
-public class OAuthSignUpTokenProvider extends TokenProvider {
+@RequiredArgsConstructor
+public class SignUpTokenProvider {
- static final String AUTH_TYPE_CLAIM_KEY = "authType";
+ private static final String AUTH_TYPE_CLAIM_KEY = "authType";
- public OAuthSignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) {
- super(jwtProperties, redisTemplate);
- }
+ private final JwtProperties jwtProperties;
+ private final RedisTemplate redisTemplate;
+ private final TokenProvider tokenProvider;
public String generateAndSaveSignUpToken(String email, AuthType authType) {
Map authTypeClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, authType));
@@ -42,7 +40,12 @@ public String generateAndSaveSignUpToken(String email, AuthType authType) {
.setExpiration(expiredDate)
.signWith(SignatureAlgorithm.HS512, jwtProperties.secret())
.compact();
- return saveToken(signUpToken, TokenType.SIGN_UP);
+ return tokenProvider.saveToken(signUpToken, TokenType.SIGN_UP);
+ }
+
+ public void deleteByEmail(String email) {
+ String key = TokenType.SIGN_UP.addPrefix(email);
+ redisTemplate.delete(key);
}
public void validateSignUpToken(String token) {
@@ -51,9 +54,9 @@ public void validateSignUpToken(String token) {
validateIssuedByServer(email);
}
- private void validateFormatAndExpiration(String token) {
+ private void validateFormatAndExpiration(String token) { // 파싱되는지, AuthType이 포함되어있는지 검증
try {
- Claims claims = parseClaims(token, jwtProperties.secret());
+ Claims claims = tokenProvider.parseClaims(token);
Objects.requireNonNull(claims.getSubject());
String serializedAuthType = claims.get(AUTH_TYPE_CLAIM_KEY, String.class);
AuthType.valueOf(serializedAuthType);
@@ -70,11 +73,11 @@ private void validateIssuedByServer(String email) {
}
public String parseEmail(String token) {
- return parseSubject(token, jwtProperties.secret());
+ return tokenProvider.parseSubject(token);
}
public AuthType parseAuthType(String token) {
- Claims claims = parseClaims(token, jwtProperties.secret());
+ Claims claims = tokenProvider.parseClaims(token);
String authTypeStr = claims.get(AUTH_TYPE_CLAIM_KEY, String.class);
return AuthType.valueOf(authTypeStr);
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/Subject.java b/src/main/java/com/example/solidconnection/auth/service/Subject.java
new file mode 100644
index 000000000..15e5c6c75
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/service/Subject.java
@@ -0,0 +1,7 @@
+package com.example.solidconnection.auth.service;
+
+public record Subject(
+ String value
+) {
+
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java
index f5f638ab3..22120b084 100644
--- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java
+++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java
@@ -1,47 +1,18 @@
package com.example.solidconnection.auth.service;
import com.example.solidconnection.auth.domain.TokenType;
-import com.example.solidconnection.config.security.JwtProperties;
import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
-import org.springframework.data.redis.core.RedisTemplate;
+import java.util.Map;
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
+public interface TokenProvider {
-import static com.example.solidconnection.util.JwtUtils.parseSubject;
+ String generateToken(String string, TokenType tokenType);
-public abstract class TokenProvider {
+ String generateToken(String string, Map claims, TokenType tokenType);
- protected final JwtProperties jwtProperties;
- protected final RedisTemplate redisTemplate;
+ String saveToken(String token, TokenType tokenType);
- public TokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) {
- this.jwtProperties = jwtProperties;
- this.redisTemplate = redisTemplate;
- }
+ String parseSubject(String token);
- protected final String generateToken(String string, TokenType tokenType) {
- Claims claims = Jwts.claims().setSubject(string);
- Date now = new Date();
- Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime());
- return Jwts.builder()
- .setClaims(claims)
- .setIssuedAt(now)
- .setExpiration(expiredDate)
- .signWith(SignatureAlgorithm.HS512, jwtProperties.secret())
- .compact();
- }
-
- protected final String saveToken(String token, TokenType tokenType) {
- String subject = parseSubject(token, jwtProperties.secret());
- redisTemplate.opsForValue().set(
- tokenType.addPrefix(subject),
- token,
- tokenType.getExpireTime(),
- TimeUnit.MILLISECONDS
- );
- return token;
- }
+ Claims parseClaims(String token);
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java
deleted file mode 100644
index 2605ad89f..000000000
--- a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example.solidconnection.auth.service.oauth;
-
-import com.example.solidconnection.auth.client.AppleOAuthClient;
-import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto;
-import com.example.solidconnection.auth.service.SignInService;
-import com.example.solidconnection.siteuser.domain.AuthType;
-import com.example.solidconnection.siteuser.repository.SiteUserRepository;
-import org.springframework.stereotype.Service;
-
-@Service
-public class AppleOAuthService extends OAuthService {
-
- private final AppleOAuthClient appleOAuthClient;
-
- public AppleOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository,
- AppleOAuthClient appleOAuthClient, SignInService signInService) {
- super(OAuthSignUpTokenProvider, siteUserRepository, signInService);
- this.appleOAuthClient = appleOAuthClient;
- }
-
- @Override
- protected OAuthUserInfoDto getOAuthUserInfo(String code) {
- return appleOAuthClient.processOAuth(code);
- }
-
- @Override
- protected AuthType getAuthType() {
- return AuthType.APPLE;
- }
-}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java
deleted file mode 100644
index c2202ab2a..000000000
--- a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.example.solidconnection.auth.service.oauth;
-
-import com.example.solidconnection.auth.client.KakaoOAuthClient;
-import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto;
-import com.example.solidconnection.auth.service.SignInService;
-import com.example.solidconnection.siteuser.domain.AuthType;
-import com.example.solidconnection.siteuser.repository.SiteUserRepository;
-import org.springframework.stereotype.Service;
-
-@Service
-public class KakaoOAuthService extends OAuthService {
-
- private final KakaoOAuthClient kakaoOAuthClient;
-
- public KakaoOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository,
- KakaoOAuthClient kakaoOAuthClient, SignInService signInService) {
- super(OAuthSignUpTokenProvider, siteUserRepository, signInService);
- this.kakaoOAuthClient = kakaoOAuthClient;
- }
-
- @Override
- protected OAuthUserInfoDto getOAuthUserInfo(String code) {
- return kakaoOAuthClient.getUserInfo(code);
- }
-
- @Override
- protected AuthType getAuthType() {
- return AuthType.KAKAO;
- }
-}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClient.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClient.java
new file mode 100644
index 000000000..a62872f0c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClient.java
@@ -0,0 +1,11 @@
+package com.example.solidconnection.auth.service.oauth;
+
+import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto;
+import com.example.solidconnection.siteuser.domain.AuthType;
+
+public interface OAuthClient {
+
+ OAuthUserInfoDto getUserInfo(String code);
+
+ AuthType getAuthType();
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClientMap.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClientMap.java
new file mode 100644
index 000000000..45e510136
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthClientMap.java
@@ -0,0 +1,32 @@
+package com.example.solidconnection.auth.service.oauth;
+
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.common.exception.ErrorCode;
+import com.example.solidconnection.siteuser.domain.AuthType;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OAuthClientMap {
+
+ private final Map oauthClientMap;
+
+ public OAuthClientMap(List oAuthClientList) {
+ this.oauthClientMap = oAuthClientList.stream()
+ .collect(Collectors.toMap(OAuthClient::getAuthType, Function.identity()));
+ }
+
+ public OAuthClient getOAuthClient(AuthType authType) {
+ OAuthClient oauthClient = oauthClientMap.get(authType);
+ if (oauthClient == null) {
+ throw new CustomException(
+ ErrorCode.NOT_DEFINED_ERROR,
+ "처리할 수 있는 OAuthClient가 없습니다. authType: " + authType.name()
+ );
+ }
+ return oauthClient;
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java
index 6e9bf7030..9343bfa21 100644
--- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java
+++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java
@@ -1,6 +1,5 @@
package com.example.solidconnection.auth.service.oauth;
-
import com.example.solidconnection.auth.dto.SignInResponse;
import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest;
import com.example.solidconnection.auth.dto.oauth.OAuthResponse;
@@ -8,54 +7,50 @@
import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto;
import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse;
import com.example.solidconnection.auth.service.SignInService;
+import com.example.solidconnection.auth.service.SignUpTokenProvider;
import com.example.solidconnection.siteuser.domain.AuthType;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
-import org.springframework.transaction.annotation.Transactional;
-
import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
/*
* OAuth 제공자로부터 이메일을 받아 기존 회원인지, 신규 회원인지 판별하고, 이에 따라 다르게 응답한다.
* 기존 회원 : 로그인한다.
* 신규 회원 : 회원가입할 때 필요한 정보를 제공한다.
* */
-public abstract class OAuthService {
+@Service
+@RequiredArgsConstructor
+public class OAuthService {
- private final OAuthSignUpTokenProvider OAuthSignUpTokenProvider;
+ private final SignUpTokenProvider signUpTokenProvider;
private final SignInService signInService;
private final SiteUserRepository siteUserRepository;
-
- protected OAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) {
- this.OAuthSignUpTokenProvider = OAuthSignUpTokenProvider;
- this.siteUserRepository = siteUserRepository;
- this.signInService = signInService;
- }
+ private final OAuthClientMap oauthClientMap;
@Transactional
- public OAuthResponse processOAuth(OAuthCodeRequest oauthCodeRequest) {
- OAuthUserInfoDto userInfoDto = getOAuthUserInfo(oauthCodeRequest.code());
- String email = userInfoDto.getEmail();
- Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, getAuthType());
+ public OAuthResponse processOAuth(AuthType authType, OAuthCodeRequest codeRequest) {
+ OAuthClient oauthClient = oauthClientMap.getOAuthClient(authType);
+ OAuthUserInfoDto userInfo = oauthClient.getUserInfo(codeRequest.code());
+ Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(userInfo.getEmail(), authType);
if (optionalSiteUser.isPresent()) {
SiteUser siteUser = optionalSiteUser.get();
return getSignInResponse(siteUser);
}
- return getSignUpPrepareResponse(userInfoDto);
+ return getSignUpPrepareResponse(userInfo, authType);
}
- protected final OAuthSignInResponse getSignInResponse(SiteUser siteUser) {
+ private OAuthSignInResponse getSignInResponse(SiteUser siteUser) {
SignInResponse signInResponse = signInService.signIn(siteUser);
return new OAuthSignInResponse(true, signInResponse.accessToken(), signInResponse.refreshToken());
}
- protected final SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto) {
- String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType());
+ private SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto, AuthType authType) {
+ String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), authType);
return SignUpPrepareResponse.of(userInfoDto, signUpToken);
}
-
- protected abstract OAuthUserInfoDto getOAuthUserInfo(String code);
- protected abstract AuthType getAuthType();
}
diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java
deleted file mode 100644
index a46728bb2..000000000
--- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.example.solidconnection.auth.service.oauth;
-
-import com.example.solidconnection.auth.dto.SignUpRequest;
-import com.example.solidconnection.auth.service.SignInService;
-import com.example.solidconnection.auth.service.SignUpService;
-import com.example.solidconnection.custom.exception.CustomException;
-import com.example.solidconnection.repositories.CountryRepository;
-import com.example.solidconnection.repositories.InterestedCountyRepository;
-import com.example.solidconnection.repositories.InterestedRegionRepository;
-import com.example.solidconnection.repositories.RegionRepository;
-import com.example.solidconnection.siteuser.domain.AuthType;
-import com.example.solidconnection.siteuser.domain.SiteUser;
-import com.example.solidconnection.siteuser.repository.SiteUserRepository;
-import org.springframework.stereotype.Service;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED;
-
-@Service
-public class OAuthSignUpService extends SignUpService {
-
- private final OAuthSignUpTokenProvider oAuthSignUpTokenProvider;
-
- OAuthSignUpService(SignInService signInService, SiteUserRepository siteUserRepository,
- RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository,
- CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository,
- OAuthSignUpTokenProvider oAuthSignUpTokenProvider) {
- super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountyRepository);
- this.oAuthSignUpTokenProvider = oAuthSignUpTokenProvider;
- }
-
- @Override
- protected void validateSignUpToken(SignUpRequest signUpRequest) {
- oAuthSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken());
- }
-
- @Override
- protected void validateUserNotDuplicated(SignUpRequest signUpRequest) {
- String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken());
- AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken());
- if (siteUserRepository.existsByEmailAndAuthType(email, authType)) {
- throw new CustomException(USER_ALREADY_EXISTED);
- }
- }
-
- @Override
- protected SiteUser createSiteUser(SignUpRequest signUpRequest) {
- String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken());
- AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken());
- return signUpRequest.toOAuthSiteUser(email, authType);
- }
-}
diff --git a/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java b/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java
new file mode 100644
index 000000000..d7c968ccf
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/token/JwtTokenProvider.java
@@ -0,0 +1,77 @@
+package com.example.solidconnection.auth.token;
+
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_TOKEN;
+
+import com.example.solidconnection.auth.domain.TokenType;
+import com.example.solidconnection.auth.service.TokenProvider;
+import com.example.solidconnection.auth.token.config.JwtProperties;
+import com.example.solidconnection.common.exception.CustomException;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class JwtTokenProvider implements TokenProvider {
+
+ private final JwtProperties jwtProperties;
+ private final RedisTemplate redisTemplate;
+
+ @Override
+ public final String generateToken(String string, TokenType tokenType) {
+ return generateJwtTokenValue(string, Map.of(), tokenType.getExpireTime());
+ }
+
+ @Override
+ public String generateToken(String string, Map customClaims, TokenType tokenType) {
+ return generateJwtTokenValue(string, customClaims, tokenType.getExpireTime());
+ }
+
+ private String generateJwtTokenValue(String subject, Map claims, long expireTime) {
+ Claims jwtClaims = Jwts.claims().setSubject(subject);
+ jwtClaims.putAll(claims);
+ Date now = new Date();
+ Date expiredDate = new Date(now.getTime() + expireTime);
+ return Jwts.builder()
+ .setClaims(jwtClaims)
+ .setIssuedAt(now)
+ .setExpiration(expiredDate)
+ .signWith(SignatureAlgorithm.HS512, jwtProperties.secret())
+ .compact();
+ }
+
+ @Override
+ public final String saveToken(String token, TokenType tokenType) {
+ String subject = parseSubject(token);
+ redisTemplate.opsForValue().set(
+ tokenType.addPrefix(subject),
+ token,
+ tokenType.getExpireTime(),
+ TimeUnit.MILLISECONDS
+ );
+ return token;
+ }
+
+ @Override
+ public String parseSubject(String token) {
+ return parseClaims(token).getSubject();
+ }
+
+ @Override
+ public Claims parseClaims(String token) {
+ try {
+ return Jwts.parser()
+ .setSigningKey(jwtProperties.secret())
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (Exception e) {
+ throw new CustomException(INVALID_TOKEN);
+ }
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java b/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java
new file mode 100644
index 000000000..7f208710c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/auth/token/TokenBlackListService.java
@@ -0,0 +1,34 @@
+package com.example.solidconnection.auth.token;
+
+import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST;
+
+import com.example.solidconnection.auth.service.AccessToken;
+import com.example.solidconnection.security.filter.BlacklistChecker;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class TokenBlackListService implements BlacklistChecker {
+
+ private static final String SIGN_OUT_VALUE = "signOut";
+
+ private final RedisTemplate redisTemplate;
+
+ /*
+ * 액세스 토큰을 블랙리스트에 저장한다.
+ * - key = BLACKLIST:{accessToken}
+ * - value = {SIGN_OUT_VALUE} -> key 의 존재만 확인하므로, value 에는 무슨 값이 들어가도 상관없다.
+ * */
+ public void addToBlacklist(AccessToken accessToken) {
+ String blackListKey = BLACKLIST.addPrefix(accessToken.token());
+ redisTemplate.opsForValue().set(blackListKey, SIGN_OUT_VALUE);
+ }
+
+ @Override
+ public boolean isTokenBlacklisted(String accessToken) {
+ String blackListTokenKey = BLACKLIST.addPrefix(accessToken);
+ return redisTemplate.hasKey(blackListTokenKey);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/config/security/JwtProperties.java b/src/main/java/com/example/solidconnection/auth/token/config/JwtProperties.java
similarity index 74%
rename from src/main/java/com/example/solidconnection/config/security/JwtProperties.java
rename to src/main/java/com/example/solidconnection/auth/token/config/JwtProperties.java
index e0c63da46..2d81601ee 100644
--- a/src/main/java/com/example/solidconnection/config/security/JwtProperties.java
+++ b/src/main/java/com/example/solidconnection/auth/token/config/JwtProperties.java
@@ -1,7 +1,8 @@
-package com.example.solidconnection.config.security;
+package com.example.solidconnection.auth.token.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(String secret) {
+
}
diff --git a/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java b/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java
index c785168b3..0d2ee9eaf 100644
--- a/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java
+++ b/src/main/java/com/example/solidconnection/cache/CacheUpdateListener.java
@@ -1,13 +1,12 @@
package com.example.solidconnection.cache;
+import java.nio.charset.StandardCharsets;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
-import java.nio.charset.StandardCharsets;
-
@Component
@RequiredArgsConstructor
@Slf4j
diff --git a/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java b/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java
index 48c36b28c..f2c08edcc 100644
--- a/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java
+++ b/src/main/java/com/example/solidconnection/cache/CompletableFutureManager.java
@@ -1,10 +1,9 @@
package com.example.solidconnection.cache;
-import org.springframework.stereotype.Component;
-
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.stereotype.Component;
@Component
public class CompletableFutureManager {
diff --git a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java
index a37e80f51..b6a9fe0b0 100644
--- a/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java
+++ b/src/main/java/com/example/solidconnection/cache/ThunderingHerdCachingAspect.java
@@ -1,8 +1,20 @@
package com.example.solidconnection.cache;
+import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_CHANNEL;
+import static com.example.solidconnection.community.post.service.RedisConstants.LOCK_TIMEOUT_MS;
+import static com.example.solidconnection.community.post.service.RedisConstants.MAX_WAIT_TIME_MS;
+import static com.example.solidconnection.community.post.service.RedisConstants.REFRESH_LIMIT_PERCENT;
+
import com.example.solidconnection.cache.annotation.ThunderingHerdCaching;
import com.example.solidconnection.cache.manager.CacheManager;
import com.example.solidconnection.util.RedisUtils;
+import java.time.Duration;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
@@ -14,19 +26,6 @@
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
-import java.time.Duration;
-import java.util.UUID;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import static com.example.solidconnection.type.RedisConstants.CREATE_CHANNEL;
-import static com.example.solidconnection.type.RedisConstants.LOCK_TIMEOUT_MS;
-import static com.example.solidconnection.type.RedisConstants.MAX_WAIT_TIME_MS;
-import static com.example.solidconnection.type.RedisConstants.REFRESH_LIMIT_PERCENT;
-
@Aspect
@Component
@Slf4j
diff --git a/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java b/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java
index c5a9e0e9b..5cf59b41d 100644
--- a/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java
+++ b/src/main/java/com/example/solidconnection/cache/annotation/ThunderingHerdCaching.java
@@ -8,6 +8,7 @@
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThunderingHerdCaching {
+
String key();
String cacheManager();
diff --git a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java
index 2e489567c..581822f73 100644
--- a/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java
+++ b/src/main/java/com/example/solidconnection/cache/manager/CustomCacheManager.java
@@ -1,14 +1,13 @@
package com.example.solidconnection.cache.manager;
+import java.time.Duration;
+import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
-import java.time.Duration;
-import java.util.Set;
-
@Component("customCacheManager")
public class CustomCacheManager implements CacheManager {
diff --git a/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java b/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java
new file mode 100644
index 000000000..6c3054355
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java
@@ -0,0 +1,27 @@
+package com.example.solidconnection.chat.config;
+
+import java.security.Principal;
+import java.util.Map;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
+
+// WebSocket 세션의 Principal을 결정한다.
+@Component
+public class CustomHandshakeHandler extends DefaultHandshakeHandler {
+
+ @Override
+ protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
+ Map attributes) {
+
+ Object userAttribute = attributes.get("user");
+
+ if (userAttribute instanceof Principal) {
+ Principal principal = (Principal) userAttribute;
+ return principal;
+ }
+
+ return super.determineUser(request, wsHandler, attributes);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/config/StompEventListener.java b/src/main/java/com/example/solidconnection/chat/config/StompEventListener.java
new file mode 100644
index 000000000..1064eef3a
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/config/StompEventListener.java
@@ -0,0 +1,27 @@
+package com.example.solidconnection.chat.config;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.springframework.context.event.EventListener;
+import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.messaging.SessionConnectEvent;
+import org.springframework.web.socket.messaging.SessionDisconnectEvent;
+
+@Component
+public class StompEventListener {
+
+ private final Set sessions = ConcurrentHashMap.newKeySet();
+
+ @EventListener
+ public void connectHandle(SessionConnectEvent event) {
+ StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
+ sessions.add(accessor.getSessionId());
+ }
+
+ @EventListener
+ public void disconnectHandle(SessionDisconnectEvent event) {
+ StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
+ sessions.remove(accessor.getSessionId());
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java
new file mode 100644
index 000000000..2e99bf9c4
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java
@@ -0,0 +1,69 @@
+package com.example.solidconnection.chat.config;
+
+import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED;
+
+import com.example.solidconnection.chat.service.ChatService;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.common.exception.ErrorCode;
+import com.example.solidconnection.security.authentication.TokenAuthentication;
+import com.example.solidconnection.security.userdetails.SiteUserDetails;
+import java.security.Principal;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import lombok.RequiredArgsConstructor;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.simp.stomp.StompCommand;
+import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
+import org.springframework.messaging.support.ChannelInterceptor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class StompHandler implements ChannelInterceptor {
+
+ private static final Pattern ROOM_ID_PATTERN = Pattern.compile("^/topic/chat/(\\d+)$");
+ private final ChatService chatService;
+
+ @Override
+ public Message> preSend(Message> message, MessageChannel channel) {
+ final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
+
+ if (StompCommand.CONNECT.equals(accessor.getCommand())) {
+ Principal user = accessor.getUser();
+ if (user == null) {
+ throw new CustomException(AUTHENTICATION_FAILED);
+ }
+ }
+
+ if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
+ Principal user = accessor.getUser();
+ if (user == null) {
+ throw new CustomException(AUTHENTICATION_FAILED);
+ }
+
+ TokenAuthentication tokenAuthentication = (TokenAuthentication) user;
+ SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal();
+
+ String destination = accessor.getDestination();
+ long roomId = Long.parseLong(extractRoomId(destination));
+
+ chatService.validateChatRoomParticipant(siteUserDetails.getSiteUser().getId(), roomId);
+ }
+
+ return message;
+ }
+
+ private String extractRoomId(String destination) {
+ if (destination == null) {
+ throw new CustomException(ErrorCode.INVALID_ROOM_ID);
+ }
+
+ Matcher matcher = ROOM_ID_PATTERN.matcher(destination);
+ if (!matcher.matches()) {
+ throw new CustomException(ErrorCode.INVALID_ROOM_ID);
+ }
+
+ return matcher.group(1);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/config/StompProperties.java b/src/main/java/com/example/solidconnection/chat/config/StompProperties.java
new file mode 100644
index 000000000..ce9663c72
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/config/StompProperties.java
@@ -0,0 +1,23 @@
+package com.example.solidconnection.chat.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "websocket")
+public record StompProperties(ThreadPool threadPool, HeartbeatProperties heartbeat) {
+
+ public record ThreadPool(InboundProperties inbound, OutboundProperties outbound) {
+
+ }
+
+ public record InboundProperties(int corePoolSize, int maxPoolSize, int queueCapacity) {
+
+ }
+
+ public record OutboundProperties(int corePoolSize, int maxPoolSize) {
+
+ }
+
+ public record HeartbeatProperties(long serverInterval, long clientInterval) {
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java
new file mode 100644
index 000000000..51259a0e1
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java
@@ -0,0 +1,61 @@
+package com.example.solidconnection.chat.config;
+
+import com.example.solidconnection.chat.config.StompProperties.HeartbeatProperties;
+import com.example.solidconnection.chat.config.StompProperties.InboundProperties;
+import com.example.solidconnection.chat.config.StompProperties.OutboundProperties;
+import com.example.solidconnection.security.config.CorsProperties;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+@Configuration
+@EnableWebSocketMessageBroker
+@RequiredArgsConstructor
+public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+ private final StompHandler stompHandler;
+ private final StompProperties stompProperties;
+ private final CorsProperties corsProperties;
+ private final WebSocketHandshakeInterceptor webSocketHandshakeInterceptor;
+ private final CustomHandshakeHandler customHandshakeHandler;
+
+ @Override
+ public void registerStompEndpoints(StompEndpointRegistry registry) {
+ List strings = corsProperties.allowedOrigins();
+ String[] allowedOrigins = strings.toArray(String[]::new);
+ registry.addEndpoint("/connect")
+ .setAllowedOrigins(allowedOrigins)
+ .addInterceptors(webSocketHandshakeInterceptor)
+ .setHandshakeHandler(customHandshakeHandler)
+ .withSockJS();
+ }
+
+ @Override
+ public void configureClientInboundChannel(ChannelRegistration registration) {
+ InboundProperties inboundProperties = stompProperties.threadPool().inbound();
+ registration.interceptors(stompHandler).taskExecutor().corePoolSize(inboundProperties.corePoolSize()).maxPoolSize(inboundProperties.maxPoolSize()).queueCapacity(inboundProperties.queueCapacity());
+ }
+
+ @Override
+ public void configureClientOutboundChannel(ChannelRegistration registration) {
+ OutboundProperties outboundProperties = stompProperties.threadPool().outbound();
+ registration.taskExecutor().corePoolSize(outboundProperties.corePoolSize()).maxPoolSize(outboundProperties.maxPoolSize());
+ }
+
+ @Override
+ public void configureMessageBroker(MessageBrokerRegistry registry) {
+ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+ scheduler.setPoolSize(1);
+ scheduler.setThreadNamePrefix("wss-heartbeat-");
+ scheduler.initialize();
+ HeartbeatProperties heartbeatProperties = stompProperties.heartbeat();
+ registry.setApplicationDestinationPrefixes("/publish");
+ registry.enableSimpleBroker("/topic").setHeartbeatValue(new long[]{heartbeatProperties.serverInterval(), heartbeatProperties.clientInterval()}).setTaskScheduler(scheduler);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java b/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java
new file mode 100644
index 000000000..e4af7a412
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java
@@ -0,0 +1,31 @@
+package com.example.solidconnection.chat.config;
+
+import java.security.Principal;
+import java.util.Map;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+// Principal을 WebSocket 세션에 저장하는 것에만 집중한다.
+@Component
+public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
+ WebSocketHandler wsHandler, Map attributes) {
+ Principal principal = request.getPrincipal();
+
+ if (principal != null) {
+ attributes.put("user", principal);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
+ WebSocketHandler wsHandler, Exception exception) {
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java
new file mode 100644
index 000000000..ddb1f9084
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java
@@ -0,0 +1,62 @@
+package com.example.solidconnection.chat.controller;
+
+import com.example.solidconnection.chat.dto.ChatMessageResponse;
+import com.example.solidconnection.chat.dto.ChatParticipantResponse;
+import com.example.solidconnection.chat.dto.ChatRoomListResponse;
+import com.example.solidconnection.chat.service.ChatService;
+import com.example.solidconnection.common.dto.SliceResponse;
+import com.example.solidconnection.common.resolver.AuthorizedUser;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/chats")
+public class ChatController {
+
+ private final ChatService chatService;
+
+ @GetMapping("/rooms")
+ public ResponseEntity getChatRooms(
+ @AuthorizedUser long siteUserId
+ ) {
+ ChatRoomListResponse chatRoomListResponse = chatService.getChatRooms(siteUserId);
+ return ResponseEntity.ok(chatRoomListResponse);
+ }
+
+ @GetMapping("/rooms/{room-id}")
+ public ResponseEntity> getChatMessages(
+ @AuthorizedUser long siteUserId,
+ @PathVariable("room-id") Long roomId,
+ @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
+ ) {
+ SliceResponse response = chatService.getChatMessages(siteUserId, roomId, pageable);
+ return ResponseEntity.ok(response);
+ }
+
+ @GetMapping("rooms/{room-id}/partner")
+ public ResponseEntity getChatPartner(
+ @AuthorizedUser long siteUserId,
+ @PathVariable("room-id") Long roomId
+ ) {
+ ChatParticipantResponse response = chatService.getChatPartner(siteUserId, roomId);
+ return ResponseEntity.ok(response);
+ }
+
+ @PutMapping("/rooms/{room-id}/read")
+ public ResponseEntity markChatMessagesAsRead(
+ @AuthorizedUser long siteUserId,
+ @PathVariable("room-id") Long roomId
+ ) {
+ chatService.markChatMessagesAsRead(siteUserId, roomId);
+ return ResponseEntity.ok().build();
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java
new file mode 100644
index 000000000..a7e158224
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java
@@ -0,0 +1,32 @@
+package com.example.solidconnection.chat.controller;
+
+import com.example.solidconnection.chat.dto.ChatMessageSendRequest;
+import com.example.solidconnection.chat.service.ChatService;
+import com.example.solidconnection.security.authentication.TokenAuthentication;
+import com.example.solidconnection.security.userdetails.SiteUserDetails;
+import jakarta.validation.Valid;
+import java.security.Principal;
+import lombok.RequiredArgsConstructor;
+import org.springframework.messaging.handler.annotation.DestinationVariable;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.stereotype.Controller;
+
+@Controller
+@RequiredArgsConstructor
+public class ChatMessageController {
+
+ private final ChatService chatService;
+
+ @MessageMapping("/chat/{roomId}")
+ public void sendChatMessage(
+ @DestinationVariable Long roomId,
+ @Valid @Payload ChatMessageSendRequest chatMessageSendRequest,
+ Principal principal
+ ) {
+ TokenAuthentication tokenAuthentication = (TokenAuthentication) principal;
+ SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal();
+
+ chatService.sendChatMessage(chatMessageSendRequest, siteUserDetails.getSiteUser().getId(), roomId);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java
new file mode 100644
index 000000000..def9263c8
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java
@@ -0,0 +1,45 @@
+package com.example.solidconnection.chat.domain;
+
+import com.example.solidconnection.common.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ChatAttachment extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private Boolean isImage;
+
+ @Column(nullable = false, length = 500)
+ private String url;
+
+ @Column(length = 500)
+ private String thumbnailUrl;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private ChatMessage chatMessage;
+
+ public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMessage chatMessage) {
+ this.isImage = isImage;
+ this.url = url;
+ this.thumbnailUrl = thumbnailUrl;
+ this.chatMessage = chatMessage;
+ if (chatMessage != null) {
+ chatMessage.getChatAttachments().add(this);
+ }
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java
new file mode 100644
index 000000000..07fc99131
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java
@@ -0,0 +1,47 @@
+package com.example.solidconnection.chat.domain;
+
+import com.example.solidconnection.common.BaseEntity;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ChatMessage extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, length = 500)
+ private String content;
+
+ private long senderId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private ChatRoom chatRoom;
+
+ @OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL)
+ private final List chatAttachments = new ArrayList<>();
+
+ public ChatMessage(String content, long senderId, ChatRoom chatRoom) {
+ this.content = content;
+ this.senderId = senderId;
+ this.chatRoom = chatRoom;
+ if (chatRoom != null) {
+ chatRoom.getChatMessages().add(this);
+ }
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java b/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java
new file mode 100644
index 000000000..60fc6b795
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java
@@ -0,0 +1,45 @@
+package com.example.solidconnection.chat.domain;
+
+import com.example.solidconnection.common.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(uniqueConstraints = {
+ @UniqueConstraint(
+ name = "uk_chat_participant_chat_room_id_site_user_id",
+ columnNames = {"chat_room_id", "site_user_id"}
+ )
+})
+public class ChatParticipant extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "site_user_id")
+ private long siteUserId;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ private ChatRoom chatRoom;
+
+ public ChatParticipant(long siteUserId, ChatRoom chatRoom) {
+ this.siteUserId = siteUserId;
+ this.chatRoom = chatRoom;
+ if (chatRoom != null) {
+ chatRoom.getChatParticipants().add(this);
+ }
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java b/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java
new file mode 100644
index 000000000..8c731738c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java
@@ -0,0 +1,40 @@
+package com.example.solidconnection.chat.domain;
+
+import com.example.solidconnection.common.BaseEntity;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(uniqueConstraints = {
+ @UniqueConstraint(
+ name = "uk_chat_read_status_chat_room_id_chat_participant_id",
+ columnNames = {"chat_room_id", "chat_participant_id"}
+ )
+})
+public class ChatReadStatus extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "chat_room_id")
+ private long chatRoomId;
+
+ @Column(name = "chat_participant_id")
+ private long chatParticipantId;
+
+ public ChatReadStatus(long chatRoomId, long chatParticipantId) {
+ this.chatRoomId = chatRoomId;
+ this.chatParticipantId = chatParticipantId;
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java
new file mode 100644
index 000000000..fc159c2cd
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java
@@ -0,0 +1,47 @@
+package com.example.solidconnection.chat.domain;
+
+import com.example.solidconnection.common.BaseEntity;
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.hibernate.annotations.BatchSize;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class ChatRoom extends BaseEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private boolean isGroup = false;
+
+ @Column(name = "mentoring_id", unique = true)
+ private Long mentoringId;
+
+ @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL)
+ @BatchSize(size = 10)
+ private final List chatParticipants = new ArrayList<>();
+
+ @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL)
+ private final List chatMessages = new ArrayList<>();
+
+ public ChatRoom(boolean isGroup) {
+ this.isGroup = isGroup;
+ }
+
+ public ChatRoom(Long mentoringId, boolean isGroup) {
+ this.mentoringId = mentoringId;
+ this.isGroup = isGroup;
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java
new file mode 100644
index 000000000..44c11246b
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java
@@ -0,0 +1,17 @@
+package com.example.solidconnection.chat.dto;
+
+import java.time.ZonedDateTime;
+
+public record ChatAttachmentResponse(
+ long id,
+ boolean isImage,
+ String url,
+ String thumbnailUrl,
+ ZonedDateTime createdAt
+) {
+
+ public static ChatAttachmentResponse of(long id, boolean isImage, String url,
+ String thumbnailUrl, ZonedDateTime createdAt) {
+ return new ChatAttachmentResponse(id, isImage, url, thumbnailUrl, createdAt);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java
new file mode 100644
index 000000000..a3728b7fd
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java
@@ -0,0 +1,18 @@
+package com.example.solidconnection.chat.dto;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+
+public record ChatMessageResponse(
+ long id,
+ String content,
+ long senderId,
+ ZonedDateTime createdAt,
+ List attachments
+) {
+
+ public static ChatMessageResponse of(long id, String content, long senderId,
+ ZonedDateTime createdAt, List attachments) {
+ return new ChatMessageResponse(id, content, senderId, createdAt, attachments);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java
new file mode 100644
index 000000000..22d652a35
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java
@@ -0,0 +1,12 @@
+package com.example.solidconnection.chat.dto;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+public record ChatMessageSendRequest(
+ @NotNull(message = "메시지를 입력해주세요.")
+ @Size(max = 500, message = "메시지는 500자를 초과할 수 없습니다")
+ String content
+) {
+
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java
new file mode 100644
index 000000000..065c7ba1c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java
@@ -0,0 +1,19 @@
+package com.example.solidconnection.chat.dto;
+
+import com.example.solidconnection.chat.domain.ChatMessage;
+
+public record ChatMessageSendResponse(
+ long messageId,
+ String content,
+ long senderId
+) {
+
+ public static ChatMessageSendResponse from(ChatMessage chatMessage) {
+ return new ChatMessageSendResponse(
+ chatMessage.getId(),
+ chatMessage.getContent(),
+ chatMessage.getSenderId()
+ );
+ }
+
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java
new file mode 100644
index 000000000..ffa6b9b8c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java
@@ -0,0 +1,12 @@
+package com.example.solidconnection.chat.dto;
+
+public record ChatParticipantResponse(
+ long partnerId,
+ String nickname,
+ String profileUrl
+) {
+
+ public static ChatParticipantResponse of(long partnerId, String nickname, String profileUrl) {
+ return new ChatParticipantResponse(partnerId, nickname, profileUrl);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java
new file mode 100644
index 000000000..add17f1d1
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java
@@ -0,0 +1,12 @@
+package com.example.solidconnection.chat.dto;
+
+import java.util.List;
+
+public record ChatRoomListResponse(
+ List chatRooms
+) {
+
+ public static ChatRoomListResponse of(List chatRooms) {
+ return new ChatRoomListResponse(chatRooms);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java
new file mode 100644
index 000000000..69ec047fb
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java
@@ -0,0 +1,22 @@
+package com.example.solidconnection.chat.dto;
+
+import java.time.ZonedDateTime;
+
+public record ChatRoomResponse(
+ long id,
+ String lastChatMessage,
+ ZonedDateTime lastReceivedTime,
+ ChatParticipantResponse partner,
+ long unReadCount
+) {
+
+ public static ChatRoomResponse of(
+ long id,
+ String lastChatMessage,
+ ZonedDateTime lastReceivedTime,
+ ChatParticipantResponse partner,
+ long unReadCount
+ ) {
+ return new ChatRoomResponse(id, lastChatMessage, lastReceivedTime, partner, unReadCount);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java
new file mode 100644
index 000000000..0d2dd3051
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java
@@ -0,0 +1,8 @@
+package com.example.solidconnection.chat.repository;
+
+import com.example.solidconnection.chat.domain.ChatAttachment;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ChatAttachmentRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java
new file mode 100644
index 000000000..ad0f15630
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java
@@ -0,0 +1,22 @@
+package com.example.solidconnection.chat.repository;
+
+import com.example.solidconnection.chat.domain.ChatMessage;
+import java.util.Optional;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface ChatMessageRepository extends JpaRepository {
+
+ @Query("""
+ SELECT cm FROM ChatMessage cm
+ LEFT JOIN FETCH cm.chatAttachments
+ WHERE cm.chatRoom.id = :roomId
+ ORDER BY cm.createdAt DESC
+ """)
+ Slice findByRoomIdWithPaging(@Param("roomId") long roomId, Pageable pageable);
+
+ Optional findFirstByChatRoomIdOrderByCreatedAtDesc(long chatRoomId);
+}
diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
new file mode 100644
index 000000000..4bce2d08c
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java
@@ -0,0 +1,12 @@
+package com.example.solidconnection.chat.repository;
+
+import com.example.solidconnection.chat.domain.ChatParticipant;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ChatParticipantRepository extends JpaRepository {
+
+ boolean existsByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId);
+
+ Optional findByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId);
+}
diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java
new file mode 100644
index 000000000..5ff82a75b
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java
@@ -0,0 +1,18 @@
+package com.example.solidconnection.chat.repository;
+
+import com.example.solidconnection.chat.domain.ChatReadStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface ChatReadStatusRepository extends JpaRepository {
+
+ @Modifying(clearAutomatically = true, flushAutomatically = true)
+ @Query(value = """
+ INSERT INTO chat_read_status (chat_room_id, chat_participant_id, created_at, updated_at)
+ VALUES (:chatRoomId, :chatParticipantId, NOW(6), NOW(6))
+ ON DUPLICATE KEY UPDATE updated_at = NOW(6)
+ """, nativeQuery = true)
+ void upsertReadStatus(@Param("chatRoomId") long chatRoomId, @Param("chatParticipantId") long chatParticipantId);
+}
diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java
new file mode 100644
index 000000000..8c0b81a4b
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java
@@ -0,0 +1,40 @@
+package com.example.solidconnection.chat.repository;
+
+import com.example.solidconnection.chat.domain.ChatRoom;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface ChatRoomRepository extends JpaRepository {
+
+ @Query("""
+ SELECT cr FROM ChatRoom cr
+ JOIN cr.chatParticipants cp
+ WHERE cp.siteUserId = :userId AND cr.isGroup = false
+ ORDER BY (
+ SELECT MAX(cm.createdAt)
+ FROM ChatMessage cm
+ WHERE cm.chatRoom = cr
+ ) DESC NULLS LAST
+ """)
+ List findOneOnOneChatRoomsByUserId(@Param("userId") long userId);
+
+ @Query("""
+ SELECT COUNT(cm) FROM ChatMessage cm
+ LEFT JOIN ChatReadStatus crs ON crs.chatRoomId = cm.chatRoom.id
+ AND crs.chatParticipantId = (
+ SELECT cp.id FROM ChatParticipant cp
+ WHERE cp.chatRoom.id = :chatRoomId
+ AND cp.siteUserId = :userId
+ )
+ WHERE cm.chatRoom.id = :chatRoomId
+ AND cm.senderId != :userId
+ AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt)
+ """)
+ long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId);
+
+ ChatRoom findByMentoringId(long mentoringId);
+
+ List findAllByMentoringIdIn(List mentoringIds);
+}
diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java
new file mode 100644
index 000000000..ae9be659b
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java
@@ -0,0 +1,193 @@
+package com.example.solidconnection.chat.service;
+
+import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTICIPANT_NOT_FOUND;
+import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE;
+import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
+
+import com.example.solidconnection.chat.domain.ChatMessage;
+import com.example.solidconnection.chat.domain.ChatParticipant;
+import com.example.solidconnection.chat.domain.ChatRoom;
+import com.example.solidconnection.chat.dto.ChatAttachmentResponse;
+import com.example.solidconnection.chat.dto.ChatMessageResponse;
+import com.example.solidconnection.chat.dto.ChatMessageSendRequest;
+import com.example.solidconnection.chat.dto.ChatMessageSendResponse;
+import com.example.solidconnection.chat.dto.ChatParticipantResponse;
+import com.example.solidconnection.chat.dto.ChatRoomListResponse;
+import com.example.solidconnection.chat.dto.ChatRoomResponse;
+import com.example.solidconnection.chat.repository.ChatMessageRepository;
+import com.example.solidconnection.chat.repository.ChatParticipantRepository;
+import com.example.solidconnection.chat.repository.ChatReadStatusRepository;
+import com.example.solidconnection.chat.repository.ChatRoomRepository;
+import com.example.solidconnection.common.dto.SliceResponse;
+import com.example.solidconnection.common.exception.CustomException;
+import com.example.solidconnection.siteuser.domain.SiteUser;
+import com.example.solidconnection.siteuser.repository.SiteUserRepository;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.messaging.simp.SimpMessageSendingOperations;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class ChatService {
+
+ private final ChatRoomRepository chatRoomRepository;
+ private final ChatMessageRepository chatMessageRepository;
+ private final ChatParticipantRepository chatParticipantRepository;
+ private final ChatReadStatusRepository chatReadStatusRepository;
+ private final SiteUserRepository siteUserRepository;
+
+ private final SimpMessageSendingOperations simpMessageSendingOperations;
+
+ public ChatService(ChatRoomRepository chatRoomRepository,
+ ChatMessageRepository chatMessageRepository,
+ ChatParticipantRepository chatParticipantRepository,
+ ChatReadStatusRepository chatReadStatusRepository,
+ SiteUserRepository siteUserRepository,
+ @Lazy SimpMessageSendingOperations simpMessageSendingOperations) {
+ this.chatRoomRepository = chatRoomRepository;
+ this.chatMessageRepository = chatMessageRepository;
+ this.chatParticipantRepository = chatParticipantRepository;
+ this.chatReadStatusRepository = chatReadStatusRepository;
+ this.siteUserRepository = siteUserRepository;
+ this.simpMessageSendingOperations = simpMessageSendingOperations;
+ }
+
+ @Transactional(readOnly = true)
+ public ChatRoomListResponse getChatRooms(long siteUserId) {
+ // todo : n + 1 문제 해결 필요!
+ List chatRooms = chatRoomRepository.findOneOnOneChatRoomsByUserId(siteUserId);
+ List chatRoomInfos = chatRooms.stream()
+ .map(chatRoom -> toChatRoomResponse(chatRoom, siteUserId))
+ .toList();
+ return ChatRoomListResponse.of(chatRoomInfos);
+ }
+
+ private ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, long siteUserId) {
+ Optional latestMessage = chatMessageRepository.findFirstByChatRoomIdOrderByCreatedAtDesc(chatRoom.getId());
+ String lastChatMessage = latestMessage.map(ChatMessage::getContent).orElse("");
+ ZonedDateTime lastReceivedTime = latestMessage.map(ChatMessage::getCreatedAt).orElse(null);
+
+ ChatParticipant partnerParticipant = findPartner(chatRoom, siteUserId);
+
+ SiteUser siteUser = siteUserRepository.findById(partnerParticipant.getSiteUserId())
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ ChatParticipantResponse partner = ChatParticipantResponse.of(siteUser.getId(), siteUser.getNickname(), siteUser.getProfileImageUrl());
+
+ long unReadCount = chatRoomRepository.countUnreadMessages(chatRoom.getId(), siteUserId);
+
+ return ChatRoomResponse.of(chatRoom.getId(), lastChatMessage, lastReceivedTime, partner, unReadCount);
+ }
+
+ @Transactional(readOnly = true)
+ public SliceResponse getChatMessages(long siteUserId, long roomId, Pageable pageable) {
+ validateChatRoomParticipant(siteUserId, roomId);
+
+ Slice chatMessages = chatMessageRepository.findByRoomIdWithPaging(roomId, pageable);
+
+ List content = chatMessages.getContent().stream()
+ .map(this::toChatMessageResponse)
+ .toList();
+
+ return SliceResponse.of(content, chatMessages);
+ }
+
+ @Transactional(readOnly = true)
+ public ChatParticipantResponse getChatPartner(long siteUserId, Long roomId) {
+ ChatRoom chatRoom = chatRoomRepository.findById(roomId)
+ .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE));
+ ChatParticipant partnerParticipant = findPartner(chatRoom, siteUserId);
+ SiteUser siteUser = siteUserRepository.findById(partnerParticipant.getSiteUserId())
+ .orElseThrow(() -> new CustomException(USER_NOT_FOUND));
+ return ChatParticipantResponse.of(siteUser.getId(), siteUser.getNickname(), siteUser.getProfileImageUrl());
+ }
+
+ private ChatParticipant findPartner(ChatRoom chatRoom, long siteUserId) {
+ if (chatRoom.isGroup()) {
+ throw new CustomException(INVALID_CHAT_ROOM_STATE);
+ }
+ return chatRoom.getChatParticipants().stream()
+ .filter(participant -> participant.getSiteUserId() != siteUserId)
+ .findFirst()
+ .orElseThrow(() -> new CustomException(CHAT_PARTNER_NOT_FOUND));
+ }
+
+ public void validateChatRoomParticipant(long siteUserId, long roomId) {
+ boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId);
+ if (!isParticipant) {
+ throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND);
+ }
+ }
+
+ private ChatMessageResponse toChatMessageResponse(ChatMessage message) {
+ List attachments = message.getChatAttachments().stream()
+ .map(attachment -> ChatAttachmentResponse.of(
+ attachment.getId(),
+ attachment.getIsImage(),
+ attachment.getUrl(),
+ attachment.getThumbnailUrl(),
+ attachment.getCreatedAt()
+ ))
+ .toList();
+
+ return ChatMessageResponse.of(
+ message.getId(),
+ message.getContent(),
+ message.getSenderId(),
+ message.getCreatedAt(),
+ attachments
+ );
+ }
+
+ @Transactional
+ public void markChatMessagesAsRead(long siteUserId, long roomId) {
+ ChatParticipant participant = chatParticipantRepository
+ .findByChatRoomIdAndSiteUserId(roomId, siteUserId)
+ .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND));
+
+ chatReadStatusRepository.upsertReadStatus(roomId, participant.getId());
+ }
+
+ @Transactional
+ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long siteUserId, long roomId) {
+ long senderId = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId)
+ .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND))
+ .getId();
+
+ ChatMessage chatMessage = new ChatMessage(
+ chatMessageSendRequest.content(),
+ senderId,
+ chatRoomRepository.findById(roomId)
+ .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE))
+ );
+
+ chatMessageRepository.save(chatMessage);
+
+ ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage);
+
+ simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
+ }
+
+ @Transactional
+ public Long createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) {
+ ChatRoom existingChatRoom = chatRoomRepository.findByMentoringId(mentoringId);
+ if (existingChatRoom != null) {
+ return existingChatRoom.getId();
+ }
+
+ // 새 채팅방 생성
+ ChatRoom chatRoom = new ChatRoom(mentoringId, false);
+ chatRoom = chatRoomRepository.save(chatRoom);
+
+ ChatParticipant mentorParticipant = new ChatParticipant(mentorId, chatRoom);
+ ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom);
+ chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant));
+
+ return chatRoom.getId();
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java b/src/main/java/com/example/solidconnection/common/BaseEntity.java
similarity index 95%
rename from src/main/java/com/example/solidconnection/entity/common/BaseEntity.java
rename to src/main/java/com/example/solidconnection/common/BaseEntity.java
index 508953f88..b3b597350 100644
--- a/src/main/java/com/example/solidconnection/entity/common/BaseEntity.java
+++ b/src/main/java/com/example/solidconnection/common/BaseEntity.java
@@ -1,19 +1,18 @@
-package com.example.solidconnection.entity.common;
+package com.example.solidconnection.common;
+
+import static java.time.ZoneOffset.UTC;
+import static java.time.temporal.ChronoUnit.MICROS;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
+import java.time.ZonedDateTime;
import lombok.Getter;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
-import java.time.ZonedDateTime;
-
-import static java.time.ZoneOffset.UTC;
-import static java.time.temporal.ChronoUnit.MICROS;
-
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
diff --git a/src/main/java/com/example/solidconnection/common/VerifyStatus.java b/src/main/java/com/example/solidconnection/common/VerifyStatus.java
new file mode 100644
index 000000000..d8848c1f0
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/common/VerifyStatus.java
@@ -0,0 +1,9 @@
+package com.example.solidconnection.common;
+
+public enum VerifyStatus {
+
+ PENDING,
+ REJECTED,
+ APPROVED,
+ ;
+}
diff --git a/src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java b/src/main/java/com/example/solidconnection/common/config/client/RestTemplateConfig.java
similarity index 90%
rename from src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java
rename to src/main/java/com/example/solidconnection/common/config/client/RestTemplateConfig.java
index 36ce3f67b..87b43eb28 100644
--- a/src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java
+++ b/src/main/java/com/example/solidconnection/common/config/client/RestTemplateConfig.java
@@ -1,12 +1,11 @@
-package com.example.solidconnection.config.client;
+package com.example.solidconnection.common.config.client;
+import java.time.Duration;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
-import java.time.Duration;
-
@Configuration
public class RestTemplateConfig {
diff --git a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java b/src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java
similarity index 95%
rename from src/main/java/com/example/solidconnection/config/redis/RedisConfig.java
rename to src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java
index 22847dc6d..a59558993 100644
--- a/src/main/java/com/example/solidconnection/config/redis/RedisConfig.java
+++ b/src/main/java/com/example/solidconnection/common/config/redis/RedisConfig.java
@@ -1,4 +1,6 @@
-package com.example.solidconnection.config.redis;
+package com.example.solidconnection.common.config.redis;
+
+import static com.example.solidconnection.community.post.service.RedisConstants.CREATE_CHANNEL;
import com.example.solidconnection.cache.CacheUpdateListener;
import org.springframework.beans.factory.annotation.Value;
@@ -16,8 +18,6 @@
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
-import static com.example.solidconnection.type.RedisConstants.CREATE_CHANNEL;
-
@Configuration
@EnableRedisRepositories
public class RedisConfig {
diff --git a/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java b/src/main/java/com/example/solidconnection/common/config/scheduler/SchedulerConfig.java
similarity index 94%
rename from src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java
rename to src/main/java/com/example/solidconnection/common/config/scheduler/SchedulerConfig.java
index 2a2cfa6a5..227b35372 100644
--- a/src/main/java/com/example/solidconnection/config/scheduler/SchedulerConfig.java
+++ b/src/main/java/com/example/solidconnection/common/config/scheduler/SchedulerConfig.java
@@ -1,4 +1,4 @@
-package com.example.solidconnection.config.scheduler;
+package com.example.solidconnection.common.config.scheduler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
diff --git a/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java b/src/main/java/com/example/solidconnection/common/config/sync/AsyncConfig.java
similarity index 92%
rename from src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java
rename to src/main/java/com/example/solidconnection/common/config/sync/AsyncConfig.java
index 417b040b3..852e3de02 100644
--- a/src/main/java/com/example/solidconnection/config/sync/AsyncConfig.java
+++ b/src/main/java/com/example/solidconnection/common/config/sync/AsyncConfig.java
@@ -1,4 +1,4 @@
-package com.example.solidconnection.config.sync;
+package com.example.solidconnection.common.config.sync;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java
similarity index 70%
rename from src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java
rename to src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java
index 6d16694cc..56bb288e8 100644
--- a/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java
+++ b/src/main/java/com/example/solidconnection/common/config/web/WebMvcConfig.java
@@ -1,28 +1,24 @@
-package com.example.solidconnection.config.web;
+package com.example.solidconnection.common.config.web;
-import com.example.solidconnection.custom.resolver.AuthorizedUserResolver;
-import com.example.solidconnection.custom.resolver.CustomPageableHandlerMethodArgumentResolver;
-import com.example.solidconnection.custom.resolver.ExpiredTokenResolver;
+import com.example.solidconnection.common.resolver.AuthorizedUserResolver;
+import com.example.solidconnection.common.resolver.CustomPageableHandlerMethodArgumentResolver;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-import java.util.List;
-
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final AuthorizedUserResolver authorizedUserResolver;
- private final ExpiredTokenResolver expiredTokenResolver;
private final CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver;
@Override
public void addArgumentResolvers(List resolvers) {
resolvers.addAll(List.of(
authorizedUserResolver,
- expiredTokenResolver,
customPageableHandlerMethodArgumentResolver
));
}
diff --git a/src/main/java/com/example/solidconnection/common/dto/SliceResponse.java b/src/main/java/com/example/solidconnection/common/dto/SliceResponse.java
new file mode 100644
index 000000000..8fe5c80b8
--- /dev/null
+++ b/src/main/java/com/example/solidconnection/common/dto/SliceResponse.java
@@ -0,0 +1,20 @@
+package com.example.solidconnection.common.dto;
+
+import java.util.List;
+import org.springframework.data.domain.Slice;
+
+public record SliceResponse(
+ List content,
+ int nextPageNumber
+) {
+
+ private static final int NO_NEXT_PAGE = -1;
+ private static final int BASE_NUMBER = 1; // 1-based
+
+ public static SliceResponse of(List content, Slice slice) {
+ int nextPageNumber = slice.hasNext()
+ ? slice.getNumber() + BASE_NUMBER + 1
+ : NO_NEXT_PAGE;
+ return new SliceResponse<>(content, nextPageNumber);
+ }
+}
diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java b/src/main/java/com/example/solidconnection/common/exception/CustomAccessDeniedHandler.java
similarity index 90%
rename from src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java
rename to src/main/java/com/example/solidconnection/common/exception/CustomAccessDeniedHandler.java
index 52b1725fc..a391ba66d 100644
--- a/src/main/java/com/example/solidconnection/custom/exception/CustomAccessDeniedHandler.java
+++ b/src/main/java/com/example/solidconnection/common/exception/CustomAccessDeniedHandler.java
@@ -1,16 +1,15 @@
-package com.example.solidconnection.custom.exception;
+package com.example.solidconnection.common.exception;
-import com.example.solidconnection.custom.response.ErrorResponse;
+import com.example.solidconnection.common.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
-import java.io.IOException;
-
@Component
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/common/exception/CustomAuthenticationEntryPoint.java
similarity index 90%
rename from src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java
rename to src/main/java/com/example/solidconnection/common/exception/CustomAuthenticationEntryPoint.java
index 20f0786b7..04b3d5aed 100644
--- a/src/main/java/com/example/solidconnection/custom/exception/CustomAuthenticationEntryPoint.java
+++ b/src/main/java/com/example/solidconnection/common/exception/CustomAuthenticationEntryPoint.java
@@ -1,16 +1,15 @@
-package com.example.solidconnection.custom.exception;
+package com.example.solidconnection.common.exception;
-import com.example.solidconnection.custom.response.ErrorResponse;
+import com.example.solidconnection.common.response.ErrorResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
-import java.io.IOException;
-
@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java b/src/main/java/com/example/solidconnection/common/exception/CustomException.java
similarity index 89%
rename from src/main/java/com/example/solidconnection/custom/exception/CustomException.java
rename to src/main/java/com/example/solidconnection/common/exception/CustomException.java
index 2f1962fbf..fab42924c 100644
--- a/src/main/java/com/example/solidconnection/custom/exception/CustomException.java
+++ b/src/main/java/com/example/solidconnection/common/exception/CustomException.java
@@ -1,4 +1,4 @@
-package com.example.solidconnection.custom.exception;
+package com.example.solidconnection.common.exception;
import lombok.Getter;
diff --git a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java
similarity index 74%
rename from src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java
rename to src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java
index c0c610bce..5700c3044 100644
--- a/src/main/java/com/example/solidconnection/custom/exception/CustomExceptionHandler.java
+++ b/src/main/java/com/example/solidconnection/common/exception/CustomExceptionHandler.java
@@ -1,23 +1,24 @@
-package com.example.solidconnection.custom.exception;
+package com.example.solidconnection.common.exception;
-import com.example.solidconnection.custom.response.ErrorResponse;
+import static com.example.solidconnection.common.exception.ErrorCode.DATA_INTEGRITY_VIOLATION;
+import static com.example.solidconnection.common.exception.ErrorCode.INVALID_INPUT;
+import static com.example.solidconnection.common.exception.ErrorCode.JSON_PARSING_FAILED;
+import static com.example.solidconnection.common.exception.ErrorCode.JWT_EXCEPTION;
+import static com.example.solidconnection.common.exception.ErrorCode.NOT_DEFINED_ERROR;
+
+import com.example.solidconnection.common.response.ErrorResponse;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import io.jsonwebtoken.JwtException;
+import java.util.ArrayList;
+import java.util.List;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_INPUT;
-import static com.example.solidconnection.custom.exception.ErrorCode.JSON_PARSING_FAILED;
-import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION;
-import static com.example.solidconnection.custom.exception.ErrorCode.NOT_DEFINED_ERROR;
-
@Slf4j
@ControllerAdvice
public class CustomExceptionHandler {
@@ -56,6 +57,15 @@ public ResponseEntity handleValidationExceptions(MethodArgumentNo
.body(errorResponse);
}
+ @ExceptionHandler(DataIntegrityViolationException.class)
+ public ResponseEntity