diff --git a/.gitignore b/.gitignore index 271c67a4..4275ec45 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,4 @@ out/ /.nb-gradle/ ### VS Code ### -.vscode/ - -### QueryDSL ### -src/main/generated/ +.vscode/ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 14b3b484..08df474a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,9 @@ dependencies { // jpa implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // security implementation 'org.springframework.boot:spring-boot-starter-security' @@ -89,25 +92,6 @@ dependencies { implementation 'io.sentry:sentry-spring-boot-starter-jakarta:8.5.0' } -def generatedQueryDsl = 'src/main/generated' - -sourceSets { - main.java.srcDirs += [generatedQueryDsl] -} - -tasks.withType(JavaCompile).configureEach { - options.getGeneratedSourceOutputDirectory().set(file(generatedQueryDsl)) -} - -clean.doLast { - file(generatedQueryDsl).deleteDir() -} - -tasks.named('test') { - useJUnitPlatform() -} - - jib { from { image = 'openjdk:21' @@ -120,4 +104,3 @@ jib { creationTime = 'USE_CURRENT_TIMESTAMP' } } - diff --git a/docs/infra/aws-elasticache-redis-local-setup.md b/docs/infra/aws-elasticache-redis-local-setup.md new file mode 100644 index 00000000..2c877759 --- /dev/null +++ b/docs/infra/aws-elasticache-redis-local-setup.md @@ -0,0 +1,122 @@ +# Redis 로컬 개발 환경 접속 가이드 (with AWS ElastiCache) + +> 본 문서는 **VPC 내 ElastiCache Redis**에 대해, 로컬 개발 환경에서도 운영 환경과 동일한 방식으로 접근할 수 있도록 포트 포워딩 기반 개발 흐름을 정리한 가이드입니다. +> Bastion Host를 별도로 구성하지 않고, **기존 EC2 인스턴스를 SSM 포워딩 노드로 활용**합니다. + +--- + +## ✅ 개요 + +| 항목 | 내용 | +|-------------|-------------------------------------------------------| +| 대상 Redis | AWS ElastiCache for Redis (Private Subnet) | +| 접근 방식 | AWS Systems Manager - `PortForwardingSession` 사용 | +| 중계 노드 | 동일 VPC 내 EC2 인스턴스 (SSM Agent 연결 상태 필요) | + +--- + +## 1. 요구 사항 + +### 1.1 사전 조건 + +- AWS CLI 설치 및 `configure` 완료 +- EC2 인스턴스에 **SSM Agent 설치 + IAM Role 연결**되어 있어야 함 +- Redis와 EC2는 동일 VPC/Subnet 내 존재 +- Redis 보안 그룹에 EC2 인스턴스 허용 설정 + +--- + +## 2. 설정 단계 + +### 2.1 AWS CLI 인증 구성 + +```bash +aws configure --profile dev-redis +``` + +- Access Key, Secret, Region 입력 +- 사용 목적에 맞게 별도 프로파일 구성 권장 + +--- + +### 2.2 EC2 인스턴스를 통한 포트 포워딩 + +1. EC2 인스턴스 ID 확인 (`i-xxxxxxxxxxxxxxxxx`) +2. SSM 포트 포워딩 세션 실행: + +```bash +aws ssm start-session \ + --target i-xxxxxxxxxxxxxxxxx \ + --document-name AWS-StartPortForwardingSession \ + --parameters '{"portNumber":["6379"],"localPortNumber":["6379"]}' \ + --profile dev-redis +``` + +> 이 세션이 유지되는 동안 `localhost:6379`는 EC2 내부 Redis 포트에 직접 연결된 것과 동일하게 동작합니다. + +--- + +### 2.3 연결 확인 + +```bash +valkey-cli --tls -h localhost -p 6379 ping +``` + +정상적으로 `PONG` 응답이 오면 연결 성공입니다. + +--- + +### 2.4 Spring Boot 환경 구성 예시 + +```yaml +spring: + data: + redis: + host: localhost + port: 6379 + ssl: + enabled: true +``` + +- 운영/로컬 환경 모두 동일 구성 사용 +- 운영에서는 EC2 → Redis 직접 연결 +- 로컬에서는 포트포워딩 세션을 통해 동일 흐름 유지 + +--- + +## 3. Redis 연결 트러블슈팅 + +### 3.1 systemd 기반 socat 포워딩 관리 (옵션) + +```bash +sudo systemctl daemon-reexec +sudo systemctl daemon-reload +sudo systemctl enable socat-redis +sudo systemctl start socat-redis +sudo systemctl status socat-redis +``` + +- 서비스 로그 확인: + +```bash +journalctl -u socat-redis +``` + +--- + +## 4. 자주 발생하는 이슈 + +| 증상 | 원인 및 해결 방안 | +|----------------------------------|------------------------------------------------------------------------------------| +| `Timeout` 또는 연결 안됨 | - SSM 세션이 종료되었거나
- Redis 보안 그룹에서 EC2 인바운드 허용 누락 | +| 포워딩 명령어 실행 시 오류 발생 | - EC2에 SSM Agent 미설치
- IAM Role에 `ssm:StartSession` 권한 미설정
- AWS CLI 인증 오류 | +| 데이터가 깨져 보임 | - Redis 클러스터 모드 사용 중
- Lettuce 클라이언트 설정을 클러스터 대응으로 변경 필요 | + +--- + +## 📎 참고 자료 + +- [AWS Blog - Port Forwarding with SSM to ElastiCache Redis](https://aws.amazon.com/blogs/mt/aws-systems-manager-session-manager-port-forwarding-to-amazon-elasticache-redis-inside-private-subnet/) +- [PR #139](https://github.com/juulabel/juulabel-back/pull/141): 인증 전략 개선 및 Redis 기반 세션 관리 적용 상세 내역 + +--- \ No newline at end of file diff --git a/docs/pr/PR-139-refactor---auth-api.md b/docs/pr/PR-139-refactor---auth-api.md new file mode 100644 index 00000000..1a45c560 --- /dev/null +++ b/docs/pr/PR-139-refactor---auth-api.md @@ -0,0 +1,96 @@ +# Auth API 리팩터링 및 인증 전략 고도화 (PR [#139](https://github.com/juulabel/juulabel-back/pull/141)) + +## 📌 Summary + +이 PR은 인증 모듈을 보안 중심의 구조로 리디자인하고, 유지보수성 및 확장성을 고려한 API 명세 리팩터링을 포함합니다. 주요 목표는 다음과 같습니다: + +- 인증 API 도메인의 **명확한 경계 설정** +- **Refresh Token Rotation** 전략 기반의 인증 안정성 확보 +- **서버 측 세션 관리**로 클라이언트 신뢰 수준 최소화 +- **비정상 로그인 탐지 기반 확장**을 고려한 로깅 구조 설계 + +--- + +## 1. 구조 리팩터링: 인증 도메인 책임 분리 + +기존 API는 `/members` 하위에 인증과 사용자 관리 로직이 혼재되어 있어, 도메인 분리에 따른 유지보수 비용이 컸습니다. 다음과 같이 명확히 분리합니다: + +| 기존 경로 | 신규 경로 | 목적 | +| ------------------------- | ---------------------- | ---------------------------- | +| `/v1/api/members/login` | `/v1/api/auth/login` | 인증 도메인 분리 | +| `/v1/api/members/sign-up` | `/v1/api/auth/sign-up` | | +| `/v1/api/members/me` | `/v1/api/auth/me` | | +| _(신규)_ | `/v1/api/auth/refresh` | Refresh Token 재발급 | +| _(신규)_ | `/v1/api/auth/logout` | 서버 측 로그아웃 (세션 종료) | + +💡 **Outcome:** 인증 흐름과 사용자 정보 흐름의 경계가 명확해져 API 소비자 및 테스트 범위가 선명해집니다. + +--- + +## 2. Refresh Token 기반 인증 및 Rotation 전략 + +### Why Rotation? + +토큰 도난 시, 고정 Refresh Token 구조는 **세션 탈취 리스크**를 증가시킵니다. 이에 따라 Rotation 전략을 적용합니다. + +### 동작 방식 + +- Access Token 만료 시 `/auth/refresh` 호출 → 새 Access + Refresh Token 응답 +- 이전 Refresh Token은 **즉시 폐기** 및 Redis 블랙리스트 등록 +- 동일 토큰 재사용 시 → 인증 실패 (401) + +💡 **보안 장점:** 사용된 토큰은 재사용 불가 → 리플레이 공격 방지 강화 + +--- + +## 3. 비정상 로그인 탐지 기반 확장 고려 + +### 수집 항목 + +- `Device-Id` (필수 헤더) +- User-Agent, IP (서버 로그 자동 수집) + +이 정보는 향후 다음 기능에 활용됩니다: + +- 동일 계정 다중 위치/디바이스 로그인 탐지 +- 의심 활동에 대한 보안 알림 트리거 +- 로그인 히스토리 시각화 + +💡 **시사점:** 인증은 단일 절차가 아닌 보안 트래픽의 출발점이며, 메타데이터 수집이 이후 기능 확장의 기반이 됩니다. + +--- + +## 4. 로그아웃: 서버 중심 세션 종료 방식으로 전환 + +기존 구조는 클라이언트 측에서 Access Token 제거만으로 로그아웃 처리하였습니다. +새로운 구조에서는 명시적 로그아웃 API 호출로 다음 동작 수행: + +- Redis에 등록된 Refresh Token을 블랙리스트화 +- 이후 해당 토큰 사용 시 인증 실패 + +💡 **효과:** 토큰 재사용 방지 → 클라이언트 신뢰도 최소화 + +--- + +## 5. Redis 기반 토큰 관리 및 인프라 구성 + +| 항목 | 내용 | +| ----------- | ---------------------------------------------------- | +| 저장소 구성 | AWS ElastiCache (Valkey) | +| 접근 방식 | VPC 내부 `socat + SSM 포트포워딩` 기반 접속 | +| 라이브러리 | `spring-data-redis (lettuce)` | +| 관리 전략 | TTL 기반 자동 만료 + Lua Script 기반 블랙리스트 삽입 | + +💡 **운영 이점:** Redis는 고성능 키-밸류 스토어로써 세션 상태 관리에 적합하며, Lua Script로 atomic 블랙리스트 처리 가능 + +--- + +## 6. 적용 시 유의사항 + +| 항목 | 설명 | +| -------------------------------------------- | --------------------------------------------------------- | +| `Device-Id` 누락 시 400 반환 | 모든 인증 요청 시 필수 포함 필요 | +| `/auth/logout` 미호출 시 Refresh 무효화 누락 | 클라이언트에서만 로그아웃 처리 시 토큰은 유효 상태 유지됨 | +| `/members/*` 인증 경로 사용 중단 | 호출 시 404 응답 발생 가능성 있음. 즉시 경로 전환 필요 | + +--- diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholType.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholType.java deleted file mode 100644 index 282d8036..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholType.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholType is a Querydsl query type for AlcoholType - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholType extends EntityPathBase { - - private static final long serialVersionUID = -1838556943L; - - public static final QAlcoholType alcoholType = new QAlcoholType("alcoholType"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath image = createString("image"); - - public final StringPath name = createString("name"); - - public final ListPath tastingNotes = this.createList("tastingNotes", com.juu.juulabel.tastingnote.domain.TastingNote.class, com.juu.juulabel.tastingnote.domain.QTastingNote.class, PathInits.DIRECT2); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QAlcoholType(String variable) { - super(AlcoholType.class, forVariable(variable)); - } - - public QAlcoholType(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QAlcoholType(PathMetadata metadata) { - super(AlcoholType.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeColor.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeColor.java deleted file mode 100644 index 3a63c2de..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeColor.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholTypeColor is a Querydsl query type for AlcoholTypeColor - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholTypeColor extends EntityPathBase { - - private static final long serialVersionUID = -1121889038L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholTypeColor alcoholTypeColor = new QAlcoholTypeColor("alcoholTypeColor"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final QAlcoholType alcoholType; - - public final QColor color; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isUsed = createBoolean("isUsed"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QAlcoholTypeColor(String variable) { - this(AlcoholTypeColor.class, forVariable(variable), INITS); - } - - public QAlcoholTypeColor(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholTypeColor(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholTypeColor(PathMetadata metadata, PathInits inits) { - this(AlcoholTypeColor.class, metadata, inits); - } - - public QAlcoholTypeColor(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholType = inits.isInitialized("alcoholType") ? new QAlcoholType(forProperty("alcoholType")) : null; - this.color = inits.isInitialized("color") ? new QColor(forProperty("color")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeFlavor.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeFlavor.java deleted file mode 100644 index 8f937cc3..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeFlavor.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholTypeFlavor is a Querydsl query type for AlcoholTypeFlavor - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholTypeFlavor extends EntityPathBase { - - private static final long serialVersionUID = -336025873L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholTypeFlavor alcoholTypeFlavor = new QAlcoholTypeFlavor("alcoholTypeFlavor"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final QAlcoholType alcoholType; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final QFlavor flavor; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isUsed = createBoolean("isUsed"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QAlcoholTypeFlavor(String variable) { - this(AlcoholTypeFlavor.class, forVariable(variable), INITS); - } - - public QAlcoholTypeFlavor(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholTypeFlavor(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholTypeFlavor(PathMetadata metadata, PathInits inits) { - this(AlcoholTypeFlavor.class, metadata, inits); - } - - public QAlcoholTypeFlavor(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholType = inits.isInitialized("alcoholType") ? new QAlcoholType(forProperty("alcoholType")) : null; - this.flavor = inits.isInitialized("flavor") ? new QFlavor(forProperty("flavor")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeScent.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeScent.java deleted file mode 100644 index bf18c0a3..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeScent.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholTypeScent is a Querydsl query type for AlcoholTypeScent - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholTypeScent extends EntityPathBase { - - private static final long serialVersionUID = -1107476950L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholTypeScent alcoholTypeScent = new QAlcoholTypeScent("alcoholTypeScent"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final QAlcoholType alcoholType; - - public final com.juu.juulabel.category.domain.QCategory category; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isUsed = createBoolean("isUsed"); - - public final QScent scent; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QAlcoholTypeScent(String variable) { - this(AlcoholTypeScent.class, forVariable(variable), INITS); - } - - public QAlcoholTypeScent(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholTypeScent(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholTypeScent(PathMetadata metadata, PathInits inits) { - this(AlcoholTypeScent.class, metadata, inits); - } - - public QAlcoholTypeScent(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholType = inits.isInitialized("alcoholType") ? new QAlcoholType(forProperty("alcoholType")) : null; - this.category = inits.isInitialized("category") ? new com.juu.juulabel.category.domain.QCategory(forProperty("category"), inits.get("category")) : null; - this.scent = inits.isInitialized("scent") ? new QScent(forProperty("scent")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeSensory.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeSensory.java deleted file mode 100644 index 1fb621c5..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholTypeSensory.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholTypeSensory is a Querydsl query type for AlcoholTypeSensory - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholTypeSensory extends EntityPathBase { - - private static final long serialVersionUID = 932258254L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholTypeSensory alcoholTypeSensory = new QAlcoholTypeSensory("alcoholTypeSensory"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final QAlcoholType alcoholType; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isUsed = createBoolean("isUsed"); - - public final QSensory sensory; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QAlcoholTypeSensory(String variable) { - this(AlcoholTypeSensory.class, forVariable(variable), INITS); - } - - public QAlcoholTypeSensory(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholTypeSensory(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholTypeSensory(PathMetadata metadata, PathInits inits) { - this(AlcoholTypeSensory.class, metadata, inits); - } - - public QAlcoholTypeSensory(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholType = inits.isInitialized("alcoholType") ? new QAlcoholType(forProperty("alcoholType")) : null; - this.sensory = inits.isInitialized("sensory") ? new QSensory(forProperty("sensory")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinks.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinks.java deleted file mode 100644 index 9fb03969..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinks.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholicDrinks is a Querydsl query type for AlcoholicDrinks - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholicDrinks extends EntityPathBase { - - private static final long serialVersionUID = -695932244L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholicDrinks alcoholicDrinks = new QAlcoholicDrinks("alcoholicDrinks"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final NumberPath alcoholContent = createNumber("alcoholContent", Double.class); - - public final QAlcoholType alcoholType; - - public final QBrewery brewery; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final StringPath description = createString("description"); - - public final NumberPath discountPrice = createNumber("discountPrice", Integer.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath image = createString("image"); - - public final StringPath name = createString("name"); - - public final NumberPath rating = createNumber("rating", Double.class); - - public final NumberPath regularPrice = createNumber("regularPrice", Integer.class); - - public final NumberPath tastingNoteCount = createNumber("tastingNoteCount", Integer.class); - - public final ListPath tastingNotes = this.createList("tastingNotes", com.juu.juulabel.tastingnote.domain.TastingNote.class, com.juu.juulabel.tastingnote.domain.QTastingNote.class, PathInits.DIRECT2); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public final NumberPath volume = createNumber("volume", Integer.class); - - public QAlcoholicDrinks(String variable) { - this(AlcoholicDrinks.class, forVariable(variable), INITS); - } - - public QAlcoholicDrinks(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholicDrinks(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholicDrinks(PathMetadata metadata, PathInits inits) { - this(AlcoholicDrinks.class, metadata, inits); - } - - public QAlcoholicDrinks(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholType = inits.isInitialized("alcoholType") ? new QAlcoholType(forProperty("alcoholType")) : null; - this.brewery = inits.isInitialized("brewery") ? new QBrewery(forProperty("brewery")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksIngredient.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksIngredient.java deleted file mode 100644 index aa82b6f3..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksIngredient.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholicDrinksIngredient is a Querydsl query type for AlcoholicDrinksIngredient - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholicDrinksIngredient extends EntityPathBase { - - private static final long serialVersionUID = 18141597L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholicDrinksIngredient alcoholicDrinksIngredient = new QAlcoholicDrinksIngredient("alcoholicDrinksIngredient"); - - public final QAlcoholicDrinks alcoholicDrinks; - - public final NumberPath id = createNumber("id", Long.class); - - public final QIngredient ingredient; - - public QAlcoholicDrinksIngredient(String variable) { - this(AlcoholicDrinksIngredient.class, forVariable(variable), INITS); - } - - public QAlcoholicDrinksIngredient(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholicDrinksIngredient(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholicDrinksIngredient(PathMetadata metadata, PathInits inits) { - this(AlcoholicDrinksIngredient.class, metadata, inits); - } - - public QAlcoholicDrinksIngredient(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholicDrinks = inits.isInitialized("alcoholicDrinks") ? new QAlcoholicDrinks(forProperty("alcoholicDrinks"), inits.get("alcoholicDrinks")) : null; - this.ingredient = inits.isInitialized("ingredient") ? new QIngredient(forProperty("ingredient")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksPairing.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksPairing.java deleted file mode 100644 index ebd4ef89..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QAlcoholicDrinksPairing.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QAlcoholicDrinksPairing is a Querydsl query type for AlcoholicDrinksPairing - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QAlcoholicDrinksPairing extends EntityPathBase { - - private static final long serialVersionUID = 1589617276L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QAlcoholicDrinksPairing alcoholicDrinksPairing = new QAlcoholicDrinksPairing("alcoholicDrinksPairing"); - - public final QAlcoholicDrinks alcoholicDrinks; - - public final NumberPath id = createNumber("id", Long.class); - - public final QPairing pairing; - - public QAlcoholicDrinksPairing(String variable) { - this(AlcoholicDrinksPairing.class, forVariable(variable), INITS); - } - - public QAlcoholicDrinksPairing(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QAlcoholicDrinksPairing(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QAlcoholicDrinksPairing(PathMetadata metadata, PathInits inits) { - this(AlcoholicDrinksPairing.class, metadata, inits); - } - - public QAlcoholicDrinksPairing(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholicDrinks = inits.isInitialized("alcoholicDrinks") ? new QAlcoholicDrinks(forProperty("alcoholicDrinks"), inits.get("alcoholicDrinks")) : null; - this.pairing = inits.isInitialized("pairing") ? new QPairing(forProperty("pairing")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QBrewery.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QBrewery.java deleted file mode 100644 index c019ef47..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QBrewery.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QBrewery is a Querydsl query type for Brewery - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QBrewery extends EntityPathBase { - - private static final long serialVersionUID = -570077389L; - - public static final QBrewery brewery = new QBrewery("brewery"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath message = createString("message"); - - public final StringPath name = createString("name"); - - public final StringPath region = createString("region"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QBrewery(String variable) { - super(Brewery.class, forVariable(variable)); - } - - public QBrewery(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QBrewery(PathMetadata metadata) { - super(Brewery.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QColor.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QColor.java deleted file mode 100644 index 4af4be86..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QColor.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QColor is a Querydsl query type for Color - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QColor extends EntityPathBase { - - private static final long serialVersionUID = 2047172524L; - - public static final QColor color = new QColor("color"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public final StringPath rgb = createString("rgb"); - - public QColor(String variable) { - super(Color.class, forVariable(variable)); - } - - public QColor(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QColor(PathMetadata metadata) { - super(Color.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QFlavor.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QFlavor.java deleted file mode 100644 index 32170085..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QFlavor.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QFlavor is a Querydsl query type for Flavor - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QFlavor extends EntityPathBase { - - private static final long serialVersionUID = -879365259L; - - public static final QFlavor flavor = new QFlavor("flavor"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public QFlavor(String variable) { - super(Flavor.class, forVariable(variable)); - } - - public QFlavor(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QFlavor(PathMetadata metadata) { - super(Flavor.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QFlavorLevel.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QFlavorLevel.java deleted file mode 100644 index 323b9c1b..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QFlavorLevel.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QFlavorLevel is a Querydsl query type for FlavorLevel - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QFlavorLevel extends EntityPathBase { - - private static final long serialVersionUID = -1624270577L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QFlavorLevel flavorLevel = new QFlavorLevel("flavorLevel"); - - public final StringPath description = createString("description"); - - public final QFlavor flavor; - - public final NumberPath id = createNumber("id", Long.class); - - public final NumberPath score = createNumber("score", Integer.class); - - public QFlavorLevel(String variable) { - this(FlavorLevel.class, forVariable(variable), INITS); - } - - public QFlavorLevel(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QFlavorLevel(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QFlavorLevel(PathMetadata metadata, PathInits inits) { - this(FlavorLevel.class, metadata, inits); - } - - public QFlavorLevel(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.flavor = inits.isInitialized("flavor") ? new QFlavor(forProperty("flavor")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QIngredient.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QIngredient.java deleted file mode 100644 index fac512c0..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QIngredient.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QIngredient is a Querydsl query type for Ingredient - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QIngredient extends EntityPathBase { - - private static final long serialVersionUID = -280754392L; - - public static final QIngredient ingredient = new QIngredient("ingredient"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public QIngredient(String variable) { - super(Ingredient.class, forVariable(variable)); - } - - public QIngredient(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QIngredient(PathMetadata metadata) { - super(Ingredient.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QPairing.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QPairing.java deleted file mode 100644 index 2e8851db..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QPairing.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QPairing is a Querydsl query type for Pairing - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QPairing extends EntityPathBase { - - private static final long serialVersionUID = -1513074479L; - - public static final QPairing pairing = new QPairing("pairing"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public QPairing(String variable) { - super(Pairing.class, forVariable(variable)); - } - - public QPairing(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QPairing(PathMetadata metadata) { - super(Pairing.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QScent.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QScent.java deleted file mode 100644 index bdfeb3be..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QScent.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QScent is a Querydsl query type for Scent - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QScent extends EntityPathBase { - - private static final long serialVersionUID = 2061584612L; - - public static final QScent scent = new QScent("scent"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public QScent(String variable) { - super(Scent.class, forVariable(variable)); - } - - public QScent(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QScent(PathMetadata metadata) { - super(Scent.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QSensory.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QSensory.java deleted file mode 100644 index 6f31b88e..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QSensory.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QSensory is a Querydsl query type for Sensory - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QSensory extends EntityPathBase { - - private static final long serialVersionUID = 1268606472L; - - public static final QSensory sensory = new QSensory("sensory"); - - public final StringPath description = createString("description"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath name = createString("name"); - - public QSensory(String variable) { - super(Sensory.class, forVariable(variable)); - } - - public QSensory(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QSensory(PathMetadata metadata) { - super(Sensory.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/alcohol/domain/QSensoryLevel.java b/src/main/generated/com/juu/juulabel/alcohol/domain/QSensoryLevel.java deleted file mode 100644 index d5869b82..00000000 --- a/src/main/generated/com/juu/juulabel/alcohol/domain/QSensoryLevel.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.juu.juulabel.alcohol.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QSensoryLevel is a Querydsl query type for SensoryLevel - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QSensoryLevel extends EntityPathBase { - - private static final long serialVersionUID = -1898460580L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QSensoryLevel sensoryLevel = new QSensoryLevel("sensoryLevel"); - - public final StringPath description = createString("description"); - - public final NumberPath id = createNumber("id", Long.class); - - public final NumberPath score = createNumber("score", Integer.class); - - public final QSensory sensory; - - public QSensoryLevel(String variable) { - this(SensoryLevel.class, forVariable(variable), INITS); - } - - public QSensoryLevel(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QSensoryLevel(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QSensoryLevel(PathMetadata metadata, PathInits inits) { - this(SensoryLevel.class, metadata, inits); - } - - public QSensoryLevel(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.sensory = inits.isInitialized("sensory") ? new QSensory(forProperty("sensory")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/category/domain/QCategory.java b/src/main/generated/com/juu/juulabel/category/domain/QCategory.java deleted file mode 100644 index 348abe78..00000000 --- a/src/main/generated/com/juu/juulabel/category/domain/QCategory.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.juu.juulabel.category.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QCategory is a Querydsl query type for Category - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QCategory extends EntityPathBase { - - private static final long serialVersionUID = -321007669L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QCategory category = new QCategory("category"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final ListPath children = this.createList("children", Category.class, QCategory.class, PathInits.DIRECT2); - - public final StringPath code = createString("code"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isUsed = createBoolean("isUsed"); - - public final StringPath name = createString("name"); - - public final QCategory parent; - - public final EnumPath type = createEnum("type", CategoryType.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QCategory(String variable) { - this(Category.class, forVariable(variable), INITS); - } - - public QCategory(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QCategory(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QCategory(PathMetadata metadata, PathInits inits) { - this(Category.class, metadata, inits); - } - - public QCategory(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.parent = inits.isInitialized("parent") ? new QCategory(forProperty("parent"), inits.get("parent")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/common/base/QBaseCreatedTimeEntity.java b/src/main/generated/com/juu/juulabel/common/base/QBaseCreatedTimeEntity.java deleted file mode 100644 index 24e59f44..00000000 --- a/src/main/generated/com/juu/juulabel/common/base/QBaseCreatedTimeEntity.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.juu.juulabel.common.base; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QBaseCreatedTimeEntity is a Querydsl query type for BaseCreatedTimeEntity - */ -@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") -public class QBaseCreatedTimeEntity extends EntityPathBase { - - private static final long serialVersionUID = -1233772198L; - - public static final QBaseCreatedTimeEntity baseCreatedTimeEntity = new QBaseCreatedTimeEntity("baseCreatedTimeEntity"); - - public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); - - public QBaseCreatedTimeEntity(String variable) { - super(BaseCreatedTimeEntity.class, forVariable(variable)); - } - - public QBaseCreatedTimeEntity(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QBaseCreatedTimeEntity(PathMetadata metadata) { - super(BaseCreatedTimeEntity.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/common/base/QBaseTimeEntity.java b/src/main/generated/com/juu/juulabel/common/base/QBaseTimeEntity.java deleted file mode 100644 index acf26e86..00000000 --- a/src/main/generated/com/juu/juulabel/common/base/QBaseTimeEntity.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.juu.juulabel.common.base; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QBaseTimeEntity is a Querydsl query type for BaseTimeEntity - */ -@Generated("com.querydsl.codegen.DefaultSupertypeSerializer") -public class QBaseTimeEntity extends EntityPathBase { - - private static final long serialVersionUID = 282798094L; - - public static final QBaseTimeEntity baseTimeEntity = new QBaseTimeEntity("baseTimeEntity"); - - public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); - - public final DateTimePath updatedAt = createDateTime("updatedAt", java.time.LocalDateTime.class); - - public QBaseTimeEntity(String variable) { - super(BaseTimeEntity.class, forVariable(variable)); - } - - public QBaseTimeEntity(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QBaseTimeEntity(PathMetadata metadata) { - super(BaseTimeEntity.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLife.java b/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLife.java deleted file mode 100644 index d1d41961..00000000 --- a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLife.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.juu.juulabel.dailylife.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QDailyLife is a Querydsl query type for DailyLife - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QDailyLife extends EntityPathBase { - - private static final long serialVersionUID = 153685271L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QDailyLife dailyLife = new QDailyLife("dailyLife"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final StringPath content = createString("content"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isPrivate = createBoolean("isPrivate"); - - public final com.juu.juulabel.member.domain.QMember member; - - public final StringPath title = createString("title"); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QDailyLife(String variable) { - this(DailyLife.class, forVariable(variable), INITS); - } - - public QDailyLife(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QDailyLife(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QDailyLife(PathMetadata metadata, PathInits inits) { - this(DailyLife.class, metadata, inits); - } - - public QDailyLife(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeComment.java b/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeComment.java deleted file mode 100644 index 37b0cb45..00000000 --- a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeComment.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.juu.juulabel.dailylife.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QDailyLifeComment is a Querydsl query type for DailyLifeComment - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QDailyLifeComment extends EntityPathBase { - - private static final long serialVersionUID = 1729618248L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QDailyLifeComment dailyLifeComment = new QDailyLifeComment("dailyLifeComment"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final StringPath content = createString("content"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final QDailyLife dailyLife; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.member.domain.QMember member; - - public final QDailyLifeComment parent; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QDailyLifeComment(String variable) { - this(DailyLifeComment.class, forVariable(variable), INITS); - } - - public QDailyLifeComment(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QDailyLifeComment(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QDailyLifeComment(PathMetadata metadata, PathInits inits) { - this(DailyLifeComment.class, metadata, inits); - } - - public QDailyLifeComment(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.dailyLife = inits.isInitialized("dailyLife") ? new QDailyLife(forProperty("dailyLife"), inits.get("dailyLife")) : null; - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - this.parent = inits.isInitialized("parent") ? new QDailyLifeComment(forProperty("parent"), inits.get("parent")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeCommentLike.java b/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeCommentLike.java deleted file mode 100644 index 07fa1453..00000000 --- a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeCommentLike.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.juu.juulabel.dailylife.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QDailyLifeCommentLike is a Querydsl query type for DailyLifeCommentLike - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QDailyLifeCommentLike extends EntityPathBase { - - private static final long serialVersionUID = 1784291583L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QDailyLifeCommentLike dailyLifeCommentLike = new QDailyLifeCommentLike("dailyLifeCommentLike"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final QDailyLifeComment dailyLifeComment; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.member.domain.QMember member; - - public QDailyLifeCommentLike(String variable) { - this(DailyLifeCommentLike.class, forVariable(variable), INITS); - } - - public QDailyLifeCommentLike(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QDailyLifeCommentLike(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QDailyLifeCommentLike(PathMetadata metadata, PathInits inits) { - this(DailyLifeCommentLike.class, metadata, inits); - } - - public QDailyLifeCommentLike(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.dailyLifeComment = inits.isInitialized("dailyLifeComment") ? new QDailyLifeComment(forProperty("dailyLifeComment"), inits.get("dailyLifeComment")) : null; - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeImage.java b/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeImage.java deleted file mode 100644 index 384dfacf..00000000 --- a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeImage.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.juu.juulabel.dailylife.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QDailyLifeImage is a Querydsl query type for DailyLifeImage - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QDailyLifeImage extends EntityPathBase { - - private static final long serialVersionUID = -1561443708L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QDailyLifeImage dailyLifeImage = new QDailyLifeImage("dailyLifeImage"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final QDailyLife dailyLife; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath imagePath = createString("imagePath"); - - public final NumberPath seq = createNumber("seq", Integer.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QDailyLifeImage(String variable) { - this(DailyLifeImage.class, forVariable(variable), INITS); - } - - public QDailyLifeImage(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QDailyLifeImage(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QDailyLifeImage(PathMetadata metadata, PathInits inits) { - this(DailyLifeImage.class, metadata, inits); - } - - public QDailyLifeImage(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.dailyLife = inits.isInitialized("dailyLife") ? new QDailyLife(forProperty("dailyLife"), inits.get("dailyLife")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeLike.java b/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeLike.java deleted file mode 100644 index bd03a5b0..00000000 --- a/src/main/generated/com/juu/juulabel/dailylife/domain/QDailyLifeLike.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.juu.juulabel.dailylife.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QDailyLifeLike is a Querydsl query type for DailyLifeLike - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QDailyLifeLike extends EntityPathBase { - - private static final long serialVersionUID = 88264014L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QDailyLifeLike dailyLifeLike = new QDailyLifeLike("dailyLifeLike"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final QDailyLife dailyLife; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.member.domain.QMember member; - - public QDailyLifeLike(String variable) { - this(DailyLifeLike.class, forVariable(variable), INITS); - } - - public QDailyLifeLike(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QDailyLifeLike(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QDailyLifeLike(PathMetadata metadata, PathInits inits) { - this(DailyLifeLike.class, metadata, inits); - } - - public QDailyLifeLike(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.dailyLife = inits.isInitialized("dailyLife") ? new QDailyLife(forProperty("dailyLife"), inits.get("dailyLife")) : null; - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/follow/domain/QFollow.java b/src/main/generated/com/juu/juulabel/follow/domain/QFollow.java deleted file mode 100644 index 74b41dc6..00000000 --- a/src/main/generated/com/juu/juulabel/follow/domain/QFollow.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.juu.juulabel.follow.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QFollow is a Querydsl query type for Follow - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QFollow extends EntityPathBase { - - private static final long serialVersionUID = 1222043633L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QFollow follow = new QFollow("follow"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath followedAt = createDateTime("followedAt", java.time.LocalDateTime.class); - - public final com.juu.juulabel.member.domain.QMember followee; - - public final com.juu.juulabel.member.domain.QMember follower; - - public final NumberPath id = createNumber("id", Long.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QFollow(String variable) { - this(Follow.class, forVariable(variable), INITS); - } - - public QFollow(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QFollow(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QFollow(PathMetadata metadata, PathInits inits) { - this(Follow.class, metadata, inits); - } - - public QFollow(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.followee = inits.isInitialized("followee") ? new com.juu.juulabel.member.domain.QMember(forProperty("followee")) : null; - this.follower = inits.isInitialized("follower") ? new com.juu.juulabel.member.domain.QMember(forProperty("follower")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/member/domain/QMember.java b/src/main/generated/com/juu/juulabel/member/domain/QMember.java deleted file mode 100644 index b81b2a59..00000000 --- a/src/main/generated/com/juu/juulabel/member/domain/QMember.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.juu.juulabel.member.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QMember is a Querydsl query type for Member - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QMember extends EntityPathBase { - - private static final long serialVersionUID = -1376470525L; - - public static final QMember member = new QMember("member1"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final StringPath email = createString("email"); - - public final EnumPath gender = createEnum("gender", Gender.class); - - public final BooleanPath hasBadge = createBoolean("hasBadge"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath introduction = createString("introduction"); - - public final BooleanPath isNotificationsAllowed = createBoolean("isNotificationsAllowed"); - - public final StringPath name = createString("name"); - - public final StringPath nickname = createString("nickname"); - - public final StringPath password = createString("password"); - - public final StringPath phone = createString("phone"); - - public final StringPath profileImage = createString("profileImage"); - - public final EnumPath provider = createEnum("provider", Provider.class); - - public final StringPath providerId = createString("providerId"); - - public final EnumPath role = createEnum("role", MemberRole.class); - - public final EnumPath status = createEnum("status", MemberStatus.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QMember(String variable) { - super(Member.class, forVariable(variable)); - } - - public QMember(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QMember(PathMetadata metadata) { - super(Member.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholType.java b/src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholType.java deleted file mode 100644 index 1a67224d..00000000 --- a/src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholType.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.juu.juulabel.member.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QMemberAlcoholType is a Querydsl query type for MemberAlcoholType - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QMemberAlcoholType extends EntityPathBase { - - private static final long serialVersionUID = 2046452069L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QMemberAlcoholType memberAlcoholType = new QMemberAlcoholType("memberAlcoholType"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final com.juu.juulabel.alcohol.domain.QAlcoholType alcoholType; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final QMember member; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QMemberAlcoholType(String variable) { - this(MemberAlcoholType.class, forVariable(variable), INITS); - } - - public QMemberAlcoholType(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QMemberAlcoholType(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QMemberAlcoholType(PathMetadata metadata, PathInits inits) { - this(MemberAlcoholType.class, metadata, inits); - } - - public QMemberAlcoholType(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholType = inits.isInitialized("alcoholType") ? new com.juu.juulabel.alcohol.domain.QAlcoholType(forProperty("alcoholType")) : null; - this.member = inits.isInitialized("member") ? new QMember(forProperty("member")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholicDrinks.java b/src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholicDrinks.java deleted file mode 100644 index 9998c679..00000000 --- a/src/main/generated/com/juu/juulabel/member/domain/QMemberAlcoholicDrinks.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.juu.juulabel.member.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QMemberAlcoholicDrinks is a Querydsl query type for MemberAlcoholicDrinks - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QMemberAlcoholicDrinks extends EntityPathBase { - - private static final long serialVersionUID = -118220512L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QMemberAlcoholicDrinks memberAlcoholicDrinks = new QMemberAlcoholicDrinks("memberAlcoholicDrinks"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - public final com.juu.juulabel.alcohol.domain.QAlcoholicDrinks alcoholicDrinks; - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final QMember member; - - public QMemberAlcoholicDrinks(String variable) { - this(MemberAlcoholicDrinks.class, forVariable(variable), INITS); - } - - public QMemberAlcoholicDrinks(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QMemberAlcoholicDrinks(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QMemberAlcoholicDrinks(PathMetadata metadata, PathInits inits) { - this(MemberAlcoholicDrinks.class, metadata, inits); - } - - public QMemberAlcoholicDrinks(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholicDrinks = inits.isInitialized("alcoholicDrinks") ? new com.juu.juulabel.alcohol.domain.QAlcoholicDrinks(forProperty("alcoholicDrinks"), inits.get("alcoholicDrinks")) : null; - this.member = inits.isInitialized("member") ? new QMember(forProperty("member")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/member/domain/QMemberTerms.java b/src/main/generated/com/juu/juulabel/member/domain/QMemberTerms.java deleted file mode 100644 index 417f946c..00000000 --- a/src/main/generated/com/juu/juulabel/member/domain/QMemberTerms.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.juu.juulabel.member.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QMemberTerms is a Querydsl query type for MemberTerms - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QMemberTerms extends EntityPathBase { - - private static final long serialVersionUID = 1507682628L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QMemberTerms memberTerms = new QMemberTerms("memberTerms"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final DateTimePath agreedAt = createDateTime("agreedAt", java.time.LocalDateTime.class); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final QMember member; - - public final com.juu.juulabel.terms.domain.QTerms terms; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QMemberTerms(String variable) { - this(MemberTerms.class, forVariable(variable), INITS); - } - - public QMemberTerms(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QMemberTerms(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QMemberTerms(PathMetadata metadata, PathInits inits) { - this(MemberTerms.class, metadata, inits); - } - - public QMemberTerms(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.member = inits.isInitialized("member") ? new QMember(forProperty("member")) : null; - this.terms = inits.isInitialized("terms") ? new com.juu.juulabel.terms.domain.QTerms(forProperty("terms")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/member/domain/QWithdrawalRecord.java b/src/main/generated/com/juu/juulabel/member/domain/QWithdrawalRecord.java deleted file mode 100644 index 0df2362a..00000000 --- a/src/main/generated/com/juu/juulabel/member/domain/QWithdrawalRecord.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.juu.juulabel.member.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QWithdrawalRecord is a Querydsl query type for WithdrawalRecord - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QWithdrawalRecord extends EntityPathBase { - - private static final long serialVersionUID = 1674934671L; - - public static final QWithdrawalRecord withdrawalRecord = new QWithdrawalRecord("withdrawalRecord"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final StringPath email = createString("email"); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath nickname = createString("nickname"); - - public final StringPath withdrawalReason = createString("withdrawalReason"); - - public QWithdrawalRecord(String variable) { - super(WithdrawalRecord.class, forVariable(variable)); - } - - public QWithdrawalRecord(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QWithdrawalRecord(PathMetadata metadata) { - super(WithdrawalRecord.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/notification/domain/QNotification.java b/src/main/generated/com/juu/juulabel/notification/domain/QNotification.java deleted file mode 100644 index 2eaa832b..00000000 --- a/src/main/generated/com/juu/juulabel/notification/domain/QNotification.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.juu.juulabel.notification.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QNotification is a Querydsl query type for Notification - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QNotification extends EntityPathBase { - - private static final long serialVersionUID = -1125011675L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QNotification notification = new QNotification("notification"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - public final NumberPath commentId = createNumber("commentId", Long.class); - - public final StringPath content = createString("content"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isRead = createBoolean("isRead"); - - public final EnumPath notificationType = createEnum("notificationType", NotificationType.class); - - public final StringPath profileImageUrl = createString("profileImageUrl"); - - public final com.juu.juulabel.member.domain.QMember receiver; - - public final StringPath relatedUrl = createString("relatedUrl"); - - public QNotification(String variable) { - this(Notification.class, forVariable(variable), INITS); - } - - public QNotification(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QNotification(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QNotification(PathMetadata metadata, PathInits inits) { - this(Notification.class, metadata, inits); - } - - public QNotification(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.receiver = inits.isInitialized("receiver") ? new com.juu.juulabel.member.domain.QMember(forProperty("receiver")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNote.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNote.java deleted file mode 100644 index 0d6269c2..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNote.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNote is a Querydsl query type for TastingNote - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNote extends EntityPathBase { - - private static final long serialVersionUID = 544443447L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNote tastingNote = new QTastingNote("tastingNote"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final com.juu.juulabel.tastingnote.domain.embedded.QAlcoholicDrinksSnapshot alcoholDrinksInfo; - - public final com.juu.juulabel.alcohol.domain.QAlcoholicDrinks alcoholicDrinks; - - public final com.juu.juulabel.alcohol.domain.QAlcoholType alcoholType; - - public final com.juu.juulabel.alcohol.domain.QColor color; - - public final StringPath content = createString("content"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isPrivate = createBoolean("isPrivate"); - - public final com.juu.juulabel.member.domain.QMember member; - - public final NumberPath rating = createNumber("rating", Double.class); - - public final ListPath tastingNoteScents = this.createList("tastingNoteScents", TastingNoteScent.class, QTastingNoteScent.class, PathInits.DIRECT2); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTastingNote(String variable) { - this(TastingNote.class, forVariable(variable), INITS); - } - - public QTastingNote(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNote(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNote(PathMetadata metadata, PathInits inits) { - this(TastingNote.class, metadata, inits); - } - - public QTastingNote(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.alcoholDrinksInfo = inits.isInitialized("alcoholDrinksInfo") ? new com.juu.juulabel.tastingnote.domain.embedded.QAlcoholicDrinksSnapshot(forProperty("alcoholDrinksInfo")) : null; - this.alcoholicDrinks = inits.isInitialized("alcoholicDrinks") ? new com.juu.juulabel.alcohol.domain.QAlcoholicDrinks(forProperty("alcoholicDrinks"), inits.get("alcoholicDrinks")) : null; - this.alcoholType = inits.isInitialized("alcoholType") ? new com.juu.juulabel.alcohol.domain.QAlcoholType(forProperty("alcoholType")) : null; - this.color = inits.isInitialized("color") ? new com.juu.juulabel.alcohol.domain.QColor(forProperty("color")) : null; - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteComment.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteComment.java deleted file mode 100644 index 02db1d84..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteComment.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteComment is a Querydsl query type for TastingNoteComment - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteComment extends EntityPathBase { - - private static final long serialVersionUID = 435559976L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteComment tastingNoteComment = new QTastingNoteComment("tastingNoteComment"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final StringPath content = createString("content"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.member.domain.QMember member; - - public final QTastingNoteComment parent; - - public final QTastingNote tastingNote; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTastingNoteComment(String variable) { - this(TastingNoteComment.class, forVariable(variable), INITS); - } - - public QTastingNoteComment(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteComment(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteComment(PathMetadata metadata, PathInits inits) { - this(TastingNoteComment.class, metadata, inits); - } - - public QTastingNoteComment(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - this.parent = inits.isInitialized("parent") ? new QTastingNoteComment(forProperty("parent"), inits.get("parent")) : null; - this.tastingNote = inits.isInitialized("tastingNote") ? new QTastingNote(forProperty("tastingNote"), inits.get("tastingNote")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteCommentLike.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteCommentLike.java deleted file mode 100644 index 278182fe..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteCommentLike.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteCommentLike is a Querydsl query type for TastingNoteCommentLike - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteCommentLike extends EntityPathBase { - - private static final long serialVersionUID = -670110241L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteCommentLike tastingNoteCommentLike = new QTastingNoteCommentLike("tastingNoteCommentLike"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.member.domain.QMember member; - - public final QTastingNoteComment tastingNoteComment; - - public QTastingNoteCommentLike(String variable) { - this(TastingNoteCommentLike.class, forVariable(variable), INITS); - } - - public QTastingNoteCommentLike(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteCommentLike(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteCommentLike(PathMetadata metadata, PathInits inits) { - this(TastingNoteCommentLike.class, metadata, inits); - } - - public QTastingNoteCommentLike(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - this.tastingNoteComment = inits.isInitialized("tastingNoteComment") ? new QTastingNoteComment(forProperty("tastingNoteComment"), inits.get("tastingNoteComment")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteFlavorLevel.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteFlavorLevel.java deleted file mode 100644 index 22fdc745..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteFlavorLevel.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteFlavorLevel is a Querydsl query type for TastingNoteFlavorLevel - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteFlavorLevel extends EntityPathBase { - - private static final long serialVersionUID = -2092980529L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteFlavorLevel tastingNoteFlavorLevel = new QTastingNoteFlavorLevel("tastingNoteFlavorLevel"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final com.juu.juulabel.alcohol.domain.QFlavorLevel flavorLevel; - - public final NumberPath id = createNumber("id", Long.class); - - public final QTastingNote tastingNote; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTastingNoteFlavorLevel(String variable) { - this(TastingNoteFlavorLevel.class, forVariable(variable), INITS); - } - - public QTastingNoteFlavorLevel(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteFlavorLevel(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteFlavorLevel(PathMetadata metadata, PathInits inits) { - this(TastingNoteFlavorLevel.class, metadata, inits); - } - - public QTastingNoteFlavorLevel(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.flavorLevel = inits.isInitialized("flavorLevel") ? new com.juu.juulabel.alcohol.domain.QFlavorLevel(forProperty("flavorLevel"), inits.get("flavorLevel")) : null; - this.tastingNote = inits.isInitialized("tastingNote") ? new QTastingNote(forProperty("tastingNote"), inits.get("tastingNote")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteImage.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteImage.java deleted file mode 100644 index a88c1103..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteImage.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteImage is a Querydsl query type for TastingNoteImage - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteImage extends EntityPathBase { - - private static final long serialVersionUID = 2012624740L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteImage tastingNoteImage = new QTastingNoteImage("tastingNoteImage"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final StringPath imagePath = createString("imagePath"); - - public final NumberPath seq = createNumber("seq", Integer.class); - - public final QTastingNote tastingNote; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTastingNoteImage(String variable) { - this(TastingNoteImage.class, forVariable(variable), INITS); - } - - public QTastingNoteImage(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteImage(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteImage(PathMetadata metadata, PathInits inits) { - this(TastingNoteImage.class, metadata, inits); - } - - public QTastingNoteImage(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.tastingNote = inits.isInitialized("tastingNote") ? new QTastingNote(forProperty("tastingNote"), inits.get("tastingNote")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteLike.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteLike.java deleted file mode 100644 index 17ca2c4b..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteLike.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteLike is a Querydsl query type for TastingNoteLike - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteLike extends EntityPathBase { - - private static final long serialVersionUID = 1727577198L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteLike tastingNoteLike = new QTastingNoteLike("tastingNoteLike"); - - public final com.juu.juulabel.common.base.QBaseCreatedTimeEntity _super = new com.juu.juulabel.common.base.QBaseCreatedTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.member.domain.QMember member; - - public final QTastingNote tastingNote; - - public QTastingNoteLike(String variable) { - this(TastingNoteLike.class, forVariable(variable), INITS); - } - - public QTastingNoteLike(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteLike(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteLike(PathMetadata metadata, PathInits inits) { - this(TastingNoteLike.class, metadata, inits); - } - - public QTastingNoteLike(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.member = inits.isInitialized("member") ? new com.juu.juulabel.member.domain.QMember(forProperty("member")) : null; - this.tastingNote = inits.isInitialized("tastingNote") ? new QTastingNote(forProperty("tastingNote"), inits.get("tastingNote")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteScent.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteScent.java deleted file mode 100644 index f55c1dac..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteScent.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteScent is a Querydsl query type for TastingNoteScent - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteScent extends EntityPathBase { - - private static final long serialVersionUID = 2021566116L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteScent tastingNoteScent = new QTastingNoteScent("tastingNoteScent"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.alcohol.domain.QScent scent; - - public final QTastingNote tastingNote; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTastingNoteScent(String variable) { - this(TastingNoteScent.class, forVariable(variable), INITS); - } - - public QTastingNoteScent(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteScent(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteScent(PathMetadata metadata, PathInits inits) { - this(TastingNoteScent.class, metadata, inits); - } - - public QTastingNoteScent(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.scent = inits.isInitialized("scent") ? new com.juu.juulabel.alcohol.domain.QScent(forProperty("scent")) : null; - this.tastingNote = inits.isInitialized("tastingNote") ? new QTastingNote(forProperty("tastingNote"), inits.get("tastingNote")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteSensoryLevel.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteSensoryLevel.java deleted file mode 100644 index cd08dbf5..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/QTastingNoteSensoryLevel.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.juu.juulabel.tastingnote.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; -import com.querydsl.core.types.dsl.PathInits; - - -/** - * QTastingNoteSensoryLevel is a Querydsl query type for TastingNoteSensoryLevel - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTastingNoteSensoryLevel extends EntityPathBase { - - private static final long serialVersionUID = 751400092L; - - private static final PathInits INITS = PathInits.DIRECT2; - - public static final QTastingNoteSensoryLevel tastingNoteSensoryLevel = new QTastingNoteSensoryLevel("tastingNoteSensoryLevel"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final NumberPath id = createNumber("id", Long.class); - - public final com.juu.juulabel.alcohol.domain.QSensoryLevel sensoryLevel; - - public final QTastingNote tastingNote; - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTastingNoteSensoryLevel(String variable) { - this(TastingNoteSensoryLevel.class, forVariable(variable), INITS); - } - - public QTastingNoteSensoryLevel(Path path) { - this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); - } - - public QTastingNoteSensoryLevel(PathMetadata metadata) { - this(metadata, PathInits.getFor(metadata, INITS)); - } - - public QTastingNoteSensoryLevel(PathMetadata metadata, PathInits inits) { - this(TastingNoteSensoryLevel.class, metadata, inits); - } - - public QTastingNoteSensoryLevel(Class type, PathMetadata metadata, PathInits inits) { - super(type, metadata, inits); - this.sensoryLevel = inits.isInitialized("sensoryLevel") ? new com.juu.juulabel.alcohol.domain.QSensoryLevel(forProperty("sensoryLevel"), inits.get("sensoryLevel")) : null; - this.tastingNote = inits.isInitialized("tastingNote") ? new QTastingNote(forProperty("tastingNote"), inits.get("tastingNote")) : null; - } - -} - diff --git a/src/main/generated/com/juu/juulabel/tastingnote/domain/embedded/QAlcoholicDrinksSnapshot.java b/src/main/generated/com/juu/juulabel/tastingnote/domain/embedded/QAlcoholicDrinksSnapshot.java deleted file mode 100644 index ee8a9f79..00000000 --- a/src/main/generated/com/juu/juulabel/tastingnote/domain/embedded/QAlcoholicDrinksSnapshot.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.juu.juulabel.tastingnote.domain.embedded; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QAlcoholicDrinksSnapshot is a Querydsl query type for AlcoholicDrinksSnapshot - */ -@Generated("com.querydsl.codegen.DefaultEmbeddableSerializer") -public class QAlcoholicDrinksSnapshot extends BeanPath { - - private static final long serialVersionUID = 579942130L; - - public static final QAlcoholicDrinksSnapshot alcoholicDrinksSnapshot = new QAlcoholicDrinksSnapshot("alcoholicDrinksSnapshot"); - - public final NumberPath alcoholContent = createNumber("alcoholContent", Double.class); - - public final StringPath alcoholicDrinksName = createString("alcoholicDrinksName"); - - public final StringPath alcoholTypeName = createString("alcoholTypeName"); - - public final StringPath breweryName = createString("breweryName"); - - public final StringPath breweryRegion = createString("breweryRegion"); - - public QAlcoholicDrinksSnapshot(String variable) { - super(AlcoholicDrinksSnapshot.class, forVariable(variable)); - } - - public QAlcoholicDrinksSnapshot(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QAlcoholicDrinksSnapshot(PathMetadata metadata) { - super(AlcoholicDrinksSnapshot.class, metadata); - } - -} - diff --git a/src/main/generated/com/juu/juulabel/terms/domain/QTerms.java b/src/main/generated/com/juu/juulabel/terms/domain/QTerms.java deleted file mode 100644 index 64210851..00000000 --- a/src/main/generated/com/juu/juulabel/terms/domain/QTerms.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.juu.juulabel.terms.domain; - -import static com.querydsl.core.types.PathMetadataFactory.*; - -import com.querydsl.core.types.dsl.*; - -import com.querydsl.core.types.PathMetadata; -import javax.annotation.processing.Generated; -import com.querydsl.core.types.Path; - - -/** - * QTerms is a Querydsl query type for Terms - */ -@Generated("com.querydsl.codegen.DefaultEntitySerializer") -public class QTerms extends EntityPathBase { - - private static final long serialVersionUID = 1516037303L; - - public static final QTerms terms = new QTerms("terms"); - - public final com.juu.juulabel.common.base.QBaseTimeEntity _super = new com.juu.juulabel.common.base.QBaseTimeEntity(this); - - public final StringPath content = createString("content"); - - //inherited - public final DateTimePath createdAt = _super.createdAt; - - public final DateTimePath deletedAt = createDateTime("deletedAt", java.time.LocalDateTime.class); - - public final NumberPath id = createNumber("id", Long.class); - - public final BooleanPath isRequired = createBoolean("isRequired"); - - public final BooleanPath isUsed = createBoolean("isUsed"); - - public final StringPath title = createString("title"); - - public final EnumPath type = createEnum("type", TermsType.class); - - //inherited - public final DateTimePath updatedAt = _super.updatedAt; - - public QTerms(String variable) { - super(Terms.class, forVariable(variable)); - } - - public QTerms(Path path) { - super(path.getType(), path.getMetadata()); - } - - public QTerms(PathMetadata metadata) { - super(Terms.class, metadata); - } - -} - diff --git a/src/main/java/com/juu/juulabel/JuulabelApplication.java b/src/main/java/com/juu/juulabel/JuulabelApplication.java index 5ae58127..49c3e8d7 100644 --- a/src/main/java/com/juu/juulabel/JuulabelApplication.java +++ b/src/main/java/com/juu/juulabel/JuulabelApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @SpringBootApplication +@EnableRedisRepositories public class JuulabelApplication { public static void main(String[] args) { diff --git a/src/main/java/com/juu/juulabel/admin/AdminController.java b/src/main/java/com/juu/juulabel/admin/AdminController.java index 897a44d9..23ed58eb 100644 --- a/src/main/java/com/juu/juulabel/admin/AdminController.java +++ b/src/main/java/com/juu/juulabel/admin/AdminController.java @@ -1,7 +1,5 @@ package com.juu.juulabel.admin; -import com.juu.juulabel.admin.response.MemberListSummary; -import com.juu.juulabel.alcohol.response.CategorySearchAlcoholRequest; import com.juu.juulabel.common.dto.request.MemberListRequest; import com.juu.juulabel.common.dto.response.MemberListResponse; import com.juu.juulabel.common.exception.code.SuccessCode; @@ -15,12 +13,9 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -@Tag( - name = "관리자 API", - description = "뱃지 부여 및 알림 발송 등 관리자 관련 API" -) +@Tag(name = "관리자 API", description = "뱃지 부여 및 알림 발송 등 관리자 관련 API") @RestController -@RequestMapping(value = {"/v1/api/admins"}) +@RequestMapping(value = { "/v1/api/admins" }) @RequiredArgsConstructor public class AdminController { @@ -29,9 +24,8 @@ public class AdminController { @Operation(summary = "뱃지 부여") @PostMapping("/badges") public ResponseEntity> assignBadge( - @AuthenticationPrincipal Member member, - @RequestParam(value = "email") String email - ) { + @AuthenticationPrincipal Member member, + @RequestParam(value = "email") String email) { adminService.assignBadge(email, member); return CommonResponse.success(SuccessCode.SUCCESS); } diff --git a/src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java b/src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java new file mode 100644 index 00000000..c661b540 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/controller/AuthApiDocs.java @@ -0,0 +1,93 @@ +package com.juu.juulabel.auth.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.juu.juulabel.common.constants.AuthConstants; +import com.juu.juulabel.common.dto.request.OAuthLoginRequest; +import com.juu.juulabel.common.dto.request.SignUpMemberRequest; +import com.juu.juulabel.common.dto.request.WithdrawalRequest; +import com.juu.juulabel.common.dto.response.LoginResponse; +import com.juu.juulabel.common.dto.response.RefreshResponse; +import com.juu.juulabel.common.dto.response.SignUpMemberResponse; +import com.juu.juulabel.common.response.CommonResponse; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.domain.Provider; + +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Schema; + +@Tag(name = "인증 API", description = "로그인, 회원가입, 회원탈퇴, 토큰 관리 등 인증 관련 API") +@RequestMapping("/v1/api/auth") +public interface AuthApiDocs { + + @Operation(summary = "OAuth 소셜 로그인", description = "지원되는 OAuth 제공자(Google, Kakao)를 통한 로그인") + @ApiResponse(responseCode = "200", description = "로그인 성공", headers = { + @Header(name = "Set-Cookie", description = "계정이 존재할시만 리프레시 토큰 발급", schema = @Schema(type = "string")) + }) + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터") + @ApiResponse(responseCode = "401", description = "인증 실패") + @PostMapping("/login/{provider}") + public ResponseEntity> oauthLogin( + @Parameter(description = "OAuth 제공자 (GOOGLE, KAKAO)", required = true) @PathVariable Provider provider, + @Valid @RequestBody OAuthLoginRequest requestBody); + + @Operation(summary = "회원가입", description = "새로운 회원 등록 및 초기 토큰 발급") + @ApiResponse(responseCode = "200", description = "회원가입 성공", headers = { + @Header(name = "Set-Cookie", description = "리프레시 토큰 발급", schema = @Schema(type = "string")) + }) + @ApiResponse(responseCode = "400", description = "유효성 검사 실패, 중복된 이메일 또는 닉네임") + @PostMapping("/sign-up") + public ResponseEntity> signUp( + @Valid @RequestBody SignUpMemberRequest request); + + @Operation(summary = "액세스 토큰 갱신", description = "리프레시 토큰을 사용하여 새로운 액세스 토큰 발급") + @ApiResponse(responseCode = "200", description = "토큰 갱신 성공", headers = { + @Header(name = "Set-Cookie", description = "리프레시 토큰 갱신", schema = @Schema(type = "string")) + }) + @ApiResponse(responseCode = "401", description = "유효하지 않은 리프레시 토큰", headers = { + @Header(name = "Set-Cookie", description = "리프레시 토큰 즉시 삭제", schema = @Schema(type = "string")) + }) + @ApiResponse(responseCode = "403", description = "토큰 재사용 감지", headers = { + @Header(name = "Set-Cookie", description = "리프레시 토큰 즉시 삭제", schema = @Schema(type = "string")) + }) + @PostMapping("/refresh") + public ResponseEntity> refresh( + @Parameter(description = "리프레시 토큰 (쿠키)", required = true) @CookieValue(value = AuthConstants.REFRESH_TOKEN_HEADER_NAME, required = true) String refreshToken, + @AuthenticationPrincipal Member member); + + @Operation(summary = "로그아웃", description = "현재 디바이스의 리프레시 토큰 무효화") + @ApiResponse(responseCode = "200", description = "로그아웃 성공", headers = { + @Header(name = "Set-Cookie", description = "리프레시 토큰 즉시 삭제", schema = @Schema(type = "string")) + }) + @ApiResponse(responseCode = "401", description = "인증되지 않은 요청") + @PostMapping("/logout") + public ResponseEntity> logout( + @Parameter(description = "리프레시 토큰 (쿠키)", required = true) @CookieValue(value = AuthConstants.REFRESH_TOKEN_HEADER_NAME, required = true) String refreshToken, + @AuthenticationPrincipal Member member); + + @Operation(summary = "회원 탈퇴", description = "회원 계정 삭제 및 모든 토큰 무효화") + @ApiResponse(responseCode = "200", description = "회원 탈퇴 성공", headers = { + @Header(name = "Set-Cookie", description = "리프레시 토큰 즉시 삭제", schema = @Schema(type = "string")) + }) + @ApiResponse(responseCode = "400", description = "잘못된 탈퇴 요청") + @ApiResponse(responseCode = "401", description = "인증되지 않은 요청") + @DeleteMapping("/me") + public ResponseEntity> deleteAccount( + @Parameter(description = "리프레시 토큰 (쿠키)", required = true) @CookieValue(value = AuthConstants.REFRESH_TOKEN_HEADER_NAME, required = true) String refreshToken, + @AuthenticationPrincipal Member member, + @Valid @RequestBody WithdrawalRequest request); + +} diff --git a/src/main/java/com/juu/juulabel/auth/controller/AuthController.java b/src/main/java/com/juu/juulabel/auth/controller/AuthController.java new file mode 100644 index 00000000..2bb03848 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/controller/AuthController.java @@ -0,0 +1,79 @@ +package com.juu.juulabel.auth.controller; + +import com.juu.juulabel.auth.service.AuthService; +import com.juu.juulabel.common.constants.AuthConstants; +import com.juu.juulabel.common.dto.request.OAuthLoginRequest; +import com.juu.juulabel.common.dto.request.SignUpMemberRequest; +import com.juu.juulabel.common.dto.request.WithdrawalRequest; +import com.juu.juulabel.common.dto.response.LoginResponse; +import com.juu.juulabel.common.dto.response.RefreshResponse; +import com.juu.juulabel.common.dto.response.SignUpMemberResponse; +import com.juu.juulabel.common.exception.code.SuccessCode; +import com.juu.juulabel.common.response.CommonResponse; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.domain.Provider; + +import io.swagger.v3.oas.annotations.Parameter; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class AuthController implements AuthApiDocs { + + private final AuthService authService; + + @Override + public ResponseEntity> oauthLogin( + @Parameter(description = "OAuth 제공자 (GOOGLE, KAKAO)", required = true) @PathVariable Provider provider, + @Valid @RequestBody OAuthLoginRequest requestBody) { + + LoginResponse loginResponse = authService.login(requestBody); + + return CommonResponse.success(SuccessCode.SUCCESS, loginResponse); + } + + @Override + public ResponseEntity> signUp( + @Valid @RequestBody SignUpMemberRequest request) { + + SignUpMemberResponse signUpMemberResponse = authService.signUp(request); + + return CommonResponse.success(SuccessCode.SUCCESS, signUpMemberResponse); + } + + @Override + public ResponseEntity> refresh( + @Parameter(description = "리프레시 토큰 (쿠키)", required = true) @CookieValue(value = AuthConstants.REFRESH_TOKEN_HEADER_NAME, required = true) String refreshToken, + @AuthenticationPrincipal Member member) { + + RefreshResponse refreshResponse = authService.refresh(refreshToken); + + return CommonResponse.success(SuccessCode.SUCCESS, refreshResponse); + } + + @Override + public ResponseEntity> logout( + @Parameter(description = "리프레시 토큰 (쿠키)", required = true) @CookieValue(value = AuthConstants.REFRESH_TOKEN_HEADER_NAME, required = true) String refreshToken, + @AuthenticationPrincipal Member member) { + + authService.logout(refreshToken); + + return CommonResponse.success(SuccessCode.SUCCESS); + } + + @Override + public ResponseEntity> deleteAccount( + @Parameter(description = "리프레시 토큰 (쿠키)", required = true) @CookieValue(value = AuthConstants.REFRESH_TOKEN_HEADER_NAME, required = true) String refreshToken, + @AuthenticationPrincipal Member member, + @Valid @RequestBody WithdrawalRequest request) { + + authService.deleteAccount(member, request, refreshToken); + + return CommonResponse.success(SuccessCode.SUCCESS_DELETE); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/domain/ClientId.java b/src/main/java/com/juu/juulabel/auth/domain/ClientId.java new file mode 100644 index 00000000..0a7b638d --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/domain/ClientId.java @@ -0,0 +1,27 @@ +package com.juu.juulabel.auth.domain; + +import java.util.Arrays; +import java.util.Optional; + +public enum ClientId { + WEB("web-client"), + IOS("ios-app"), + ANDROID("android-app"), + ADMIN("admin-panel"); + + private final String value; + + ClientId(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static Optional from(String value) { + return Arrays.stream(values()) + .filter(c -> c.value.equals(value)) + .findFirst(); + } +} diff --git a/src/main/java/com/juu/juulabel/auth/domain/RefreshToken.java b/src/main/java/com/juu/juulabel/auth/domain/RefreshToken.java new file mode 100644 index 00000000..55f423cd --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/domain/RefreshToken.java @@ -0,0 +1,62 @@ +package com.juu.juulabel.auth.domain; + +import lombok.*; + +import static com.juu.juulabel.common.constants.AuthConstants.REFRESH_TOKEN_DURATION; +import static com.juu.juulabel.common.constants.AuthConstants.REFRESH_TOKEN_HASH_PREFIX; +import static com.juu.juulabel.common.constants.AuthConstants.REFRESH_TOKEN_INDEX_PREFIX; + +import java.io.Serializable; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RefreshToken implements Serializable { + + private String token; + + private String hashedToken; + + private Long memberId; + + private String deviceId; + + private ClientId clientId; + + private String ipAddress; + + private String userAgent; + + private Long ttl; + + private boolean revoked; + + @Builder + public RefreshToken(String token, String hashedToken, Long memberId, ClientId clientId, String deviceId, + String ipAddress, String userAgent) { + this.token = token; + this.hashedToken = hashedToken; + this.memberId = memberId; + this.clientId = clientId; + this.deviceId = deviceId; + this.ipAddress = ipAddress; + this.userAgent = userAgent; + this.ttl = REFRESH_TOKEN_DURATION.getSeconds(); + this.revoked = false; + } + + public String getTokenKey() { + return REFRESH_TOKEN_HASH_PREFIX + ":" + hashedToken; + } + + public String getIndexKey() { + return REFRESH_TOKEN_INDEX_PREFIX + ":" + memberId + ":" + clientId + ":" + deviceId; + } + + public List getArgs() { + return List.of(memberId.toString(), clientId.toString(), deviceId, ipAddress, userAgent, ttl.toString()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/executor/LoginRefreshTokenScriptExecutor.java b/src/main/java/com/juu/juulabel/auth/executor/LoginRefreshTokenScriptExecutor.java new file mode 100644 index 00000000..25c43b79 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/LoginRefreshTokenScriptExecutor.java @@ -0,0 +1,45 @@ +package com.juu.juulabel.auth.executor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import com.juu.juulabel.auth.domain.RefreshToken; + +@Component +public class LoginRefreshTokenScriptExecutor implements RedisScriptExecutor { + + private final RedisTemplate redisTemplate; + private final RedisScript redisScript; + + public LoginRefreshTokenScriptExecutor(RedisTemplate redisTemplate) throws IOException { + this.redisTemplate = redisTemplate; + String scriptText = Files.readString( + new ClassPathResource("scripts/login_refresh_token.lua").getFile().toPath(), StandardCharsets.UTF_8); + this.redisScript = RedisScript.of(scriptText, Object.class); + } + + @Override + public Object execute(RefreshToken refreshToken, Object... args) { + String newTokenKey = refreshToken.getTokenKey(); + String indexKey = refreshToken.getIndexKey(); + + List keys = List.of(newTokenKey, indexKey); + List arguments = refreshToken.getArgs(); + + try { + return redisTemplate.execute(redisScript, keys, arguments.toArray()); + } catch (RedisSystemException e) { + handleRedisException(e); + throw e; // This line will never be reached due to the exception thrown above + } + } + +} diff --git a/src/main/java/com/juu/juulabel/auth/executor/RedisScriptExecutor.java b/src/main/java/com/juu/juulabel/auth/executor/RedisScriptExecutor.java new file mode 100644 index 00000000..8bb5673a --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/RedisScriptExecutor.java @@ -0,0 +1,34 @@ +package com.juu.juulabel.auth.executor; + +import org.springframework.data.redis.RedisSystemException; + +import com.juu.juulabel.common.constants.AuthConstants; +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.common.util.HttpResponseUtil; + +import io.lettuce.core.RedisCommandExecutionException; + +public interface RedisScriptExecutor { + T execute(R arg, Object... args); + + default void handleRedisException(RedisSystemException e) { + // Check if the cause is a RedisCommandExecutionException + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof RedisCommandExecutionException) { + handleRedisScriptError(cause.getMessage()); + return; + } + cause = cause.getCause(); + } + + // If no RedisCommandExecutionException found, check the main exception message + handleRedisScriptError(e.getMessage()); + } + + default void handleRedisScriptError(String errorMessage) { + HttpResponseUtil.addCookie(AuthConstants.REFRESH_TOKEN_HEADER_NAME, "", 0); + throw new BaseException(errorMessage, ErrorCode.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/executor/RedisScriptName.java b/src/main/java/com/juu/juulabel/auth/executor/RedisScriptName.java new file mode 100644 index 00000000..be473390 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/RedisScriptName.java @@ -0,0 +1,18 @@ +package com.juu.juulabel.auth.executor; + +public enum RedisScriptName { + ROTATE_REFRESH_TOKEN("RotateRefreshTokenScriptExecutor"), + LOGIN_REFRESH_TOKEN("LoginRefreshTokenScriptExecutor"), + SAVE_REFRESH_TOKEN("SaveRefreshTokenScriptExecutor"), + REVOKE_REFRESH_TOKEN_BY_INDEX_KEY("RevokeRefreshTokenByIndexKeyExecutor"); + + private final String executorName; + + RedisScriptName(String name) { + this.executorName = name; + } + + public String getExecutorName() { + return executorName; + } +} diff --git a/src/main/java/com/juu/juulabel/auth/executor/RevokeRefreshTokenByIndexKeyExecutor.java b/src/main/java/com/juu/juulabel/auth/executor/RevokeRefreshTokenByIndexKeyExecutor.java new file mode 100644 index 00000000..f7ef25ed --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/RevokeRefreshTokenByIndexKeyExecutor.java @@ -0,0 +1,38 @@ +package com.juu.juulabel.auth.executor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +@Component +public class RevokeRefreshTokenByIndexKeyExecutor implements RedisScriptExecutor { + + private final RedisTemplate redisTemplate; + private final RedisScript redisScript; + + public RevokeRefreshTokenByIndexKeyExecutor(RedisTemplate redisTemplate) throws IOException { + this.redisTemplate = redisTemplate; + String scriptText = Files.readString( + new ClassPathResource("scripts/revoke_refresh_token_by_index_key.lua").getFile().toPath(), + StandardCharsets.UTF_8); + this.redisScript = RedisScript.of(scriptText, Object.class); + } + + @Override + public Object execute(String indexKey, Object... args) { + try { + return redisTemplate.execute(redisScript, List.of(indexKey)); + } catch (RedisSystemException e) { + handleRedisException(e); + throw e; // This line will never be reached due to the exception thrown above + } + } + +} diff --git a/src/main/java/com/juu/juulabel/auth/executor/RotateRefreshTokenScriptExecutor.java b/src/main/java/com/juu/juulabel/auth/executor/RotateRefreshTokenScriptExecutor.java new file mode 100644 index 00000000..0c83b421 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/RotateRefreshTokenScriptExecutor.java @@ -0,0 +1,64 @@ +package com.juu.juulabel.auth.executor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import com.juu.juulabel.auth.domain.RefreshToken; +import com.juu.juulabel.common.constants.AuthConstants; +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.common.util.HttpResponseUtil; + +@Component +public class RotateRefreshTokenScriptExecutor implements RedisScriptExecutor { + + private final RedisTemplate redisTemplate; + private final RedisScript redisScript; + + public RotateRefreshTokenScriptExecutor(RedisTemplate redisTemplate) throws IOException { + this.redisTemplate = redisTemplate; + String scriptText = Files.readString( + new ClassPathResource("scripts/rotate_refresh_token.lua").getFile().toPath(), StandardCharsets.UTF_8); + this.redisScript = RedisScript.of(scriptText, Object.class); + } + + @Override + public Object execute(RefreshToken refreshToken, Object... args) { + String newTokenKey = refreshToken.getTokenKey(); + String indexKey = refreshToken.getIndexKey(); + String oldTokenKey = args[0].toString(); + + List keys = Arrays.asList(newTokenKey, indexKey, oldTokenKey); + List arguments = refreshToken.getArgs(); + + try { + return redisTemplate.execute(redisScript, keys, arguments.toArray()); + } catch (RedisSystemException e) { + handleRedisException(e); + throw e; // This line will never be reached due to the exception thrown above + } + } + + @Override + public void handleRedisScriptError(String errorMessage) { + HttpResponseUtil.addCookie(AuthConstants.REFRESH_TOKEN_HEADER_NAME, "", 0); + if (errorMessage.contains("OLD_TOKEN_NOT_FOUND")) { + throw new BaseException(ErrorCode.REFRESH_TOKEN_NOT_FOUND); + } else if (errorMessage.contains("OLD_TOKEN_ALREADY_REVOKED_ALL_TOKENS_INVALIDATED")) { + throw new BaseException(ErrorCode.REFRESH_TOKEN_REUSE_DETECTED); + } else if (errorMessage.contains("DEVICE_ID_MISMATCH")) { + throw new BaseException(ErrorCode.DEVICE_ID_MISMATCH); + } else { + throw new BaseException(errorMessage, ErrorCode.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/juu/juulabel/auth/executor/SaveRefreshTokenScriptExecutor.java b/src/main/java/com/juu/juulabel/auth/executor/SaveRefreshTokenScriptExecutor.java new file mode 100644 index 00000000..0ed0e2e2 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/SaveRefreshTokenScriptExecutor.java @@ -0,0 +1,45 @@ +package com.juu.juulabel.auth.executor; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import com.juu.juulabel.auth.domain.RefreshToken; + +@Component +public class SaveRefreshTokenScriptExecutor implements RedisScriptExecutor { + + private final RedisTemplate redisTemplate; + private final RedisScript redisScript; + + public SaveRefreshTokenScriptExecutor(RedisTemplate redisTemplate) throws IOException { + this.redisTemplate = redisTemplate; + String scriptText = Files.readString( + new ClassPathResource("scripts/save_refresh_token.lua").getFile().toPath(), StandardCharsets.UTF_8); + this.redisScript = RedisScript.of(scriptText, Object.class); + } + + @Override + public Object execute(RefreshToken refreshToken, Object... args) { + String newTokenKey = refreshToken.getTokenKey(); + String indexKey = refreshToken.getIndexKey(); + + List keys = Arrays.asList(newTokenKey, indexKey); + List arguments = refreshToken.getArgs(); + + try { + return redisTemplate.execute(redisScript, keys, arguments.toArray()); + } catch (RedisSystemException e) { + handleRedisException(e); + throw e; // This line will never be reached due to the exception thrown above + } + } +} diff --git a/src/main/java/com/juu/juulabel/auth/executor/ScriptRegistry.java b/src/main/java/com/juu/juulabel/auth/executor/ScriptRegistry.java new file mode 100644 index 00000000..b300067d --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/executor/ScriptRegistry.java @@ -0,0 +1,24 @@ +package com.juu.juulabel.auth.executor; + +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 ScriptRegistry { + + private final Map> scripts; + + public ScriptRegistry(List> executors) { + this.scripts = executors.stream() + .collect(Collectors.toMap(e -> e.getClass().getSimpleName(), Function.identity())); + } + + @SuppressWarnings("unchecked") + public RedisScriptExecutor get(RedisScriptName name) { + return (RedisScriptExecutor) scripts.get(name.getExecutorName()); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/repository/RedisRefreshTokenRepository.java b/src/main/java/com/juu/juulabel/auth/repository/RedisRefreshTokenRepository.java new file mode 100644 index 00000000..6e55cbc5 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/repository/RedisRefreshTokenRepository.java @@ -0,0 +1,50 @@ +package com.juu.juulabel.auth.repository; + +import com.juu.juulabel.auth.domain.ClientId; +import com.juu.juulabel.auth.domain.RefreshToken; +import com.juu.juulabel.auth.executor.RedisScriptName; +import com.juu.juulabel.auth.executor.ScriptRegistry; +import com.juu.juulabel.common.constants.AuthConstants; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class RedisRefreshTokenRepository implements RefreshTokenRepository { + + private final ScriptRegistry scriptRegistry; + + @Override + public void save(RefreshToken refreshToken) { + scriptRegistry.get(RedisScriptName.SAVE_REFRESH_TOKEN).execute(refreshToken); + } + + @Override + public void rotate(RefreshToken refreshToken, String hashedOldToken) { + + String oldTokenKey = AuthConstants.REFRESH_TOKEN_HASH_PREFIX + ":" + hashedOldToken; + + scriptRegistry.get(RedisScriptName.ROTATE_REFRESH_TOKEN).execute(refreshToken, oldTokenKey); + } + + @Override + public void login(RefreshToken refreshToken) { + scriptRegistry.get(RedisScriptName.LOGIN_REFRESH_TOKEN).execute(refreshToken); + } + + @Override + public void revokeByMemberAndDevice(Long memberId, ClientId clientId, String deviceId) { + String indexKey = AuthConstants.REFRESH_TOKEN_INDEX_PREFIX + ":" + memberId + ":" + clientId + ":" + deviceId + + ":*"; + + scriptRegistry.get(RedisScriptName.REVOKE_REFRESH_TOKEN_BY_INDEX_KEY).execute(indexKey); + } + + @Override + public void revokeAllByMember(Long memberId) { + String indexKey = AuthConstants.REFRESH_TOKEN_INDEX_PREFIX + ":" + memberId + ":*"; + + scriptRegistry.get(RedisScriptName.REVOKE_REFRESH_TOKEN_BY_INDEX_KEY).execute(indexKey); + } + +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/repository/RefreshTokenRepository.java b/src/main/java/com/juu/juulabel/auth/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..4dbe50e4 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,33 @@ +package com.juu.juulabel.auth.repository; + +import com.juu.juulabel.auth.domain.ClientId; +import com.juu.juulabel.auth.domain.RefreshToken; + +public interface RefreshTokenRepository { + + /** + * Saves a refresh token + */ + void save(RefreshToken refreshToken); + + /** + * Rotate + */ + void rotate(RefreshToken refreshToken, String hashedOldToken); + + /** + * Login + */ + void login(RefreshToken refreshToken); + + /** + * Revokes all refresh tokens for a member and device + */ + void revokeByMemberAndDevice(Long memberId, ClientId clientId, String deviceId); + + /** + * Revokes all refresh tokens for a member + */ + void revokeAllByMember(Long memberId); + +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/service/AuthService.java b/src/main/java/com/juu/juulabel/auth/service/AuthService.java new file mode 100644 index 00000000..e230fb21 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/service/AuthService.java @@ -0,0 +1,125 @@ +package com.juu.juulabel.auth.service; + +import com.juu.juulabel.common.dto.request.SignUpMemberRequest; +import com.juu.juulabel.common.dto.request.WithdrawalRequest; +import com.juu.juulabel.common.dto.response.LoginResponse; +import com.juu.juulabel.common.dto.response.RefreshResponse; +import com.juu.juulabel.common.dto.response.SignUpMemberResponse; +import com.juu.juulabel.common.exception.InvalidParamException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.common.factory.OAuthProviderFactory; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.domain.WithdrawalRecord; +import com.juu.juulabel.member.repository.MemberReader; +import com.juu.juulabel.member.repository.MemberWriter; +import com.juu.juulabel.member.repository.WithdrawalRecordReader; +import com.juu.juulabel.member.repository.WithdrawalRecordWriter; +import com.juu.juulabel.member.request.OAuthLoginInfo; +import com.juu.juulabel.member.token.Token; +import com.juu.juulabel.member.util.MemberUtils; +import lombok.RequiredArgsConstructor; +import com.juu.juulabel.member.domain.Provider; +import com.juu.juulabel.common.constants.AuthConstants; +import com.juu.juulabel.member.request.OAuthUser; +import com.juu.juulabel.member.request.OAuthUserInfo; +import com.juu.juulabel.common.dto.request.OAuthLoginRequest; + +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final MemberReader memberReader; + private final MemberWriter memberWriter; + private final WithdrawalRecordWriter withdrawalRecordWriter; + private final MemberUtils memberUtils; + private final OAuthProviderFactory providerFactory; + private final WithdrawalRecordReader withdrawalRecordReader; + private final TokenService tokenService; + + @Transactional + public LoginResponse login(OAuthLoginRequest oAuthLoginRequest) { + OAuthLoginInfo authLoginInfo = oAuthLoginRequest.toDto(); + Provider provider = authLoginInfo.provider(); + + String accessToken = providerFactory.getAccessToken( + provider, + authLoginInfo.propertyMap().get(AuthConstants.REDIRECT_URI), + authLoginInfo.propertyMap().get(AuthConstants.CODE)); + + OAuthUser oAuthUser = providerFactory.getOAuthUser(provider, accessToken); + String email = oAuthUser.email(); + + validateNotWithdrawnMember(email); + + boolean isNewMember = !memberReader.existsByEmailAndProvider(email, provider); + Optional memberOpt = isNewMember ? Optional.empty() : Optional.of(memberReader.getByEmail(email)); + + Optional token = tokenService.createAccessToken(memberOpt); + + // Create refresh token for existing members + memberOpt.ifPresent(member -> tokenService.createLoginRefreshToken(member)); + + return new LoginResponse( + token.orElse(new Token(null, null)), + isNewMember, + new OAuthUserInfo( + memberOpt.map(Member::getId).orElse(null), + email, + oAuthUser.id(), + provider)); + } + + @Transactional + public SignUpMemberResponse signUp(SignUpMemberRequest signUpRequest) { + validateSignUpRequest(signUpRequest); + + Member member = Member.create(signUpRequest); + memberWriter.store(member); + + memberUtils.processAlcoholTypes(member, signUpRequest); + memberUtils.processTermsAgreements(member, signUpRequest); + + Token token = tokenService.createTokenPair(member); + + return new SignUpMemberResponse(member.getId(), token); + } + + @Transactional + public RefreshResponse refresh(String oldToken) { + Token newToken = tokenService.rotateRefreshToken(oldToken); + return new RefreshResponse(newToken.accessToken()); + } + + public void logout(String oldToken) { + tokenService.revokeRefreshToken(oldToken); + } + + @Transactional + public void deleteAccount(Member loginMember, WithdrawalRequest request, String oldToken) { + loginMember.deleteAccount(); + withdrawalRecordWriter.store( + WithdrawalRecord.create(request.withdrawalReason(), loginMember.getEmail(), loginMember.getNickname())); + + tokenService.revokeAllRefreshTokens(oldToken); + } + + private void validateNotWithdrawnMember(String email) { + if (withdrawalRecordReader.existEmail(email)) { + throw new InvalidParamException(ErrorCode.MEMBER_WITHDRAWN); + } + } + + private void validateSignUpRequest(SignUpMemberRequest signUpRequest) { + if (memberReader.existActiveNickname(signUpRequest.nickname())) { + throw new InvalidParamException(ErrorCode.MEMBER_NICKNAME_DUPLICATE); + } + + if (memberReader.existActiveEmail(signUpRequest.email())) { + throw new InvalidParamException(ErrorCode.MEMBER_EMAIL_DUPLICATE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/auth/service/FraudDetectionService.java b/src/main/java/com/juu/juulabel/auth/service/FraudDetectionService.java new file mode 100644 index 00000000..83ea0535 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/service/FraudDetectionService.java @@ -0,0 +1,7 @@ +package com.juu.juulabel.auth.service; + +public interface FraudDetectionService { + RiskAssessment assessRisk(T data, + String currentIpAddress, String currentUserAgent, String currentDeviceId); + +} diff --git a/src/main/java/com/juu/juulabel/auth/service/RiskAssessment.java b/src/main/java/com/juu/juulabel/auth/service/RiskAssessment.java new file mode 100644 index 00000000..41d7188a --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/service/RiskAssessment.java @@ -0,0 +1,28 @@ +package com.juu.juulabel.auth.service; + +import lombok.Getter; + +/** + * Risk assessment result + */ +@Getter +public class RiskAssessment { + private final double score; // 0.0 (low) to 1.0 (high) + private final String reason; + private final boolean familyShouldBeCompromised; + + public RiskAssessment(double score, String reason, boolean familyShouldBeCompromised) { + this.score = score; + this.reason = reason; + this.familyShouldBeCompromised = familyShouldBeCompromised; + } + + public boolean isHighRisk() { + /* e.g., score > 0.8 */ + return score > 0.8; + } + + public boolean isFamilyCompromised() { + return familyShouldBeCompromised; + } +} diff --git a/src/main/java/com/juu/juulabel/auth/service/TokenService.java b/src/main/java/com/juu/juulabel/auth/service/TokenService.java new file mode 100644 index 00000000..88c01451 --- /dev/null +++ b/src/main/java/com/juu/juulabel/auth/service/TokenService.java @@ -0,0 +1,131 @@ +package com.juu.juulabel.auth.service; + +import com.juu.juulabel.auth.domain.ClientId; +import com.juu.juulabel.auth.domain.RefreshToken; +import com.juu.juulabel.auth.repository.RefreshTokenRepository; +import com.juu.juulabel.common.constants.AuthConstants; +import com.juu.juulabel.common.provider.JwtTokenProvider; +import com.juu.juulabel.common.util.DeviceIdExtractor; +import com.juu.juulabel.common.util.IpAddressExtractor; +import com.juu.juulabel.common.util.UserAgentExtractor; +import com.juu.juulabel.common.util.HttpResponseUtil; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.token.Token; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenService { + + private final JwtTokenProvider jwtTokenProvider; + private final RefreshTokenRepository refreshTokenRepository; + + /** + * Creates access and refresh tokens for a member + */ + @Transactional + public Token createTokenPair(Member member) { + String accessToken = jwtTokenProvider.createAccessToken(member); + RefreshToken refreshToken = createRefreshToken(member); + + refreshTokenRepository.save(refreshToken); + setRefreshTokenCookie(refreshToken.getToken()); + + return new Token(accessToken, jwtTokenProvider.getExpirationByToken(accessToken)); + } + + /** + * Creates access token only (for existing members during login) + */ + public Optional createAccessToken(Optional memberOpt) { + return memberOpt.map(member -> { + String accessToken = jwtTokenProvider.createAccessToken(member); + return new Token(accessToken, jwtTokenProvider.getExpirationByToken(accessToken)); + }); + } + + /** + * Creates refresh token for login (revokes existing tokens for same device) + */ + @Transactional + public void createLoginRefreshToken(Member member) { + + RefreshToken refreshToken = createRefreshToken(member); + + refreshTokenRepository.login(refreshToken); + setRefreshTokenCookie(refreshToken.getToken()); + } + + /** + * Rotates refresh token + */ + @Transactional + public Token rotateRefreshToken(String oldToken) { + Member member = jwtTokenProvider.getMemberFromToken(oldToken); + String hashedOldToken = jwtTokenProvider.hashToken(oldToken); + + RefreshToken newRefreshToken = createRefreshToken(member); + + refreshTokenRepository.rotate(newRefreshToken, hashedOldToken); + + setRefreshTokenCookie(newRefreshToken.getToken()); + + String newAccessToken = jwtTokenProvider.createAccessToken(member); + return new Token(newAccessToken, jwtTokenProvider.getExpirationByToken(newAccessToken)); + } + + /** + * Revokes refresh token (logout) + */ + @Transactional + public void revokeRefreshToken(String token) { + Member member = jwtTokenProvider.getMemberFromToken(token); + String deviceId = DeviceIdExtractor.getDeviceId(); + + refreshTokenRepository.revokeByMemberAndDevice(member.getId(), ClientId.WEB, deviceId); + clearRefreshTokenCookie(); + } + + /** + * Revokes all refresh tokens for a member (account deletion) + */ + @Transactional + public void revokeAllRefreshTokens(String token) { + Member member = jwtTokenProvider.getMemberFromToken(token); + + refreshTokenRepository.revokeAllByMember(member.getId()); + clearRefreshTokenCookie(); + } + + private RefreshToken createRefreshToken(Member member) { + String token = jwtTokenProvider.createRefreshToken(member); + String hashedToken = jwtTokenProvider.hashToken(token); + + return RefreshToken.builder() + .token(token) + .hashedToken(hashedToken) + .memberId(member.getId()) + .clientId(ClientId.WEB) + .deviceId(DeviceIdExtractor.getDeviceId()) + .ipAddress(IpAddressExtractor.getClientIpAddress()) + .userAgent(UserAgentExtractor.getUserAgent()) + .build(); + } + + private void setRefreshTokenCookie(String token) { + HttpResponseUtil.addCookie( + AuthConstants.REFRESH_TOKEN_HEADER_NAME, + token, + (int) AuthConstants.REFRESH_TOKEN_DURATION.getSeconds()); + } + + private void clearRefreshTokenCookie() { + HttpResponseUtil.addCookie(AuthConstants.REFRESH_TOKEN_HEADER_NAME, "", 0); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java b/src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java index 7c9fe38c..c5bb9a93 100644 --- a/src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java +++ b/src/main/java/com/juu/juulabel/common/config/QuerydslConfig.java @@ -9,11 +9,11 @@ @Configuration public class QuerydslConfig { - @PersistenceContext - private EntityManager entityManager; + @PersistenceContext + private EntityManager entityManager; - @Bean - public JPAQueryFactory queryFactory() { - return new JPAQueryFactory(entityManager); - } + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(entityManager); + } } diff --git a/src/main/java/com/juu/juulabel/common/config/RedisConfig.java b/src/main/java/com/juu/juulabel/common/config/RedisConfig.java new file mode 100644 index 00000000..f7c37545 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/config/RedisConfig.java @@ -0,0 +1,52 @@ +package com.juu.juulabel.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Value("${spring.data.redis.ssl.enabled}") + private boolean sslEnabled; + + @Bean + LettuceConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHost, redisPort); + + LettuceClientConfiguration clientConfig; + + if (sslEnabled) { + clientConfig = LettuceClientConfiguration.builder() + .useSsl() + .disablePeerVerification() // trust any certificate (disable hostname check) + .build(); + } else { + clientConfig = LettuceClientConfiguration.builder().build(); + } + + return new LettuceConnectionFactory(config, clientConfig); + } + + @Bean + RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); + return template; + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java index 378405b9..389e8691 100644 --- a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java +++ b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java @@ -25,68 +25,99 @@ @RequiredArgsConstructor public class SecurityConfig { - private final JwtAuthorizationFilter jwtAuthenticationFilter; - private final JwtExceptionFilter jwtExceptionFilter; - - private static final String[] PERMIT_PATHS = { - "/swagger-ui/**", "/v3/api-docs/**", "/error", "/favicon.ico", "/", "/actuator/**", - "/v1/api/alcohols/**", "/v1/api/terms/**", "/v1/api/images", - "/v1/api/members/**", "/v1/api/shared-space/tasting-notes/**", "/v1/api/notifications/**", - "/v1/api/daily-lives/**", "/v1/api/alcoholicDrinks/**", "v1/api/follow" , "/**", "/v1/api/reports" - }; - - private static final String[] ALLOW_ORIGINS = { - "http://localhost:8084", - "http://localhost:8080", - "http://localhost:5173", - "http://localhost:3000", - "https://api.juulabel.com", - "https://dev.juulabel.com", - "https://qa.juulabel.com", - "https://juulabel.com", - "https://juulabel.shop", - "https://juulabel-front.vercel.app/", - "https://juulabel-front-seven.vercel.app/", - "https://d3jwyw9rpnxu8p.cloudfront.net" - }; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - return http - .csrf(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) - .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .formLogin(AbstractHttpConfigurer::disable) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - - .authorizeHttpRequests(authorize -> authorize - .requestMatchers("/v1/api/members/logout").authenticated() - .requestMatchers(OPTIONS, "**").permitAll() - .requestMatchers(PERMIT_PATHS).permitAll() - .requestMatchers("/v1/api/admins/permission/test").hasAnyAuthority(MemberRole.ROLE_ADMIN.name()) - .anyRequest().authenticated() - ) - - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(jwtExceptionFilter, JwtAuthorizationFilter.class) - - .build(); - } - - @Bean - public UrlBasedCorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - config.addAllowedHeader("*"); - config.addAllowedMethod("*"); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); - config.setAllowedOrigins(List.of(ALLOW_ORIGINS)); - config.addExposedHeader(HttpHeaders.AUTHORIZATION); - config.setAllowCredentials(true); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; - } + private final JwtAuthorizationFilter jwtAuthenticationFilter; + private final JwtExceptionFilter jwtExceptionFilter; + // Public endpoints that don't require authentication + private static final String[] PUBLIC_ENDPOINTS = { + "/swagger-ui/**", "/v3/api-docs/**", "/error", "/favicon.ico", "/", "/actuator/**", + "/v1/api/alcohols/**", "/v1/api/terms/**", "/v1/api/images", + "/v1/api/auth/**", "/v1/api/shared-space/tasting-notes/**", "/v1/api/notifications/**", + "/v1/api/daily-lives/**", "/v1/api/alcoholicDrinks/**", "v1/api/follow", "/**", + "/v1/api/reports" + }; + + // Admin-only endpoints + private static final String[] ADMIN_ENDPOINTS = { + "/v1/api/admins/permission/test" + }; + + // Allowed origins for CORS + private static final String[] ALLOWED_ORIGINS = { + "http://localhost:8084", + "http://localhost:8080", + "http://localhost:5173", + "http://localhost:3000", + "https://api.juulabel.com", + "https://dev.juulabel.com", + "https://qa.juulabel.com", + "https://juulabel.com", + "https://juulabel.shop", + "https://juulabel-front.vercel.app/", + "https://juulabel-front-seven.vercel.app/", + "https://d3jwyw9rpnxu8p.cloudfront.net" + }; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + // Disable unnecessary features for stateless API + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // Configure headers + .headers(headers -> headers + .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) + + // Configure CORS + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // Configure authorization rules + .authorizeHttpRequests(this::configureAuthorization) + + // Add custom filters + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtExceptionFilter, JwtAuthorizationFilter.class) + + .build(); + } + + private void configureAuthorization( + org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorize) { + authorize + // Allow OPTIONS requests for CORS preflight + .requestMatchers(OPTIONS, "**").permitAll() + + // Public endpoints + .requestMatchers(PUBLIC_ENDPOINTS).permitAll() + + // Admin endpoints + .requestMatchers(ADMIN_ENDPOINTS).hasAuthority(MemberRole.ROLE_ADMIN.name()) + + // Specific authenticated endpoints + .requestMatchers("/v1/api/members/logout").authenticated() + + // All other requests require authentication + .anyRequest().authenticated(); + } + + @Bean + public UrlBasedCorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + + // Configure CORS settings + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedOrigins(List.of(ALLOWED_ORIGINS)); + config.addExposedHeader(HttpHeaders.AUTHORIZATION); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } } diff --git a/src/main/java/com/juu/juulabel/common/config/WebConfig.java b/src/main/java/com/juu/juulabel/common/config/WebConfig.java new file mode 100644 index 00000000..49a23b71 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/config/WebConfig.java @@ -0,0 +1,19 @@ +package com.juu.juulabel.common.config; + +import com.juu.juulabel.common.converter.ProviderConverter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final ProviderConverter providerConverter; + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(providerConverter); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java b/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java index 1480d7aa..bda251ab 100644 --- a/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java +++ b/src/main/java/com/juu/juulabel/common/constants/AuthConstants.java @@ -1,5 +1,7 @@ package com.juu.juulabel.common.constants; +import java.time.Duration; + import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -10,5 +12,12 @@ public class AuthConstants { public static final String REDIRECT_URI = "redirectUri"; public static final String TOKEN_PREFIX = "Bearer "; + // The RFC 6648 (published in 2012) deprecated the X- prefix for custom headers: + public static final String REFRESH_TOKEN_HEADER_NAME = "Refresh-Token"; + + public static final String REFRESH_TOKEN_HASH_PREFIX = "RefreshToken"; + public static final String REFRESH_TOKEN_INDEX_PREFIX = "RefreshIndex"; + public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1); + public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(30); } diff --git a/src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java b/src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java new file mode 100644 index 00000000..a6b53546 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/converter/ProviderConverter.java @@ -0,0 +1,29 @@ +package com.juu.juulabel.common.converter; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.member.domain.Provider; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +public class ProviderConverter implements Converter { + + private static final Set ALLOWED_PROVIDERS = Set.of(Provider.GOOGLE, Provider.KAKAO); + + @Override + public Provider convert(String source) { + try { + final Provider provider = Provider.valueOf(source.toUpperCase()); + if (!ALLOWED_PROVIDERS.contains(provider)) { + throw new BaseException(ErrorCode.OAUTH_PROVIDER_NOT_FOUND); + } + return provider; + } catch (IllegalArgumentException e) { + throw new BaseException(ErrorCode.OAUTH_PROVIDER_NOT_FOUND); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/dto/response/MemberProfileResponse.java b/src/main/java/com/juu/juulabel/common/dto/response/MemberProfileResponse.java index 01ec4717..419cc989 100644 --- a/src/main/java/com/juu/juulabel/common/dto/response/MemberProfileResponse.java +++ b/src/main/java/com/juu/juulabel/common/dto/response/MemberProfileResponse.java @@ -1,6 +1,5 @@ package com.juu.juulabel.common.dto.response; -import com.querydsl.core.types.dsl.BooleanExpression; import io.swagger.v3.oas.annotations.media.Schema; public record MemberProfileResponse( diff --git a/src/main/java/com/juu/juulabel/common/dto/response/RefreshResponse.java b/src/main/java/com/juu/juulabel/common/dto/response/RefreshResponse.java new file mode 100644 index 00000000..2a1418f4 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/dto/response/RefreshResponse.java @@ -0,0 +1,5 @@ +package com.juu.juulabel.common.dto.response; + +public record RefreshResponse(String accessToken) { + +} diff --git a/src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java b/src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java index 7305e7fe..277de5d7 100644 --- a/src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java +++ b/src/main/java/com/juu/juulabel/common/exception/InvalidParamException.java @@ -1,6 +1,5 @@ package com.juu.juulabel.common.exception; - import com.juu.juulabel.common.exception.code.ErrorCode; public class InvalidParamException extends BaseException { diff --git a/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java b/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java index 11793ace..982e9344 100644 --- a/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java +++ b/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java @@ -21,6 +21,7 @@ public enum ErrorCode { IS_NULL(HttpStatus.BAD_REQUEST, "NULL 값이 들어왔습니다."), COMMON_INVALID_PARAM(HttpStatus.BAD_REQUEST, "요청한 값이 올바르지 않습니다."), INVALID_AUTHENTICATION(HttpStatus.UNAUTHORIZED, "인증이 올바르지 않습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."), /** * Json Web Token @@ -33,7 +34,13 @@ public enum ErrorCode { /** * Authentication */ + DEVICE_ID_REQUIRED(HttpStatus.BAD_REQUEST, "Device-Id 헤더가 필요합니다."), + OAUTH_PROVIDER_NOT_FOUND(HttpStatus.BAD_REQUEST, "Provider를 찾을 수 없습니다."), + REFRESH_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "토큰을 찾을 수 없습니다."), + DEVICE_ID_MISMATCH(HttpStatus.BAD_REQUEST, "Device-Id 불일치"), + REFRESH_TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), + REFRESH_TOKEN_REUSE_DETECTED(HttpStatus.FORBIDDEN, "토큰 재사용 감지"), /** * Admin, Member @@ -58,13 +65,11 @@ public enum ErrorCode { ALCOHOLIC_DRINKS_TYPE_NOT_FOUND(HttpStatus.BAD_REQUEST, "전통주를 찾을 수 없습니다."), ALCOHOLIC_DRINKS_INVALID_RATING(HttpStatus.BAD_REQUEST, "잘못된 평점입니다. 평점은 0.00에서 5.00 사이여야 합니다."), - /** * Notification */ NOTIFICATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 알림이 존재하지 않거나, 권한이 없습니다."), - /** * Comment */ @@ -85,7 +90,6 @@ public enum ErrorCode { EXCEEDED_FILE_COUNT(HttpStatus.BAD_REQUEST, "파일 첨부 허용 개수를 초과했습니다."), FILE_TOO_LARGE(HttpStatus.BAD_REQUEST, "파일 크기가 10MB를 초과했습니다."), - /** * Tasting Note */ diff --git a/src/main/java/com/juu/juulabel/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/juu/juulabel/common/exception/handler/GlobalExceptionHandler.java index 68cf2cf6..a8befaea 100644 --- a/src/main/java/com/juu/juulabel/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/juu/juulabel/common/exception/handler/GlobalExceptionHandler.java @@ -5,7 +5,6 @@ import com.juu.juulabel.common.exception.CustomJwtException; import com.juu.juulabel.common.exception.code.ErrorCode; import com.juu.juulabel.common.response.CommonResponse; -import io.jsonwebtoken.ExpiredJwtException; import io.sentry.Sentry; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -13,6 +12,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.resource.NoResourceFoundException; import java.util.Arrays; @@ -36,13 +36,6 @@ public ResponseEntity> handle(BaseException e) { return CommonResponse.fail(e.getErrorCode()); } - @ExceptionHandler(RuntimeException.class) - public ResponseEntity> handle(RuntimeException e) { - log.error("RuntimeException :", e); - Sentry.captureException(e); - return CommonResponse.fail(ErrorCode.INTERNAL_SERVER_ERROR, e.getMessage()); - } - @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handle(MethodArgumentNotValidException e) { log.error("MethodArgumentNotValidException :", e); @@ -57,9 +50,9 @@ public ResponseEntity> handle(CustomJwtException e) { } @ExceptionHandler(NoResourceFoundException.class) - public void handle(NoResourceFoundException e) { - // 이거 키면 출력이 너무 많이 됨 - // log.warn("NoResourceFoundException : {}", e.getMessage()); + public ResponseEntity> handle(NoResourceFoundException e) { + log.warn("NoResourceFoundException : {}", e.getMessage()); + return CommonResponse.fail(ErrorCode.NOT_FOUND); } @ExceptionHandler(HttpMessageNotReadableException.class) @@ -78,4 +71,20 @@ public ResponseEntity> handleValidationException(HttpMess } return CommonResponse.fail(ErrorCode.VALIDATION_ERROR, errorDetails); } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity> handle(MethodArgumentTypeMismatchException e) { + log.error("MethodArgumentTypeMismatchException :", e); + + // Check if the underlying cause is a BaseException + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof BaseException baseException) { + return CommonResponse.fail(baseException.getErrorCode()); + } + cause = cause.getCause(); + } + + return CommonResponse.fail(ErrorCode.VALIDATION_ERROR, e.getMessage()); + } } diff --git a/src/main/java/com/juu/juulabel/common/filter/JwtAuthorizationFilter.java b/src/main/java/com/juu/juulabel/common/filter/JwtAuthorizationFilter.java index f2cb0a18..ab5ba481 100644 --- a/src/main/java/com/juu/juulabel/common/filter/JwtAuthorizationFilter.java +++ b/src/main/java/com/juu/juulabel/common/filter/JwtAuthorizationFilter.java @@ -3,6 +3,8 @@ import com.juu.juulabel.common.provider.JwtTokenProvider; import com.juu.juulabel.common.exception.code.ErrorCode; import com.juu.juulabel.common.response.CommonResponse; +import com.juu.juulabel.common.util.AuthorizationExtractor; + import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; import jakarta.servlet.FilterChain; @@ -10,7 +12,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; @@ -28,11 +29,11 @@ public class JwtAuthorizationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { + throws ServletException, IOException { - String header = request.getHeader(HttpHeaders.AUTHORIZATION); + String header = AuthorizationExtractor.getAuthorization(); if (header != null) { - String token = jwtTokenProvider.resolveToken(request.getHeader(HttpHeaders.AUTHORIZATION)); + String token = jwtTokenProvider.resolveToken(header); try { if (jwtTokenProvider.isValidateToken(token)) { authenticate(token); @@ -55,7 +56,8 @@ private void handleJwtException(HttpServletResponse response) throws IOException response.setCharacterEncoding("UTF-8"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.getWriter().write(CommonResponse.fail(ErrorCode.INVALID_AUTHENTICATION, "만료되었거나 잘못된 토큰입니다.").toString()); + response.getWriter() + .write(CommonResponse.fail(ErrorCode.INVALID_AUTHENTICATION, "만료되었거나 잘못된 토큰입니다.").toString()); } } \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java b/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java index fc9da767..efdccfb5 100644 --- a/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java +++ b/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java @@ -1,11 +1,11 @@ package com.juu.juulabel.common.provider; -import com.juu.juulabel.common.constants.AuthConstants; import com.juu.juulabel.common.exception.CustomJwtException; import com.juu.juulabel.common.exception.InvalidParamException; import com.juu.juulabel.common.exception.code.ErrorCode; import com.juu.juulabel.member.domain.Member; import com.juu.juulabel.member.domain.MemberRole; + import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; @@ -13,82 +13,119 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; +import static com.juu.juulabel.common.constants.AuthConstants.*; + import javax.crypto.SecretKey; import java.time.Duration; import java.util.*; +import java.util.function.Function; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; @Component public class JwtTokenProvider { - - private static final long ACCESS_TOKEN_EXPIRE_TIME = Duration.ofDays(1).toMillis(); private static final String ISSUER = "juulabel"; private static final String ROLE_CLAIM = "role"; private final SecretKey key; + private final JwtParser jwtParser; public JwtTokenProvider(@Value("${spring.jwt.secret}") String key) { - byte[] keyBytes = Base64.getDecoder().decode(key); - this.key = Keys.hmacShaKeyFor(keyBytes); + this.key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(key)); + this.jwtParser = Jwts.parser().verifyWith(this.key).build(); } public String createAccessToken(Member member) { - return Jwts.builder() - .subject(String.valueOf(member.getId())) - .claim(ROLE_CLAIM, member.getRole().name()) - .issuedAt(new Date()) + return buildToken(member.getId(), member.getRole().name(), ACCESS_TOKEN_DURATION); + } + + public String createRefreshToken(Member member) { + return buildToken(member.getId(), member.getRole().name(), REFRESH_TOKEN_DURATION); + } + + private String buildToken(Long memberId, String role, Duration duration) { + Date now = new Date(); + Date expirationDate = new Date(now.getTime() + duration.toMillis()); + + JwtBuilder builder = Jwts.builder() + .subject(String.valueOf(memberId)) + .issuedAt(now) .issuer(ISSUER) - .expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_TIME)) - .signWith(key) - .compact(); + .expiration(expirationDate) + .signWith(key); + + if (role != null) { + builder.claim(ROLE_CLAIM, role); + } + + return builder.compact(); } public Authentication getAuthentication(String accessToken) { - Claims claims = parseClaims(accessToken); - - Collection roles = Collections - .singletonList(new SimpleGrantedAuthority(claims.get(ROLE_CLAIM, String.class))); + return extractFromClaims(accessToken, claims -> { + String role = claims.get(ROLE_CLAIM, String.class); + Long memberId = Long.parseLong(claims.getSubject()); + + Member member = Member.builder() + .id(memberId) + .role(MemberRole.valueOf(role)) + .build(); + + return new UsernamePasswordAuthenticationToken( + member, + null, + Collections.singletonList(new SimpleGrantedAuthority(role))); + }); + } - Member member = Member.builder() - .id(Long.parseLong(claims.getSubject())) - .role(MemberRole.valueOf(claims.get(ROLE_CLAIM, String.class))) - .build(); + public Member getMemberFromToken(String token) { + return extractFromClaims(token, claims -> { + Long memberId = Long.parseLong(claims.getSubject()); + String role = claims.get(ROLE_CLAIM, String.class); - return new UsernamePasswordAuthenticationToken( - member, - null, - roles); + return Member.builder() + .id(memberId) + .role(role != null ? MemberRole.valueOf(role) : MemberRole.ROLE_USER) + .build(); + }); } public String resolveToken(String header) { - return Optional.ofNullable(header) - .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_AUTHENTICATION)) - .replace(AuthConstants.TOKEN_PREFIX, ""); + if (!StringUtils.hasText(header)) { + throw new InvalidParamException(ErrorCode.INVALID_AUTHENTICATION); + } + return header.replace(TOKEN_PREFIX, ""); } public boolean isValidateToken(String token) { if (!StringUtils.hasText(token)) { return false; } - - return !getExpirationByToken(token).before(new Date()); + try { + return !parseClaims(token).getExpiration().before(new Date()); + } catch (CustomJwtException e) { + return false; + } } public Date getExpirationByToken(String token) { - return parseClaims(token).getExpiration(); + return extractFromClaims(token, Claims::getExpiration); + } + + private T extractFromClaims(String token, Function claimsResolver) { + return claimsResolver.apply(parseClaims(token)); } private Claims parseClaims(String token) { try { - return Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token) - .getPayload(); + return jwtParser.parseSignedClaims(token).getPayload(); } catch (SignatureException | MalformedJwtException ex) { throw new CustomJwtException(ErrorCode.JWT_MALFORMED_EXCEPTION); } catch (ExpiredJwtException ex) { @@ -100,4 +137,13 @@ private Claims parseClaims(String token) { } } + public String hashToken(String token) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashedBytes = digest.digest(token.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hashedBytes); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } } diff --git a/src/main/java/com/juu/juulabel/common/util/AbstractHttpUtil.java b/src/main/java/com/juu/juulabel/common/util/AbstractHttpUtil.java new file mode 100644 index 00000000..df8eba8f --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/util/AbstractHttpUtil.java @@ -0,0 +1,61 @@ +package com.juu.juulabel.common.util; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.Optional; +import java.util.function.Function; + +/** + * Abstract base class for HTTP utility operations + */ +public abstract class AbstractHttpUtil { + + /** + * Protected constructor to prevent direct instantiation + */ + protected AbstractHttpUtil() { + throw new AssertionError("Utility class should not be instantiated"); + } + + /** + * Gets the current HTTP request from the servlet context + * + * @return the current HttpServletRequest + * @throws BaseException if request attributes are not available + */ + protected static HttpServletRequest getCurrentRequest() { + return getFromRequestAttributes(ServletRequestAttributes::getRequest); + } + + /** + * Gets the current HTTP response from the servlet context + * + * @return the current HttpServletResponse + * @throws BaseException if request attributes are not available + */ + protected static HttpServletResponse getCurrentResponse() { + return getFromRequestAttributes(ServletRequestAttributes::getResponse); + } + + /** + * Extracts data from ServletRequestAttributes using the provided function + * + * @param extractor function to extract data from ServletRequestAttributes + * @return extracted data + * @throws BaseException if request attributes are not available + */ + protected static T getFromRequestAttributes(Function extractor) { + return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .filter(ServletRequestAttributes.class::isInstance) + .map(ServletRequestAttributes.class::cast) + .map(extractor) + .orElseThrow(() -> new BaseException(ErrorCode.INTERNAL_SERVER_ERROR)); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/util/AuthorizationExtractor.java b/src/main/java/com/juu/juulabel/common/util/AuthorizationExtractor.java new file mode 100644 index 00000000..bcb345c4 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/util/AuthorizationExtractor.java @@ -0,0 +1,27 @@ +package com.juu.juulabel.common.util; + +import org.springframework.http.HttpHeaders; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Utility class for authorization header extraction + */ +public final class AuthorizationExtractor extends AbstractHttpUtil { + + /** + * Private constructor to prevent instantiation + */ + private AuthorizationExtractor() { + super(); + } + + /** + * Extract authorization header from request + * + * @return authorization header value + */ + public static String getAuthorization() { + HttpServletRequest request = getCurrentRequest(); + return request.getHeader(HttpHeaders.AUTHORIZATION); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/util/DeviceIdExtractor.java b/src/main/java/com/juu/juulabel/common/util/DeviceIdExtractor.java new file mode 100644 index 00000000..3f6cb9d3 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/util/DeviceIdExtractor.java @@ -0,0 +1,35 @@ +package com.juu.juulabel.common.util; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Utility class for device ID extraction + */ +public final class DeviceIdExtractor extends AbstractHttpUtil { + + private static final String DEVICE_ID_HEADER_NAME = "Device-Id"; + + /** + * Private constructor to prevent instantiation + */ + private DeviceIdExtractor() { + super(); + } + + /** + * Extract device ID from request headers + * + * @return device ID from Device-Id header + * @throws BaseException if Device-Id header is missing or empty + */ + public static String getDeviceId() { + HttpServletRequest request = getCurrentRequest(); + String deviceId = request.getHeader(DEVICE_ID_HEADER_NAME); + if (deviceId == null || deviceId.trim().isEmpty()) { + throw new BaseException(ErrorCode.DEVICE_ID_REQUIRED); + } + return deviceId.trim(); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/util/HttpResponseUtil.java b/src/main/java/com/juu/juulabel/common/util/HttpResponseUtil.java new file mode 100644 index 00000000..a66ec0e8 --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/util/HttpResponseUtil.java @@ -0,0 +1,46 @@ +package com.juu.juulabel.common.util; + +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +public final class HttpResponseUtil extends AbstractHttpUtil { + + private HttpResponseUtil() { + super(); + } + + /** + * Adds a secure HTTP-only cookie to the response + */ + public static void addCookie(String name, String value, int maxAge) { + HttpServletResponse response = getCurrentResponse(); + Cookie cookie = createSecureCookie(name, value, maxAge); + response.addCookie(cookie); + } + + /** + * Removes a cookie by setting its max age to 0 + */ + public static void removeCookie(String name) { + addCookie(name, "", 0); + } + + /** + * Creates a secure cookie with default settings + */ + private static Cookie createSecureCookie(String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setMaxAge(maxAge); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setSecure(true); + return cookie; + } + + public static HttpServletResponse getCurrentResponse() { + return getFromRequestAttributes(ServletRequestAttributes::getResponse); + } + +} diff --git a/src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java b/src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java new file mode 100644 index 00000000..f2ebf42e --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/util/IpAddressExtractor.java @@ -0,0 +1,227 @@ +package com.juu.juulabel.common.util; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Utility class for IP address extraction and validation + */ +public final class IpAddressExtractor extends AbstractHttpUtil { + + private static final String UNKNOWN = "unknown"; + + // Ordered by reliability - most trusted first + private static final List IP_HEADER_CANDIDATES = List.of( + "CF-Connecting-IP", // Cloudflare (most reliable if using CF) + "True-Client-IP", // Akamai + "X-Real-IP", // Nginx proxy + "X-Forwarded-For", // Standard but easily spoofed + "X-Cluster-Client-IP", // Google Cloud + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_X_FORWARDED_FOR", + "HTTP_X_FORWARDED", + "HTTP_X_CLUSTER_CLIENT_IP", + "HTTP_CLIENT_IP", + "HTTP_FORWARDED_FOR", + "HTTP_FORWARDED", + "HTTP_VIA", + "REMOTE_ADDR"); + + // IPv4 pattern + private static final Pattern IPV4_PATTERN = Pattern.compile( + "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"); + + // IPv6 pattern (simplified) + private static final Pattern IPV6_PATTERN = Pattern.compile( + "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7}|:)|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4})$"); + + /** + * Private constructor to prevent instantiation + */ + private IpAddressExtractor() { + super(); + } + + /** + * Extract client IP address with validation and reliability checks + * + * @return most reliable client IP address found + */ + public static String getClientIpAddress() { + HttpServletRequest request = getCurrentRequest(); + + return IP_HEADER_CANDIDATES.stream() + .map(request::getHeader) + .filter(ip -> ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) + .map(ip -> ip.split(",")[0].trim()) // Take first IP from comma-separated list + .filter(IpAddressExtractor::isValidIpAddress) + .filter(IpAddressExtractor::isPublicIpAddress) // Prefer public IPs + .findFirst() + .orElseGet(() -> { + // Fallback: try to get any valid IP (including private) + String fallbackIp = IP_HEADER_CANDIDATES.stream() + .map(request::getHeader) + .filter(ip -> ip != null && !ip.isEmpty() && !UNKNOWN.equalsIgnoreCase(ip)) + .map(ip -> ip.split(",")[0].trim()) + .filter(IpAddressExtractor::isValidIpAddress) + .findFirst() + .orElse(request.getRemoteAddr()); + + return fallbackIp != null ? fallbackIp : "unknown"; + }); + } + + /** + * Get client IP with reliability score for monitoring/logging + */ + public static IpAddressInfo getClientIpAddressWithInfo() { + HttpServletRequest request = getCurrentRequest(); + + for (int i = 0; i < IP_HEADER_CANDIDATES.size(); i++) { + String headerName = IP_HEADER_CANDIDATES.get(i); + String headerValue = request.getHeader(headerName); + + if (headerValue != null && !headerValue.isEmpty() && !UNKNOWN.equalsIgnoreCase(headerValue)) { + String ip = headerValue.split(",")[0].trim(); + if (isValidIpAddress(ip)) { + ReliabilityLevel reliability = getReliabilityLevel(headerName, ip); + return new IpAddressInfo(ip, headerName, reliability); + } + } + } + + String remoteAddr = request.getRemoteAddr(); + return new IpAddressInfo( + remoteAddr != null ? remoteAddr : "unknown", + "REMOTE_ADDR", + ReliabilityLevel.LOW); + } + + /** + * Validate if string is a valid IP address (IPv4 or IPv6) + */ + private static boolean isValidIpAddress(String ip) { + if (ip == null || ip.trim().isEmpty()) { + return false; + } + + try { + InetAddress.getByName(ip); + return IPV4_PATTERN.matcher(ip).matches() || IPV6_PATTERN.matcher(ip).matches(); + } catch (UnknownHostException e) { + return false; + } + } + + /** + * Check if IP address is public (not private/local) + */ + private static boolean isPublicIpAddress(String ip) { + if (!isValidIpAddress(ip)) { + return false; + } + + return !isPrivateIpAddress(ip) && !isSpecialAddress(ip); + } + + /** + * Check if IP is in private ranges + */ + private static boolean isPrivateIpAddress(String ip) { + // Check IPv6 private ranges first + if (ip.contains(":")) { + return isPrivateIpv6(ip); + } + + if (ip.startsWith("10.") || ip.startsWith("192.168.")) { + return true; + } + if (ip.startsWith("172.")) { + return isPrivate172Range(ip); + } + return false; + } + + private static boolean isPrivateIpv6(String ip) { + try { + InetAddress addr = InetAddress.getByName(ip); + return addr.isSiteLocalAddress() + || addr.isLinkLocalAddress() + || ip.toLowerCase().startsWith("fc") + || ip.toLowerCase().startsWith("fd"); + } catch (UnknownHostException e) { + return false; + } + } + + /** + * Check if 172.x.x.x IP is in private range (172.16.0.0 to 172.31.255.255) + */ + private static boolean isPrivate172Range(String ip) { + String[] octets = ip.split("\\."); + if (octets.length < 2) { + return false; + } + + try { + int secondOctet = Integer.parseInt(octets[1]); + return secondOctet >= 16 && secondOctet <= 31; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * Check if IP is localhost or other special addresses + */ + private static boolean isSpecialAddress(String ip) { + return ip.equals("127.0.0.1") || ip.equals("::1") || ip.equals("0.0.0.0"); + } + + private static ReliabilityLevel getReliabilityLevel(String headerName, String ip) { + // Rate headers by trustworthiness + return switch (headerName) { + case "CF-Connecting-IP", "True-Client-IP" -> ReliabilityLevel.HIGH; + case "X-Real-IP", "X-Cluster-Client-IP" -> ReliabilityLevel.MEDIUM; + case "X-Forwarded-For" -> isPublicIpAddress(ip) ? ReliabilityLevel.MEDIUM : ReliabilityLevel.LOW; + default -> ReliabilityLevel.LOW; + }; + } + + /** + * Data class for IP address information + */ + public static class IpAddressInfo { + private final String ipAddress; + private final String sourceHeader; + private final ReliabilityLevel reliability; + + public IpAddressInfo(String ipAddress, String sourceHeader, ReliabilityLevel reliability) { + this.ipAddress = ipAddress; + this.sourceHeader = sourceHeader; + this.reliability = reliability; + } + + public String getIpAddress() { + return ipAddress; + } + + public String getSourceHeader() { + return sourceHeader; + } + + public ReliabilityLevel getReliability() { + return reliability; + } + } + + public enum ReliabilityLevel { + HIGH, // Cloudflare, Akamai - very reliable + MEDIUM, // Nginx, proper proxies - generally reliable + LOW // Easy to spoof headers - use with caution + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/common/util/UserAgentExtractor.java b/src/main/java/com/juu/juulabel/common/util/UserAgentExtractor.java new file mode 100644 index 00000000..e9acf5fc --- /dev/null +++ b/src/main/java/com/juu/juulabel/common/util/UserAgentExtractor.java @@ -0,0 +1,27 @@ +package com.juu.juulabel.common.util; + +import org.springframework.http.HttpHeaders; +import jakarta.servlet.http.HttpServletRequest; + +/** + * Utility class for user agent extraction + */ +public final class UserAgentExtractor extends AbstractHttpUtil { + + /** + * Private constructor to prevent instantiation + */ + private UserAgentExtractor() { + super(); + } + + /** + * Extract user agent from request headers + * + * @return user agent string from User-Agent header + */ + public static String getUserAgent() { + HttpServletRequest request = getCurrentRequest(); + return request.getHeader(HttpHeaders.USER_AGENT); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/member/controller/MemberController.java b/src/main/java/com/juu/juulabel/member/controller/MemberController.java index bf6c6796..049199ac 100644 --- a/src/main/java/com/juu/juulabel/member/controller/MemberController.java +++ b/src/main/java/com/juu/juulabel/member/controller/MemberController.java @@ -8,153 +8,114 @@ import com.juu.juulabel.member.domain.Member; import com.juu.juulabel.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; -@Tag( - name = "회원 API", - description = "로그인,회원가입,프로필 수정,내가 작성한 게시글 조회 등 회원 관련 API" -) +@Tag(name = "회원 API", description = "프로필 수정, 내 정보 조회 등 회원 관련 API") @RestController -@RequestMapping(value = {"/v1/api/members"}) +@RequestMapping(value = { "/v1/api/members" }) @RequiredArgsConstructor public class MemberController { private final MemberService memberService; - @Operation(summary = "카카오 로그인") - @PostMapping("/login/kakao") - public ResponseEntity> kakaoLogin(@Valid @RequestBody OAuthLoginRequest request) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.login(request)); - } - - @Operation(summary = "구글 로그인") - @PostMapping("/login/google") - public ResponseEntity> googleLogin(@Valid @RequestBody OAuthLoginRequest request) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.login(request)); - } - - @Operation(summary = "회원가입") - @PostMapping("/sign-up") - public ResponseEntity> signUp(@Valid @RequestBody SignUpMemberRequest request) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.signUp(request)); - } - @Operation(summary = "닉네임 중복 검사") @GetMapping("/nicknames/{nickname}/exists") - public ResponseEntity> checkNickname(@NotNull @PathVariable String nickname) { + public ResponseEntity> checkNickname(@PathVariable String nickname) { return CommonResponse.success(SuccessCode.SUCCESS, memberService.checkNickname(nickname)); } @Operation(summary = "프로필 수정") @PutMapping("/me/profile") public ResponseEntity> updateProfile( - @AuthenticationPrincipal Member member, - @Valid @RequestPart(value = "request") UpdateProfileRequest request, - @RequestPart(value = "image", required = false) MultipartFile image - ) { + @AuthenticationPrincipal Member member, + @Valid @RequestPart(value = "request") UpdateProfileRequest request, + @RequestPart(value = "image", required = false) MultipartFile image) { return CommonResponse.success(SuccessCode.SUCCESS, memberService.updateProfile(member, request, image)); } + @Operation(summary = "내 정보 조회") + @GetMapping("/my-info") + public ResponseEntity> getMyInfo(@AuthenticationPrincipal Member member) { + return CommonResponse.success(SuccessCode.SUCCESS, memberService.getMyInfo(member)); + } + + @Operation(summary = "내 공간 조회") + @GetMapping("/my-space") + public ResponseEntity> getMySpace(@AuthenticationPrincipal Member member) { + return CommonResponse.success(SuccessCode.SUCCESS, memberService.getMySpace(member)); + } + + @Operation(summary = "타 유저 프로필 조회") + @GetMapping("/{memberId}/profile") + public ResponseEntity> getMemberProfile( + @AuthenticationPrincipal Member member, + @PathVariable Long memberId) { + return CommonResponse.success(SuccessCode.SUCCESS, memberService.getMemberProfile(member, memberId)); + } + @Operation(summary = "내가 작성한 일상생활 목록 조회") @Parameters(@Parameter(name = "request", description = "내가 작성한 일상생활 목록 조회 요청", required = true)) @GetMapping("/daily-lives/my") public ResponseEntity> loadMyDailyLifeList( - @AuthenticationPrincipal Member member, - @Valid DailyLifeListRequest request - ) { + @AuthenticationPrincipal Member member, + @Valid DailyLifeListRequest request) { return CommonResponse.success(SuccessCode.SUCCESS, memberService.loadMyDailyLifeList(member, request)); } @Operation(summary = "내가 작성한 시음노트 목록 조회") @Parameters(@Parameter(name = "request", description = "내가 작성한 시음노트 목록 조회 요청", required = true)) - @GetMapping("/tasting_notes/my") + @GetMapping("/tasting-notes/my") public ResponseEntity> loadMyTastingNoteList( - @AuthenticationPrincipal Member member, - @Valid TastingNoteListRequest request - ) { + @AuthenticationPrincipal Member member, + @Valid TastingNoteListRequest request) { return CommonResponse.success(SuccessCode.SUCCESS, memberService.loadMyTastingNoteList(member, request)); } - @Operation(hidden = true, summary = "전통주 저장") - @PostMapping("/{alcoholicDrinksId}/save") + @Operation(summary = "전통주 저장") + @PostMapping("/alcoholic-drinks/{alcoholicDrinksId}/save") public ResponseEntity> saveAlcoholicDrinks( - @AuthenticationPrincipal Member member, - @PathVariable Long alcoholicDrinksId - ) { + @AuthenticationPrincipal Member member, + @PathVariable Long alcoholicDrinksId) { boolean isSaved = memberService.saveAlcoholicDrinks(member, alcoholicDrinksId); return CommonResponse.success(isSaved ? SuccessCode.SUCCESS_INSERT : SuccessCode.SUCCESS_DELETE); } - @Operation(hidden = true, summary = "내가 저장한 전통주 목록 조회") + @Operation(summary = "내가 저장한 전통주 목록 조회") @Parameters(@Parameter(name = "request", description = "내가 저장한 전통주 목록 조회 요청", required = true)) @GetMapping("/alcoholic-drinks/my") public ResponseEntity> loadMyAlcoholicDrinks( - @AuthenticationPrincipal Member member, - @Valid MyAlcoholicDrinksListRequest request - ) { + @AuthenticationPrincipal Member member, + @Valid MyAlcoholicDrinksListRequest request) { return CommonResponse.success(SuccessCode.SUCCESS, memberService.loadMyAlcoholicDrinks(member, request)); } - @Operation(summary = "내 공간 조회") - @GetMapping("/my-space") - public ResponseEntity> getMySpace(@AuthenticationPrincipal Member member) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.getMySpace(member)); - } - - @Operation(summary = "내 정보 조회") - @GetMapping("/my-info") - public ResponseEntity> getMyInfo(@AuthenticationPrincipal Member member) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.getMyInfo(member)); - } - - @Operation(summary = "타 유저 프로필 조회") - @GetMapping("/{memberId}/profile") - public ResponseEntity> getMemberProfile( - @AuthenticationPrincipal Member member, - @PathVariable Long memberId - ) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.getMemberProfile(member, memberId)); - } - @Operation(summary = "특정 회원이 작성한 시음노트 목록 조회") @Parameters(@Parameter(name = "request", description = "특정 회원이 작성한 시음노트 목록 조회 요청", required = true)) - @GetMapping("/{memberId}/tasting_notes") + @GetMapping("/{memberId}/tasting-notes") public ResponseEntity> loadMemberTastingNoteList( - @AuthenticationPrincipal Member member, - @Valid TastingNoteListRequest request, - @PathVariable Long memberId - ) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.loadMemberTastingNoteList(member, request, memberId)); + @AuthenticationPrincipal Member member, + @Valid TastingNoteListRequest request, + @PathVariable Long memberId) { + return CommonResponse.success(SuccessCode.SUCCESS, + memberService.loadMemberTastingNoteList(member, request, memberId)); } @Operation(summary = "특정 회원이 작성한 일상생활 목록 조회") @Parameters(@Parameter(name = "request", description = "특정 회원이 작성한 일상생활 목록 조회 요청", required = true)) @GetMapping("/{memberId}/daily-lives") public ResponseEntity> loadMemberDailyLifeList( - @AuthenticationPrincipal Member member, - @Valid DailyLifeListRequest request, - @PathVariable Long memberId - ) { - return CommonResponse.success(SuccessCode.SUCCESS, memberService.loadMemberDailyLifeList(member, request, memberId)); + @AuthenticationPrincipal Member member, + @Valid DailyLifeListRequest request, + @PathVariable Long memberId) { + return CommonResponse.success(SuccessCode.SUCCESS, + memberService.loadMemberDailyLifeList(member, request, memberId)); } - - @Operation(summary = "회원 탈퇴") - @DeleteMapping("/me") - public ResponseEntity> deleteAccount( - @AuthenticationPrincipal Member member, - @RequestBody WithdrawalRequest request - ) { - memberService.deleteAccount(member, request); - return CommonResponse.success(SuccessCode.SUCCESS_DELETE); - } - } diff --git a/src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java b/src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java index a2b9330b..647ae475 100644 --- a/src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java +++ b/src/main/java/com/juu/juulabel/member/request/OAuthUserInfo.java @@ -3,6 +3,7 @@ import com.juu.juulabel.member.domain.Provider; public record OAuthUserInfo( + Long memberId, String email, String providerId, Provider provider diff --git a/src/main/java/com/juu/juulabel/member/service/MemberContentService.java b/src/main/java/com/juu/juulabel/member/service/MemberContentService.java new file mode 100644 index 00000000..44b1eb86 --- /dev/null +++ b/src/main/java/com/juu/juulabel/member/service/MemberContentService.java @@ -0,0 +1,133 @@ +package com.juu.juulabel.member.service; + +import com.juu.juulabel.alcohol.domain.AlcoholicDrinks; +import com.juu.juulabel.alcohol.repository.AlcoholicDrinksReader; +import com.juu.juulabel.alcohol.repository.TastingNoteReader; +import com.juu.juulabel.alcohol.response.AlcoholicDrinksSummary; +import com.juu.juulabel.common.dto.request.MyAlcoholicDrinksListRequest; +import com.juu.juulabel.common.dto.request.TastingNoteListRequest; +import com.juu.juulabel.common.dto.response.DailyLifeListResponse; +import com.juu.juulabel.common.dto.response.MyAlcoholicDrinksListResponse; +import com.juu.juulabel.common.dto.response.MyDailyLifeListResponse; +import com.juu.juulabel.common.dto.response.MyTastingNoteListResponse; +import com.juu.juulabel.common.dto.response.TastingNoteListResponse; +import com.juu.juulabel.dailylife.repository.DailyLifeReader; +import com.juu.juulabel.dailylife.response.DailyLifeListRequest; +import com.juu.juulabel.dailylife.response.DailyLifeSummary; +import com.juu.juulabel.dailylife.response.MyDailyLifeSummary; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.domain.MemberAlcoholicDrinks; +import com.juu.juulabel.member.repository.MemberAlcoholicDrinksReader; +import com.juu.juulabel.member.repository.MemberAlcoholicDrinksWriter; +import com.juu.juulabel.tastingnote.request.MyTastingNoteSummary; +import com.juu.juulabel.tastingnote.request.TastingNoteSummary; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/** + * 회원 콘텐츠 관리 서비스 + * 일상생활, 시음노트, 전통주 관련 작업을 처리합니다. + */ +@Service +@RequiredArgsConstructor +public class MemberContentService { + + // 일상생활, 시음노트 관련 + private final DailyLifeReader dailyLifeReader; + private final TastingNoteReader tastingNoteReader; + + // 전통주 관련 + private final AlcoholicDrinksReader alcoholicDrinksReader; + private final MemberAlcoholicDrinksReader memberAlcoholicDrinksReader; + private final MemberAlcoholicDrinksWriter memberAlcoholicDrinksWriter; + + // ===== 일상생활 관련 메소드 ===== + + /** + * 내가 작성한 일상생활 목록 조회 + */ + @Transactional(readOnly = true) + public MyDailyLifeListResponse loadMyDailyLifeList(Member member, DailyLifeListRequest request) { + Slice myDailyLifeList = dailyLifeReader.getAllMyDailyLives(member, + request.lastDailyLifeId(), request.pageSize()); + + return new MyDailyLifeListResponse(myDailyLifeList); + } + + /** + * 특정 회원이 작성한 일상생활 목록 조회 + */ + @Transactional(readOnly = true) + public DailyLifeListResponse loadMemberDailyLifeList(Member loginMember, DailyLifeListRequest request, + Long memberId) { + Slice dailyLifeList = dailyLifeReader.getAllDailyLivesByMember(loginMember, memberId, + request.lastDailyLifeId(), request.pageSize()); + + return new DailyLifeListResponse(dailyLifeList); + } + + // ===== 시음노트 관련 메소드 ===== + + /** + * 내가 작성한 시음노트 목록 조회 + */ + @Transactional(readOnly = true) + public MyTastingNoteListResponse loadMyTastingNoteList(Member member, TastingNoteListRequest request) { + Slice myTastingNoteList = tastingNoteReader.getAllMyTastingNotes(member, + request.lastTastingNoteId(), request.pageSize()); + + return new MyTastingNoteListResponse(myTastingNoteList); + } + + /** + * 특정 회원이 작성한 시음노트 목록 조회 + */ + @Transactional(readOnly = true) + public TastingNoteListResponse loadMemberTastingNoteList(Member loginMember, TastingNoteListRequest request, + Long memberId) { + Slice tastingNoteList = tastingNoteReader.getAllTastingNotesByMember(loginMember, memberId, + request.lastTastingNoteId(), request.pageSize()); + + return new TastingNoteListResponse(tastingNoteList); + } + + // ===== 전통주 관련 메소드 ===== + + /** + * 전통주 저장하기 또는 저장 취소 + * + * @return true if saved, false if unsaved + */ + @Transactional + public boolean saveAlcoholicDrinks(Member member, Long alcoholicDrinksId) { + AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getById(alcoholicDrinksId); + Optional memberAlcoholicDrinks = memberAlcoholicDrinksReader + .findByMemberAndAlcoholicDrinks(member, alcoholicDrinks); + + // 전통주가 이미 저장되어 있다면 삭제, 저장되어 있지 않다면 등록 + return memberAlcoholicDrinks + .map(save -> { + memberAlcoholicDrinksWriter.delete(save); + return false; + }) + .orElseGet(() -> { + memberAlcoholicDrinksWriter.store(member, alcoholicDrinks); + return true; + }); + } + + /** + * 내가 저장한 전통주 목록 조회 + */ + @Transactional(readOnly = true) + public MyAlcoholicDrinksListResponse loadMyAlcoholicDrinks(Member member, MyAlcoholicDrinksListRequest request) { + Slice alcoholicDrinksSummaries = alcoholicDrinksReader.getAllMyAlcoholicDrinks(member, + request.lastAlcoholicDrinksId(), request.pageSize()); + + return new MyAlcoholicDrinksListResponse(alcoholicDrinksSummaries); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/member/service/MemberLookupService.java b/src/main/java/com/juu/juulabel/member/service/MemberLookupService.java new file mode 100644 index 00000000..adac1790 --- /dev/null +++ b/src/main/java/com/juu/juulabel/member/service/MemberLookupService.java @@ -0,0 +1,41 @@ +package com.juu.juulabel.member.service; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.repository.MemberReader; +import com.juu.juulabel.member.repository.jpa.MemberJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 회원 조회 서비스 + */ +@Service +@RequiredArgsConstructor +public class MemberLookupService { + + private final MemberReader memberReader; + private final MemberJpaRepository memberJpaRepository; + + /** + * ID로 회원 조회 + */ + @Transactional(readOnly = true) + @Cacheable(value = "memberById", key = "#memberId", unless = "#result == null") + public Member findById(Long memberId) { + return memberJpaRepository.findById(memberId) + .orElseThrow(() -> new BaseException(ErrorCode.MEMBER_NOT_FOUND)); + } + + /** + * 이메일로 회원 조회 + */ + @Transactional(readOnly = true) + @Cacheable(value = "memberByEmail", key = "#email", unless = "#result == null") + public Member getMemberByEmail(String email) { + return memberReader.getByEmail(email); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/member/service/MemberProfileService.java b/src/main/java/com/juu/juulabel/member/service/MemberProfileService.java new file mode 100644 index 00000000..a99cb6eb --- /dev/null +++ b/src/main/java/com/juu/juulabel/member/service/MemberProfileService.java @@ -0,0 +1,165 @@ +package com.juu.juulabel.member.service; + +import com.juu.juulabel.alcohol.repository.AlcoholTypeReader; +import com.juu.juulabel.alcohol.repository.TastingNoteReader; +import com.juu.juulabel.common.dto.request.UpdateProfileRequest; +import com.juu.juulabel.common.dto.response.MemberProfileResponse; +import com.juu.juulabel.common.dto.response.MyInfoResponse; +import com.juu.juulabel.common.dto.response.MySpaceResponse; +import com.juu.juulabel.common.dto.response.UpdateProfileResponse; +import com.juu.juulabel.dailylife.repository.DailyLifeReader; +import com.juu.juulabel.follow.repository.FollowReader; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.domain.MemberAlcoholType; +import com.juu.juulabel.member.repository.MemberAlcoholTypeReader; +import com.juu.juulabel.member.repository.MemberAlcoholTypeWriter; +import com.juu.juulabel.member.repository.MemberReader; +import com.juu.juulabel.member.util.MemberUtils; +import com.juu.juulabel.s3.S3Service; +import com.juu.juulabel.s3.UploadImageInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 회원 프로필 관리 서비스 + */ +@Service +@RequiredArgsConstructor +public class MemberProfileService { + + private final MemberReader memberReader; + private final MemberAlcoholTypeWriter memberAlcoholTypeWriter; + private final MemberAlcoholTypeReader memberAlcoholTypeReader; + private final AlcoholTypeReader alcoholTypeReader; + private final S3Service s3Service; + private final DailyLifeReader dailyLifeReader; + private final TastingNoteReader tastingNoteReader; + private final FollowReader followReader; + private final MemberUtils memberUtils; + + /** + * 닉네임 중복 확인 + */ + @Transactional(readOnly = true) + public boolean checkNickname(String nickname) { + return memberReader.existActiveNickname(nickname); + } + + /** + * 프로필 수정 + */ + @Transactional + public UpdateProfileResponse updateProfile(Member loginMember, UpdateProfileRequest request, MultipartFile image) { + Member member = memberReader.getByEmail(loginMember.getEmail()); + String profileImageUrl = processProfileImage(image); + + // 프로필 업데이트 + member.updateProfile(request, profileImageUrl); + + memberAlcoholTypeWriter.deleteAllByMember(member); + + // 알콜 타입 업데이트 + updateMemberAlcoholTypes(member, request.alcoholTypeIds()); + + return new UpdateProfileResponse(member.getId()); + } + + /** + * 프로필 이미지 처리 + */ + private String processProfileImage(MultipartFile image) { + if (image != null && !image.isEmpty()) { + UploadImageInfo uploadImageInfo = s3Service.uploadMemberProfileImage(image); + return uploadImageInfo.ImageUrl(); + } + return null; + } + + /** + * 회원의 알콜 타입 업데이트 + */ + private void updateMemberAlcoholTypes(Member member, List alcoholTypeIds) { + if (!CollectionUtils.isEmpty(alcoholTypeIds)) { + List memberAlcoholTypeList = memberUtils.getMemberAlcoholTypeList( + member, alcoholTypeIds, alcoholTypeReader); + if (!memberAlcoholTypeList.isEmpty()) { + memberAlcoholTypeWriter.storeAll(memberAlcoholTypeList); + } + } + } + + /** + * 내 공간 정보 조회 + */ + @Transactional(readOnly = true) + public MySpaceResponse getMySpace(Member loginMember) { + Member member = memberReader.getById(loginMember.getId()); + long tastingNoteCount = tastingNoteReader.getMyTastingNoteCount(member); + long dailyLifeCount = dailyLifeReader.getMyDailyLifeCount(member); + long followingCount = followReader.countFollowing(member); + long followerCount = followReader.countFollower(member); + + return new MySpaceResponse( + member.getId(), + member.getProfileImage(), + member.getNickname(), + member.getIntroduction(), + member.isHasBadge(), + tastingNoteCount, + dailyLifeCount, + followingCount, + followerCount, + 0); + } + + /** + * 내 정보 조회 + */ + @Transactional(readOnly = true) + public MyInfoResponse getMyInfo(Member loginMember) { + Member member = memberReader.getById(loginMember.getId()); + List alcoholTypeIdList = memberAlcoholTypeReader.getIdListByMember(member); + return new MyInfoResponse( + member.getId(), + member.getNickname(), + member.getEmail(), + member.isHasBadge(), + member.isNotificationsAllowed(), + member.getIntroduction(), + member.getProfileImage(), + member.getGender(), + alcoholTypeIdList); + } + + /** + * 타 유저 프로필 조회 + */ + @Transactional(readOnly = true) + @Cacheable(value = "memberProfile", key = "#memberId", unless = "#result == null") + public MemberProfileResponse getMemberProfile(Member loginMember, Long memberId) { + Member member = memberReader.getById(memberId); + long tastingNoteCount = tastingNoteReader.getTastingNoteCountByMemberId(memberId, loginMember); + long dailyLifeCount = dailyLifeReader.getDailyLifeCountByMemberId(memberId, loginMember); + long followingCount = followReader.countFollowing(member); + long followerCount = followReader.countFollower(member); + boolean isFollowing = followReader.isFollowing(loginMember, member); + + return new MemberProfileResponse( + member.getId(), + member.getNickname(), + member.getProfileImage(), + member.getIntroduction(), + member.isHasBadge(), + tastingNoteCount, + dailyLifeCount, + followingCount, + followerCount, + isFollowing); + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/member/service/MemberService.java b/src/main/java/com/juu/juulabel/member/service/MemberService.java index bddf38a5..c504938d 100644 --- a/src/main/java/com/juu/juulabel/member/service/MemberService.java +++ b/src/main/java/com/juu/juulabel/member/service/MemberService.java @@ -1,364 +1,131 @@ package com.juu.juulabel.member.service; -import com.juu.juulabel.alcohol.domain.AlcoholType; -import com.juu.juulabel.alcohol.domain.AlcoholicDrinks; -import com.juu.juulabel.alcohol.repository.AlcoholTypeReader; -import com.juu.juulabel.alcohol.repository.AlcoholicDrinksReader; -import com.juu.juulabel.alcohol.repository.TastingNoteReader; -import com.juu.juulabel.alcohol.response.AlcoholicDrinksSummary; -import com.juu.juulabel.common.constants.AuthConstants; import com.juu.juulabel.common.dto.request.*; import com.juu.juulabel.common.dto.response.*; -import com.juu.juulabel.common.exception.BaseException; -import com.juu.juulabel.common.exception.InvalidParamException; -import com.juu.juulabel.common.exception.code.ErrorCode; -import com.juu.juulabel.common.factory.OAuthProviderFactory; -import com.juu.juulabel.common.provider.JwtTokenProvider; -import com.juu.juulabel.dailylife.repository.DailyLifeReader; import com.juu.juulabel.dailylife.response.DailyLifeListRequest; -import com.juu.juulabel.dailylife.response.DailyLifeSummary; -import com.juu.juulabel.dailylife.response.MyDailyLifeSummary; -import com.juu.juulabel.follow.repository.FollowReader; import com.juu.juulabel.member.domain.*; -import com.juu.juulabel.member.repository.*; -import com.juu.juulabel.member.repository.jpa.MemberJpaRepository; -import com.juu.juulabel.member.request.OAuthLoginInfo; -import com.juu.juulabel.member.request.OAuthUser; -import com.juu.juulabel.member.request.OAuthUserInfo; -import com.juu.juulabel.member.token.Token; -import com.juu.juulabel.s3.S3Service; -import com.juu.juulabel.s3.UploadImageInfo; -import com.juu.juulabel.tastingnote.request.MyTastingNoteSummary; -import com.juu.juulabel.tastingnote.request.TastingNoteSummary; -import com.juu.juulabel.terms.domain.Terms; -import com.juu.juulabel.terms.request.TermsAgreement; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - +/** + * 회원 서비스 파사드 (Facade) 클래스 + * 다른 회원 관련 서비스 클래스들에 위임하는 역할을 담당 + */ @Service @RequiredArgsConstructor public class MemberService { - private final OAuthProviderFactory providerFactory; - private final JwtTokenProvider jwtTokenProvider; - private final MemberReader memberReader; - private final MemberWriter memberWriter; - private final TermsReader termsReader; - private final MemberTermsWriter memberTermsWriter; - private final MemberAlcoholTypeWriter memberAlcoholTypeWriter; - private final MemberAlcoholTypeReader memberAlcoholTypeReader; - private final AlcoholTypeReader alcoholTypeReader; - private final S3Service s3Service; - private final DailyLifeReader dailyLifeReader; - private final AlcoholicDrinksReader alcoholicDrinksReader; - private final MemberAlcoholicDrinksReader memberAlcoholicDrinksReader; - private final MemberAlcoholicDrinksWriter memberAlcoholicDrinksWriter; - private final TastingNoteReader tastingNoteReader; - private final WithdrawalRecordWriter withdrawalRecordWriter; - private final WithdrawalRecordReader withdrawalRecordReader; - private final FollowReader followReader; - private final MemberJpaRepository memberJpaRepository; - - - public Member getMemberByEmail(String email) { - return memberReader.getByEmail(email); - } - - @Transactional - public LoginResponse login(OAuthLoginRequest oAuthLoginRequest) { - OAuthLoginInfo authLoginInfo = oAuthLoginRequest.toDto(); - Provider provider = authLoginInfo.provider(); - - // 인가 코드를 이용해 토큰 발급 요청 - String accessToken = providerFactory.getAccessToken( - provider, - authLoginInfo.propertyMap().get(AuthConstants.REDIRECT_URI), - authLoginInfo.propertyMap().get(AuthConstants.CODE) - ); - - // 토큰을 이용해 사용자 정보 가져오기 - OAuthUser oAuthUser = providerFactory.getOAuthUser(provider, accessToken); - - // 회원가입 or 로그인 - String email = oAuthUser.email(); - boolean isNewMember = !memberReader.existsByEmailAndProvider(email, provider); - - validateNotWithdrawnMember(email); - - String generatedToken = jwtTokenProvider.createAccessToken(getMemberByEmail(email)); - Token token; - if (isNewMember) { - token = new Token(null, null); - } else { - token = new Token(generatedToken, jwtTokenProvider.getExpirationByToken(generatedToken)); - } - return new LoginResponse( - token, - isNewMember, - new OAuthUserInfo(email, oAuthUser.id(), provider) - ); - } - - @Transactional - public SignUpMemberResponse signUp(SignUpMemberRequest signUpRequest) { - validateNickname(signUpRequest.nickname()); - validateEmail(signUpRequest.email()); - - Member member = Member.create(signUpRequest); - memberWriter.store(member); - - // 선호전통주 주종 등록 - List memberAlcoholTypeList = - getMemberAlcoholTypeList(member, signUpRequest.alcoholTypeIds()); - if (!memberAlcoholTypeList.isEmpty()) { - memberAlcoholTypeWriter.storeAll(memberAlcoholTypeList); - } - - // 약관 등록 - List memberTerms = - getAndValidateTermsWithMapping(member, signUpRequest.termsAgreements()); - if (!memberTerms.isEmpty()) { - memberTermsWriter.storeAll(memberTerms); - } - - String token = jwtTokenProvider.createAccessToken(member); - - return new SignUpMemberResponse( - member.getId(), - new Token(token, jwtTokenProvider.getExpirationByToken(token)) - ); - } - - - private List getMemberAlcoholTypeList(Member member, List alcoholTypeIdList) { - return alcoholTypeIdList.stream() - .map(alcoholTypeId -> { - AlcoholType alcoholType = alcoholTypeReader.getById(alcoholTypeId); - return MemberAlcoholType.create(member, alcoholType); - }) - .toList(); - } - - private List getAndValidateTermsWithMapping(Member member, List termsAgreements) { - List usedTermsList = termsReader.getAllByIsUsed(); - // 사용중인 약관이 존재하지 않을 경우 생성하지 않는다. - if (!usedTermsList.isEmpty()) { - validateTermsList(usedTermsList, termsAgreements); - } - - return getMemberTermsList(member, usedTermsList, termsAgreements); - } - - private void validateTermsList(List usedTermsList, List termsAgreements) { - if (usedTermsList.size() != termsAgreements.size()) { - throw new InvalidParamException(ErrorCode.TERMS_AGREEMENT_MISMATCH); - } - } - - private List getMemberTermsList(Member member, List usedTermsList, List termsAgreements) { - List mappings = new ArrayList<>(); - final LocalDateTime now = LocalDateTime.now(); - - usedTermsList.forEach(terms -> { - TermsAgreement termsAgreement = termsAgreements.stream() - .filter(agreement -> agreement.termsId().equals(terms.getId())) - .findFirst() - .orElseThrow(() -> new InvalidParamException(ErrorCode.TERMS_NOT_FOUND)); - - final boolean isAgreed = termsAgreement.isAgreed(); - - if (terms.isRequired() && !isAgreed) { - throw new InvalidParamException(ErrorCode.TERMS_AGREEMENT_MISSING_REQUIRED); - } - - mappings.add(MemberTerms.create(member, terms, isAgreed, now)); - }); - - return mappings; - } + private final MemberProfileService memberProfileService; + private final MemberLookupService memberLookupService; + private final MemberContentService memberContentService; + /** + * 닉네임 중복 확인 + */ @Transactional(readOnly = true) public boolean checkNickname(String nickname) { - return memberReader.existActiveNickname(nickname); + return memberProfileService.checkNickname(nickname); } + /** + * 프로필 수정 + */ @Transactional public UpdateProfileResponse updateProfile(Member loginMember, UpdateProfileRequest request, MultipartFile image) { - Member member = memberReader.getByEmail(loginMember.getEmail()); - - String profileImageUrl = null; - if (image != null) { - UploadImageInfo uploadImageInfo = s3Service.uploadMemberProfileImage(image); - profileImageUrl = uploadImageInfo.ImageUrl(); - } - member.updateProfile(request, profileImageUrl); - - memberAlcoholTypeWriter.deleteAllByMember(member); - - List memberAlcoholTypeList = getMemberAlcoholTypeList(member, request.alcoholTypeIds()); - if (!memberAlcoholTypeList.isEmpty()) { - memberAlcoholTypeWriter.storeAll(memberAlcoholTypeList); - } - - return new UpdateProfileResponse(member.getId()); + return memberProfileService.updateProfile(loginMember, request, image); } + /** + * 내 공간 정보 조회 + */ @Transactional(readOnly = true) - public MyDailyLifeListResponse loadMyDailyLifeList(Member member, DailyLifeListRequest request) { - Slice myDailyLifeList = - dailyLifeReader.getAllMyDailyLives(member, request.lastDailyLifeId(), request.pageSize()); - - return new MyDailyLifeListResponse(myDailyLifeList); + public MySpaceResponse getMySpace(Member loginMember) { + return memberProfileService.getMySpace(loginMember); } + /** + * 내 정보 조회 + */ @Transactional(readOnly = true) - public MyTastingNoteListResponse loadMyTastingNoteList(Member member, TastingNoteListRequest request) { - Slice myTastingNoteList = - tastingNoteReader.getAllMyTastingNotes(member, request.lastTastingNoteId(), request.pageSize()); - - return new MyTastingNoteListResponse(myTastingNoteList); - } - - @Transactional - public boolean saveAlcoholicDrinks(Member member, Long alcoholicDrinksId) { - AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getById(alcoholicDrinksId); - Optional memberAlcoholicDrinks = - memberAlcoholicDrinksReader.findByMemberAndAlcoholicDrinks(member, alcoholicDrinks); - - // 전통주가 이미 저장되어 있다면 삭제, 저장되어 있지 않다면 등록 - return memberAlcoholicDrinks - .map(save -> { - memberAlcoholicDrinksWriter.delete(save); - return false; - }) - .orElseGet(() -> { - memberAlcoholicDrinksWriter.store(member, alcoholicDrinks); - return true; - }); + public MyInfoResponse getMyInfo(Member loginMember) { + return memberProfileService.getMyInfo(loginMember); } + /** + * 타 유저 프로필 조회 + */ @Transactional(readOnly = true) - public MyAlcoholicDrinksListResponse loadMyAlcoholicDrinks(Member member, MyAlcoholicDrinksListRequest request) { - Slice alcoholicDrinksSummaries = - alcoholicDrinksReader.getAllMyAlcoholicDrinks(member, request.lastAlcoholicDrinksId(), request.pageSize()); - - return new MyAlcoholicDrinksListResponse(alcoholicDrinksSummaries); + public MemberProfileResponse getMemberProfile(Member loginMember, Long memberId) { + return memberProfileService.getMemberProfile(loginMember, memberId); } + /** + * ID로 회원 조회 + */ @Transactional(readOnly = true) - public MySpaceResponse getMySpace(Member member) { - long tastingNoteCount = tastingNoteReader.getMyTastingNoteCount(member); - long dailyLifeCount = dailyLifeReader.getMyDailyLifeCount(member); - long followingCount = followReader.countFollowing(member); - long followerCount = followReader.countFollower(member); - - return new MySpaceResponse( - member.getId(), - member.getProfileImage(), - member.getNickname(), - member.getIntroduction(), - member.isHasBadge(), - tastingNoteCount, - dailyLifeCount, - followingCount, - followerCount, - 0 - ); + public Member findById(Long memberId) { + return memberLookupService.findById(memberId); } + /** + * 이메일로 회원 조회 + */ @Transactional(readOnly = true) - public MyInfoResponse getMyInfo(Member member) { - List alcoholTypeIdList = memberAlcoholTypeReader.getIdListByMember(member); - return new MyInfoResponse( - member.getId(), - member.getNickname(), - member.getEmail(), - member.isHasBadge(), - member.isNotificationsAllowed(), - member.getIntroduction(), - member.getProfileImage(), - member.getGender(), - alcoholTypeIdList - ); + public Member getMemberByEmail(String email) { + return memberLookupService.getMemberByEmail(email); } + /** + * 내가 작성한 일상생활 목록 조회 + */ @Transactional(readOnly = true) - public DailyLifeListResponse loadMemberDailyLifeList(Member loginMember, DailyLifeListRequest request, Long memberId) { - Slice dailyLifeList = - dailyLifeReader.getAllDailyLivesByMember(loginMember, memberId, request.lastDailyLifeId(), request.pageSize()); - - return new DailyLifeListResponse(dailyLifeList); + public MyDailyLifeListResponse loadMyDailyLifeList(Member member, DailyLifeListRequest request) { + return memberContentService.loadMyDailyLifeList(member, request); } + /** + * 특정 회원이 작성한 일상생활 목록 조회 + */ @Transactional(readOnly = true) - public TastingNoteListResponse loadMemberTastingNoteList(Member loginMember, TastingNoteListRequest request, Long memberId) { - Slice tastingNoteList = - tastingNoteReader.getAllTastingNotesByMember(loginMember, memberId, request.lastTastingNoteId(), request.pageSize()); - - return new TastingNoteListResponse(tastingNoteList); + public DailyLifeListResponse loadMemberDailyLifeList(Member loginMember, DailyLifeListRequest request, + Long memberId) { + return memberContentService.loadMemberDailyLifeList(loginMember, request, memberId); } + /** + * 내가 작성한 시음노트 목록 조회 + */ @Transactional(readOnly = true) - public MemberProfileResponse getMemberProfile(Member loginMember, Long memberId) { - Member member = memberReader.getById(memberId); - long tastingNoteCount = tastingNoteReader.getTastingNoteCountByMemberId(memberId, loginMember); - long dailyLifeCount = dailyLifeReader.getDailyLifeCountByMemberId(memberId, loginMember); - long followingCount = followReader.countFollowing(member); - long followerCount = followReader.countFollower(member); - boolean isFollowing = followReader.isFollowing(loginMember, member); - - return new MemberProfileResponse( - member.getId(), - member.getNickname(), - member.getProfileImage(), - member.getIntroduction(), - member.isHasBadge(), - tastingNoteCount, - dailyLifeCount, - followingCount, - followerCount, - isFollowing - ); - } - - private void validateNickname(String nickname) { - if (memberReader.existActiveNickname(nickname)) { - throw new InvalidParamException(ErrorCode.MEMBER_NICKNAME_DUPLICATE); - } - } - - private void validateNotWithdrawnMember(String email) { - if (withdrawalRecordReader.existEmail(email)) { - throw new InvalidParamException(ErrorCode.MEMBER_WITHDRAWN); - } + public MyTastingNoteListResponse loadMyTastingNoteList(Member member, TastingNoteListRequest request) { + return memberContentService.loadMyTastingNoteList(member, request); } - private void validateEmail(String email) { - if (memberReader.existActiveEmail(email)) { - throw new InvalidParamException(ErrorCode.MEMBER_EMAIL_DUPLICATE); - } + /** + * 특정 회원이 작성한 시음노트 목록 조회 + */ + @Transactional(readOnly = true) + public TastingNoteListResponse loadMemberTastingNoteList(Member loginMember, TastingNoteListRequest request, + Long memberId) { + return memberContentService.loadMemberTastingNoteList(loginMember, request, memberId); } + /** + * 전통주 저장하기 또는 저장 취소 + * + * @return true if saved, false if unsaved + */ @Transactional - public void deleteAccount(Member loginMember, WithdrawalRequest request) { - loginMember.deleteAccount(); - withdrawalRecordWriter.store( - WithdrawalRecord.create(request.withdrawalReason(), loginMember.getEmail(), loginMember.getNickname()) - ); + public boolean saveAlcoholicDrinks(Member member, Long alcoholicDrinksId) { + return memberContentService.saveAlcoholicDrinks(member, alcoholicDrinksId); } + /** + * 내가 저장한 전통주 목록 조회 + */ @Transactional(readOnly = true) - public Member findById(Long memberId) { - return memberJpaRepository.findById(memberId) - .orElseThrow(() -> new BaseException(ErrorCode.MEMBER_NOT_FOUND)); + public MyAlcoholicDrinksListResponse loadMyAlcoholicDrinks(Member member, MyAlcoholicDrinksListRequest request) { + return memberContentService.loadMyAlcoholicDrinks(member, request); } } - diff --git a/src/main/java/com/juu/juulabel/member/util/MemberUtils.java b/src/main/java/com/juu/juulabel/member/util/MemberUtils.java new file mode 100644 index 00000000..b099a702 --- /dev/null +++ b/src/main/java/com/juu/juulabel/member/util/MemberUtils.java @@ -0,0 +1,123 @@ +package com.juu.juulabel.member.util; + +import com.juu.juulabel.alcohol.domain.AlcoholType; +import com.juu.juulabel.alcohol.repository.AlcoholTypeReader; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.domain.MemberAlcoholType; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.juu.juulabel.common.dto.request.SignUpMemberRequest; +import com.juu.juulabel.common.exception.InvalidParamException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.member.domain.MemberTerms; +import com.juu.juulabel.member.repository.MemberAlcoholTypeWriter; +import com.juu.juulabel.member.repository.MemberTermsWriter; +import com.juu.juulabel.member.repository.TermsReader; +import com.juu.juulabel.terms.domain.Terms; +import com.juu.juulabel.terms.request.TermsAgreement; + +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.Collections; + +/** + * Member 관련 유틸리티 클래스 + */ +@Component +@RequiredArgsConstructor +public class MemberUtils { + + /** + * 회원-주종 관계 목록 생성 + * + * @param member 회원 + * @param alcoholTypeIdList 주종 ID 목록 + * @param alcoholTypeReader 주종 조회 리포지토리 + * @return 회원-주종 관계 목록 + */ + private final TermsReader termsReader; + private final MemberAlcoholTypeWriter memberAlcoholTypeWriter; + private final AlcoholTypeReader alcoholTypeReader; + private final MemberTermsWriter memberTermsWriter; + + public List getMemberAlcoholTypeList(Member member, List alcoholTypeIdList, + AlcoholTypeReader alcoholTypeReader) { + return alcoholTypeIdList.stream() + .map(alcoholTypeId -> { + AlcoholType alcoholType = alcoholTypeReader.getById(alcoholTypeId); + return MemberAlcoholType.create(member, alcoholType); + }) + .toList(); + } + + public void processAlcoholTypes(Member member, SignUpMemberRequest signUpRequest) { + List memberAlcoholTypeList = getMemberAlcoholTypeList( + member, signUpRequest.alcoholTypeIds(), alcoholTypeReader); + if (!memberAlcoholTypeList.isEmpty()) { + memberAlcoholTypeWriter.storeAll(memberAlcoholTypeList); + } + } + + public void processTermsAgreements(Member member, SignUpMemberRequest signUpRequest) { + List memberTerms = getAndValidateTermsWithMapping(member, + signUpRequest.termsAgreements()); + if (!memberTerms.isEmpty()) { + memberTermsWriter.storeAll(memberTerms); + } + } + + /** + * 약관 동의 정보 검증 및 매핑 생성 + */ + public List getAndValidateTermsWithMapping(Member member, List termsAgreements) { + List usedTermsList = termsReader.getAllByIsUsed(); + + if (usedTermsList.isEmpty()) { + return Collections.emptyList(); + } + + validateTermsList(usedTermsList, termsAgreements); + return createMemberTermsList(member, usedTermsList, termsAgreements); + } + + public List createMemberTermsList(Member member, List usedTermsList, + List termsAgreements) { + + // 약관 ID를 키로 하는 맵으로 변환하여 조회 성능 개선 + Map agreementMap = termsAgreements.stream() + .collect(Collectors.toMap(TermsAgreement::termsId, Function.identity())); + + final LocalDateTime now = LocalDateTime.now(); + List mappings = new ArrayList<>(usedTermsList.size()); + + for (Terms terms : usedTermsList) { + TermsAgreement termsAgreement = Optional.ofNullable(agreementMap.get(terms.getId())) + .orElseThrow(() -> new InvalidParamException(ErrorCode.TERMS_NOT_FOUND)); + + final boolean isAgreed = termsAgreement.isAgreed(); + + if (terms.isRequired() && !isAgreed) { + throw new InvalidParamException(ErrorCode.TERMS_AGREEMENT_MISSING_REQUIRED); + } + + mappings.add(MemberTerms.create(member, terms, isAgreed, now)); + } + + return mappings; + } + + public void validateTermsList(List usedTermsList, List termsAgreements) { + if (usedTermsList.size() != termsAgreements.size()) { + throw new InvalidParamException(ErrorCode.TERMS_AGREEMENT_MISMATCH); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/s3/S3Controller.java b/src/main/java/com/juu/juulabel/s3/S3Controller.java index 5edc4bce..ecf494fa 100644 --- a/src/main/java/com/juu/juulabel/s3/S3Controller.java +++ b/src/main/java/com/juu/juulabel/s3/S3Controller.java @@ -14,12 +14,9 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -@Tag( - name = "이미지 파일 업로드 API", - description = "프론트 개발 편의를 위한 이미지 개별 업로드 API" -) +@Tag(name = "이미지 파일 업로드 API", description = "프론트 개발 편의를 위한 이미지 개별 업로드 API") @RestController -@RequestMapping(value = {"/v1/api/images"}) +@RequestMapping(value = { "/v1/api/images" }) @RequiredArgsConstructor public class S3Controller { @@ -28,8 +25,7 @@ public class S3Controller { @Operation(summary = "이미지 파일 업로드") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> uploadImage( - @RequestPart(value = "image") MultipartFile image - ) { + @RequestPart(value = "image") MultipartFile image) { return CommonResponse.success(SuccessCode.SUCCESS, s3Service.uploadMemberImage(image)); } diff --git a/src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java b/src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java index 25178ffd..fa1aceb7 100644 --- a/src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java +++ b/src/main/java/com/juu/juulabel/tastingnote/domain/TastingNote.java @@ -21,9 +21,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) @Entity -@Table( - name = "tasting_note" -) +@Table(name = "tasting_note") public class TastingNote extends BaseTimeEntity { @Id @@ -67,15 +65,14 @@ public class TastingNote extends BaseTimeEntity { @OneToMany(mappedBy = "tastingNote", cascade = CascadeType.ALL, orphanRemoval = true) private List tastingNoteScents = new ArrayList<>(); - public static TastingNote of(Member member, - AlcoholType alcoholType, - AlcoholicDrinks alcoholicDrinks, - Color color, - AlcoholicDrinksSnapshot alcoholDrinksInfo, - Double rating, - String content, - boolean isPrivate) { + AlcoholType alcoholType, + AlcoholicDrinks alcoholicDrinks, + Color color, + AlcoholicDrinksSnapshot alcoholDrinksInfo, + Double rating, + String content, + boolean isPrivate) { return TastingNote.builder() .member(member) .alcoholType(alcoholType) @@ -89,14 +86,13 @@ public static TastingNote of(Member member, } public void update( - AlcoholType alcoholType, - AlcoholicDrinks alcoholicDrinks, - Color color, - AlcoholicDrinksSnapshot alcoholDrinksInfo, - Double rating, - String content, - boolean isPrivate - ) { + AlcoholType alcoholType, + AlcoholicDrinks alcoholicDrinks, + Color color, + AlcoholicDrinksSnapshot alcoholDrinksInfo, + Double rating, + String content, + boolean isPrivate) { this.alcoholType = alcoholType; this.alcoholicDrinks = alcoholicDrinks; this.color = color; diff --git a/src/main/resources/scripts/login_refresh_token.lua b/src/main/resources/scripts/login_refresh_token.lua new file mode 100644 index 00000000..0568f5f1 --- /dev/null +++ b/src/main/resources/scripts/login_refresh_token.lua @@ -0,0 +1,44 @@ +-- KEYS[1] = newTokenKey (e.g., "RefreshToken:{hashedToken}") +-- KEYS[2] = indexKey (e.g., "RefreshIndex:{memberId}:{clientId}:{deviceId}") +-- ARGV[1] = memberId +-- ARGV[2] = clientId +-- ARGV[3] = deviceId +-- ARGV[4] = ipAddress +-- ARGV[5] = userAgent +-- ARGV[6] = ttl in seconds +local newTokenKey = KEYS[1] +local indexKey = KEYS[2] + +local memberId = ARGV[1] +local clientId = ARGV[2] +local deviceId = ARGV[3] +local ipAddress = ARGV[4] +local userAgent = ARGV[5] +local ttl = tonumber(ARGV[6]) + +-- Find all keys for the user+client+device +local oldTokenKeys = redis.call("SMEMBERS", indexKey) + +-- Revoke all old tokens +for _, key in ipairs(oldTokenKeys) do + if redis.call("EXISTS", key) == 1 then + redis.call("HSET", key, "revoked", 1) + else + redis.call("SREM", indexKey, key) -- clean up dead keys + end +end + +-- Save the new token hash +redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress", + ipAddress, "userAgent", userAgent, "revoked", 0) + +-- Set TTL +redis.call("EXPIRE", newTokenKey, ttl) + +-- Update the index with new token +redis.call("SADD", indexKey, newTokenKey) +redis.call("EXPIRE", indexKey, ttl) + +return { + ok = "LOGIN_SUCCESS" +} diff --git a/src/main/resources/scripts/revoke_refresh_token_by_index_key.lua b/src/main/resources/scripts/revoke_refresh_token_by_index_key.lua new file mode 100644 index 00000000..beedff35 --- /dev/null +++ b/src/main/resources/scripts/revoke_refresh_token_by_index_key.lua @@ -0,0 +1,41 @@ +local pattern = KEYS[1] + +-- Use SCAN instead of KEYS for better performance with large datasets +local cursor = "0" +local batchSize = 100 + +repeat + local result = redis.call("SCAN", cursor, "MATCH", pattern, "COUNT", batchSize) + cursor = result[1] + local indexKeys = result[2] + + for _, idxKey in ipairs(indexKeys) do + local tokenKeys = redis.call("SMEMBERS", idxKey) + + -- Batch revoke tokens to reduce Redis calls + if #tokenKeys > 0 then + for i = 1, #tokenKeys, batchSize do + local batch = {} + local endIdx = math.min(i + batchSize - 1, #tokenKeys) + + for j = i, endIdx do + table.insert(batch, "HSET") + table.insert(batch, tokenKeys[j]) + table.insert(batch, "revoked") + table.insert(batch, 1) + end + + if #batch > 0 then + redis.call(unpack(batch)) + end + end + + -- Clean up the index key + redis.call("DEL", idxKey) + end + end +until cursor == "0" + +return { + ok = "REVOKED_ALL_TOKENS_BY_INDEX_KEY" +} diff --git a/src/main/resources/scripts/rotate_refresh_token.lua b/src/main/resources/scripts/rotate_refresh_token.lua new file mode 100644 index 00000000..4dd699e6 --- /dev/null +++ b/src/main/resources/scripts/rotate_refresh_token.lua @@ -0,0 +1,95 @@ +-- KEYS[1] = new token key (e.g., "RefreshToken:{hashedToken}") +-- KEYS[2] = indexKey (e.g., "RefreshIndex:{memberId}:{clientId}:{deviceId}") +-- KEYS[3] = old token key (e.g., "RefreshToken:{hashedToken}") +-- ARGV[1] = memberId +-- ARGV[2] = clientId +-- ARGV[3] = deviceId +-- ARGV[4] = ipAddress +-- ARGV[5] = userAgent +-- ARGV[6] = ttl in seconds +local newTokenKey = KEYS[1] +local indexKey = KEYS[2] +local oldTokenKey = KEYS[3] +local memberId = ARGV[1] +local clientId = ARGV[2] +local deviceId = ARGV[3] +local ipAddress = ARGV[4] +local userAgent = ARGV[5] +local ttl = tonumber(ARGV[6]) + +-- Helper function to revoke all member tokens +local function revokeAllMemberTokens(memberId) + local cursor = "0" + local pattern = "RefreshIndex:" .. memberId .. ":*" + + repeat + local result = redis.call("SCAN", cursor, "MATCH", pattern, "COUNT", 100) + cursor = result[1] + local indexKeys = result[2] + + for _, idxKey in ipairs(indexKeys) do + local tokenKeys = redis.call("SMEMBERS", idxKey) + + -- Batch revoke tokens (max 100 at a time to avoid large commands) + for i = 1, #tokenKeys, 100 do + local batch = {} + local endIdx = math.min(i + 99, #tokenKeys) + + for j = i, endIdx do + table.insert(batch, "HSET") + table.insert(batch, tokenKeys[j]) + table.insert(batch, "revoked") + table.insert(batch, 1) + end + + if #batch > 0 then + redis.call(unpack(batch)) + end + end + + -- Clean up index + redis.call("DEL", idxKey) + end + until cursor == "0" +end + +-- Check if old token exists and get all fields at once +local oldToken = redis.call("HMGET", oldTokenKey, "revoked", "deviceId") +if not oldToken[1] and not oldToken[2] then + return { + err = "OLD_TOKEN_NOT_FOUND" + } +end + +-- Check if token is already revoked (using direct array access) +if oldToken[1] == "1" then + revokeAllMemberTokens(memberId) + return { + err = "OLD_TOKEN_ALREADY_REVOKED_ALL_TOKENS_INVALIDATED" + } +end + +-- Check device ID mismatch (using direct array access) +if oldToken[2] ~= deviceId then + revokeAllMemberTokens(memberId) + return { + err = "DEVICE_ID_MISMATCH" + } +end + +-- Revoke old token +redis.call("HSET", oldTokenKey, "revoked", 1) +redis.call("SREM", indexKey, oldTokenKey) + +-- Store new token with all fields at once +redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress", + ipAddress, "userAgent", userAgent, "revoked", 0) + +-- Set expiration and update index +redis.call("EXPIRE", newTokenKey, ttl) +redis.call("SADD", indexKey, newTokenKey) +redis.call("EXPIRE", indexKey, ttl) + +return { + ok = "ROTATION_SUCCESS" +} diff --git a/src/main/resources/scripts/save_refresh_token.lua b/src/main/resources/scripts/save_refresh_token.lua new file mode 100644 index 00000000..5742579c --- /dev/null +++ b/src/main/resources/scripts/save_refresh_token.lua @@ -0,0 +1,31 @@ +-- KEYS[1] = newTokenKey (e.g., "RefreshToken:{hashedToken}") +-- KEYS[2] = indexKey (e.g., "RefreshIndex:{memberId}:{clientId}:{deviceId}") +-- ARGV[1] = memberId +-- ARGV[2] = clientId +-- ARGV[3] = deviceId +-- ARGV[4] = ipAddress +-- ARGV[5] = userAgent +-- ARGV[6] = ttl in seconds +local newTokenKey = KEYS[1] +local indexKey = KEYS[2] + +local memberId = ARGV[1] +local clientId = ARGV[2] +local deviceId = ARGV[3] +local ipAddress = ARGV[4] +local userAgent = ARGV[5] +local ttl = tonumber(ARGV[6]) + +-- Store token +-- Save the new token hash +redis.call("HSET", newTokenKey, "memberId", memberId, "clientId", clientId, "deviceId", deviceId, "ipAddress", + ipAddress, "userAgent", userAgent, "revoked", 0) +redis.call("EXPIRE", newTokenKey, ttl) + +-- Update index with limited size +redis.call("SADD", indexKey, newTokenKey) +redis.call("EXPIRE", indexKey, ttl) + +return { + ok = "SAVE_REFRESH_TOKEN_SUCCESS" +}