From af6607a436f216e074ce503d5b0be752b5d86fcf Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 6 Sep 2025 10:17:27 +0900 Subject: [PATCH 001/291] =?UTF-8?q?feat=20:=20Book=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20Entity(DB=ED=85=8C=EC=9D=B4=EB=B8=94)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Author - Book - Translator --- .../mvp/domain/book/entity/Author.java | 31 +++++++++++++ .../BookPick/mvp/domain/book/entity/Book.java | 46 +++++++++++++++++++ .../mvp/domain/book/entity/Translator.java | 31 +++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Author.java create mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Book.java create mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Translator.java diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Author.java b/src/main/java/BookPick/mvp/domain/book/entity/Author.java new file mode 100644 index 0000000..a09ab19 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/entity/Author.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.book.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "author") +@Getter +@Setter +public class Author { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 저자 ID (PK) + + @Column(nullable = false, length = 100) + private String name; // 저자명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java new file mode 100644 index 0000000..ecfa738 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -0,0 +1,46 @@ +package BookPick.mvp.domain.book.entity; + +import BookPick.mvp.domain.taste.entity.Genre; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + + + +@Entity +@Table(name = "book") +@Getter +@Setter +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 도서 ID (PK) + + @Column(nullable = false, length = 255) + private String title; // 도서 제목 + + @ManyToOne + @JoinColumn(name = "author_id", nullable = false) + private Author author; // 저자 ID (FK → author 테이블) + + @ManyToOne + @JoinColumn(name = "translator_id") + private Translator translator; // 역자 ID (FK → translator 테이블) + + @ManyToOne + @JoinColumn(name = "genre_id") + private Genre genre; // 장르 ID (FK → genre 테이블) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Translator.java b/src/main/java/BookPick/mvp/domain/book/entity/Translator.java new file mode 100644 index 0000000..114d6b2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/entity/Translator.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.book.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "translator") +@Getter +@Setter +public class Translator { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 역자 ID (PK) + + @Column(nullable = false, length = 100) + private String name; // 역자명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} From 6b6d2069f55c5761efce345b9a94f33cece7d4b3 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 6 Sep 2025 10:19:00 +0900 Subject: [PATCH 002/291] =?UTF-8?q?feat=20:=20DB=20=08=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20taste=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Genre - Habit - KeyWord - Mbti - Mood - ReadingStyle --- .../mvp/domain/taste/entity/Genre.java | 31 +++++++++++++++++++ .../mvp/domain/taste/entity/Habit.java | 31 +++++++++++++++++++ .../mvp/domain/taste/entity/Keyword.java | 31 +++++++++++++++++++ .../mvp/domain/taste/entity/Mbti.java | 31 +++++++++++++++++++ .../mvp/domain/taste/entity/Mood.java | 31 +++++++++++++++++++ .../mvp/domain/taste/entity/ReadingStyle.java | 31 +++++++++++++++++++ 6 files changed, 186 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Genre.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Habit.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Mood.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Genre.java b/src/main/java/BookPick/mvp/domain/taste/entity/Genre.java new file mode 100644 index 0000000..9d2321d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/Genre.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.taste.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "genre") +@Getter +@Setter +public class Genre { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 장르 ID (PK) + + @Column(nullable = false, length = 50) + private String name; // 장르명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Habit.java b/src/main/java/BookPick/mvp/domain/taste/entity/Habit.java new file mode 100644 index 0000000..a318473 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/Habit.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.taste.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "habit") +@Getter +@Setter +public class Habit { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 습관 ID (PK) + + @Column(nullable = false, length = 50) + private String name; // 습관명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java b/src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java new file mode 100644 index 0000000..5f58a31 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.taste.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "keyword") +@Getter +@Setter +public class Keyword { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 키워드 ID (PK) + + @Column(nullable = false, length = 50) + private String name; // 키워드명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java b/src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java new file mode 100644 index 0000000..ab3a4d9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.taste.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "mbti") +@Getter +@Setter +public class Mbti { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // MBTI ID (PK) + + @Column(nullable = false, length = 8) + private String name; // MBTI + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Mood.java b/src/main/java/BookPick/mvp/domain/taste/entity/Mood.java new file mode 100644 index 0000000..a5c96f5 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/Mood.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.taste.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "mood") +@Getter +@Setter +public class Mood { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 무드 ID (PK) + + @Column(nullable = false, length = 50) + private String name; // 무드명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java b/src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java new file mode 100644 index 0000000..e75300e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.taste.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "reading_style") +@Getter +@Setter +public class ReadingStyle { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 리딩 스타일 ID (PK) + + @Column(nullable = false, length = 50) + private String name; // 리딩 스타일 명 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} From c215db4070949fb6c25a4ea935d456b7e45c2fae Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 6 Sep 2025 10:19:39 +0900 Subject: [PATCH 003/291] =?UTF-8?q?feat=20:=20DB=20=08=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20User=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User - UserTaste - follow --- .../BookPick/mvp/domain/user/entity/User.java | 44 +++++++++++++++++++ .../mvp/domain/user/entity/UserTaste.java | 38 ++++++++++++++++ .../mvp/domain/user/entity/follow.java | 31 +++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/user/entity/User.java create mode 100644 src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java create mode 100644 src/main/java/BookPick/mvp/domain/user/entity/follow.java diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java new file mode 100644 index 0000000..458e5ef --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -0,0 +1,44 @@ +package BookPick.mvp.domain.user.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user") +@Getter +@Setter +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long id; // 내부 식별자 (PK) + + @Column(name = "login_email", nullable = false, unique = true, length = 255) + private String email; // 로그인 ID, 고유 + + @Column(name = "login_password", nullable = false, length = 255) + private String passwordHash; // 비밀번호 해시 + + @Column(length = 50) + private String nickname; // 프로필 닉네임 + + @Column(length = 255) + private String bio; // 자기소개 문구 + + @Column(name = "profile_image_url", length = 500) + private String profileImageUrl; // 프로필 사진 경로 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java b/src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java new file mode 100644 index 0000000..e79c233 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java @@ -0,0 +1,38 @@ +package BookPick.mvp.domain.user.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_taste") +@Getter +@Setter +public class UserTaste { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 사용자 선호도 ID (PK) + + @OneToOne + @JoinColumn(name = "user_id", nullable = false, unique = true) // UNIQUE + FK + private User user; // user 테이블 참조 + + @Column(length = 4) + private String mbti; // MBTI + + @Column(name = "reading_style", length = 50) + private String readingStyle; // 독서 스타일 + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; // 수정 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/follow.java b/src/main/java/BookPick/mvp/domain/user/entity/follow.java new file mode 100644 index 0000000..54a5510 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/entity/follow.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.user.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "follow") +@Getter +@Setter +public class follow { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 내부 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "follower_id", nullable = false) + private User follower; // 팔로우를 건 유저 (FK → User.user_id) + + @ManyToOne + @JoinColumn(name = "following_id", nullable = false) + private User following; // 팔로우 당한 유저 (FK → User.user_id) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 팔로우 생성 시각 +} From 9921098c0b45c507368c8f2bcc282504dae67e3c Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 6 Sep 2025 10:21:54 +0900 Subject: [PATCH 004/291] =?UTF-8?q?chore:=20Gradle=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B8=B0=EB=B3=B8=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes .gitattributes | 3 + .gitignore | 37 +++ build.gradle | 45 ++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew 17-57-10-142 | 251 ++++++++++++++++++ gradlew.bat | 94 +++++++ settings.gradle | 1 + .../BookPick/mvp/BookPickApplication.java | 13 + .../BookPick/mvp/global/config/JwtFilter.java | 95 +++++++ .../mvp/global/config/SecurityConfig.java | 73 +++++ .../BookPick/mvp/global/util/JwtUtil.java | 52 ++++ src/main/resources/application.properties | 8 + .../mvp/BookPickApplicationTests.java | 13 + 15 files changed, 692 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew 17-57-10-142 create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/BookPick/mvp/BookPickApplication.java create mode 100644 src/main/java/BookPick/mvp/global/config/JwtFilter.java create mode 100644 src/main/java/BookPick/mvp/global/config/SecurityConfig.java create mode 100644 src/main/java/BookPick/mvp/global/util/JwtUtil.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/BookPick/mvp/BookPickApplicationTests.java diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..79f93c21bc9d1e1f2dc762c9e41b05e7366be3d7 GIT binary patch literal 6148 zcmeHK%Wl&^6g`uMCQ*fyMUlF+#I{u)Z9$ugrO_|1)D^vsq2YsrxA)Gc@00r zm+(EDc@$v+3pNO$xzfzJuj6=*Baa7w2$s_U&;`)uP}t~j`h}^w8e7(|nI=)_YowUP zt6~wSGT!QTiGNW6_3U;rM~(e(sayqenN17L4Naj#n{*G>>*LYs zp?7k8Z*%CaN5ek-(QvcrwC~*QojspirdOH#WP}v_&uP1D@diHAsjcZc%(GNxbLJIi z|Ht)W1rGz<#}Q8TJV2lR04EqO%J}iJD06Zi;3W#os8fL+IT9Hz@R1qisft9*%9vh( zOO%WkjOR$`_wbBNi4Uy4XRNLfFqZ5*Vl81-c~5Z8sD~?DYxbYr>r;ELt)JG%+jr&^ za0)mDY6{r*XM3-fb%9gBDc}?^-%1{ZQ11!fVr5XjIymVg08xELYNM~t5{lzl^et8f zS)mC-B^s)*M+{-;%zL)aw^$i8bO?L+5cZdaJ)sEw>%6|F+aY{|ZgvVd1?mdyn6Rt% z|3|++|JO+_atb&F{woDUa~zHXF3Ik#TZ>bBt;g|>Lz(KU41TB3(OWTl)mD7Kp^bS@ XIYi%LWsn(~`w-AFxXCH-R~7gMQy_L< literal 0 HcmV?d00001 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..06e4370 --- /dev/null +++ b/build.gradle @@ -0,0 +1,45 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.5' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'BookPick' +version = '0.0.1-SNAPSHOT' +description = 'Book-Pick Project MVP version' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..013308e --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'mvp' diff --git a/src/main/java/BookPick/mvp/BookPickApplication.java b/src/main/java/BookPick/mvp/BookPickApplication.java new file mode 100644 index 0000000..a2d36ea --- /dev/null +++ b/src/main/java/BookPick/mvp/BookPickApplication.java @@ -0,0 +1,13 @@ +package BookPick.mvp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BookPickApplication { + + public static void main(String[] args) { + SpringApplication.run(BookPickApplication.class, args); + } + +} diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java new file mode 100644 index 0000000..5ebd86c --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -0,0 +1,95 @@ +//package BookPick.mvp.global.config; +// +//import com.apple.shop.domain.member.service.MyUserDetailsService.CustomUser; +//import com.apple.shop.global.util.JwtUtil; +//import io.jsonwebtoken.Claims; +//import io.jsonwebtoken.ExpiredJwtException; +//import io.jsonwebtoken.JwtException; +//import jakarta.servlet.FilterChain; +//import jakarta.servlet.ServletException; +//import jakarta.servlet.http.Cookie; +//import jakarta.servlet.http.HttpServletRequest; +//import jakarta.servlet.http.HttpServletResponse; +//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +//import org.springframework.security.core.authority.SimpleGrantedAuthority; +//import org.springframework.security.core.context.SecurityContextHolder; +//import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +//import org.springframework.web.filter.OncePerRequestFilter; +// +//import java.io.IOException; +//import java.util.Arrays; +// +//public class JwtFilter extends OncePerRequestFilter { +// +// @Override +// protected void doFilterInternal( +// HttpServletRequest request, HttpServletResponse response, FilterChain filterChain +// ) throws ServletException, IOException { +// +// Cookie jwtCookie = findCookie(request, "jwt"); +// if (jwtCookie == null || isAuthenticatedAlready()) { +// filterChain.doFilter(request, response); +// return; +// } +// +// try { +// // 1) 파싱/검증 +// Claims claim = JwtUtil.extractToken(jwtCookie.getValue()); +// +// // 2) 인증 세팅 +// String[] arr = claim.get("authorities").toString().split(","); +// var authorities = Arrays.stream(arr).map(SimpleGrantedAuthority::new).toList(); +// +// String username = String.valueOf(claim.get("username")); +// CustomUser principal = new CustomUser(username, "", authorities); +// principal.displayName = String.valueOf(claim.get("displayName")); +// +// var authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); +// authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); +// SecurityContextHolder.getContext().setAuthentication(authToken); +// +// filterChain.doFilter(request, response); +// } catch (ExpiredJwtException e) { +// // 만료 토큰: 쿠키 제거 +// clearJwtCookie(response); +// // API 경로면 401, 뷰/정적은 계속 통과 +// if (isApi(request)) { +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// return; +// } +// filterChain.doFilter(request, response); +// } catch (JwtException | IllegalArgumentException e) { +// // 서명 불일치/손상 등: 쿠키 제거 후 동일 처리 +// clearJwtCookie(response); +// if (isApi(request)) { +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// return; +// } +// filterChain.doFilter(request, response); +// } +// } +// +// private boolean isAuthenticatedAlready() { +// return SecurityContextHolder.getContext().getAuthentication() != null; +// } +// +// private boolean isApi(HttpServletRequest req) { +// String uri = req.getRequestURI(); +// return uri != null && uri.startsWith("/api/"); +// } +// +// private Cookie findCookie(HttpServletRequest request, String name) { +// Cookie[] cookies = request.getCookies(); +// if (cookies == null) return null; +// for (Cookie c : cookies) if (name.equals(c.getName())) return c; +// return null; +// } +// +// private void clearJwtCookie(HttpServletResponse res) { +// Cookie c = new Cookie("jwt", null); +// c.setPath("/"); +// c.setMaxAge(0); +// c.setHttpOnly(true); +// res.addCookie(c); +// } +//} diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java new file mode 100644 index 0000000..02443ef --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -0,0 +1,73 @@ +//package BookPick.mvp.global.config; +// +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.http.HttpMethod; +//import org.springframework.security.config.Customizer; +//import org.springframework.security.config.annotation.web.builders.HttpSecurity; +//import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +//import org.springframework.security.config.http.SessionCreationPolicy; +//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +//import org.springframework.security.crypto.password.PasswordEncoder; +//import org.springframework.security.web.SecurityFilterChain; +//import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +// +//@Configuration +//@EnableWebSecurity +//public class SecurityConfig { +// +// private static final String[] SWAGGER_WHITELIST = { +// "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html" +// }; +// +// private static final String[] STATIC_RESOURCES = { +// "/css/**", "/js/**", "/images/**", "/webjars/**", "/favicon.ico" +// }; +// +// // 뷰 라우트(비로그인 허용 페이지) +// private static final String[] VIEW_PUBLIC = { +// "/", "/item/list", "/member/login", "/member/signup", "/member/add", +// "/member/logout", "/member/logout/jwt" // 필요시 POST도 허용 +// }; +// +// @Bean +// PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } +// +// @Bean +// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { +// http.csrf(csrf -> csrf.disable()); +// http.cors(Customizer.withDefaults()); +// +// // ✅ JWT 필터는 UsernamePasswordAuthenticationFilter 앞에 +// http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class); +// +// // JWT 사용 → 세션 미사용 +// http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); +// +// http.authorizeHttpRequests(auth -> auth +// // 프리플라이트 +// .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() +// // 정적/스웨거/공개 뷰 +// .requestMatchers(STATIC_RESOURCES).permitAll() +// .requestMatchers(SWAGGER_WHITELIST).permitAll() +// .requestMatchers(VIEW_PUBLIC).permitAll() +// // API는 인증 필수 +// .requestMatchers("/api/**").authenticated() +// // 나머지는 필요에 따라: 뷰를 전부 공개하려면 permitAll, 내부페이지면 authenticated +// .anyRequest().permitAll() +// ); +// +// // 폼 로그인 페이지(뷰용): 페이지 자체는 permitAll로 열려 있으니 유지 가능 +// http.formLogin(form -> form +// .loginPage("/member/login") +// .defaultSuccessUrl("/item/list", true) +// ); +// +// http.logout(logout -> logout +// .logoutUrl("/member/logout") +// .logoutSuccessUrl("/item/list") +// ); +// +// return http.build(); +// } +//} diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java new file mode 100644 index 0000000..6d8be81 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -0,0 +1,52 @@ +//package BookPick.mvp.global.util; +// +//import com.apple.shop.domain.member.service.MyUserDetailsService; +//import io.jsonwebtoken.Claims; +//import io.jsonwebtoken.Jwts; +//import io.jsonwebtoken.io.Decoders; +//import io.jsonwebtoken.security.Keys; +//import org.springframework.security.core.Authentication; +//import org.springframework.stereotype.Component; +// +//import javax.crypto.SecretKey; +//import java.util.Date; +//import java.util.stream.Collectors; +// +//@Component +//public class JwtUtil { +// // 1. 키발급 +// static final SecretKey key = +// Keys.hmacShaKeyFor(Decoders.BASE64.decode( +// "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" +// )); +// +// +// // 2. JWT 생성 +// public static String createToken(Authentication auth) { +// MyUserDetailsService.CustomUser usr = (MyUserDetailsService.CustomUser) auth.getPrincipal(); +// +// String authorities = auth.getAuthorities().stream() //getAuthorities -> List return +// .map(a->a.getAuthority()) // getAuthority() -> String return +// .collect(Collectors.joining(",")); +// +// +// +// String jwt = Jwts.builder() +// .claim("username", usr.getUsername()) +// .claim("displayName", usr.displayName ) +// .claim("authorities", authorities) +// .issuedAt(new Date(System.currentTimeMillis())) +// .expiration(new Date(System.currentTimeMillis() + 1000*60*60)) // expiration : 만료 +// .signWith(key) +// .compact(); +// return jwt; +// } +// +// +// //3. JWT 오픈 +// public static Claims extractToken(String token) { +// Claims claims = Jwts.parser().verifyWith(key).build() +// .parseSignedClaims(token).getPayload(); +// return claims; +// } +//} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..755d597 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.application.name=BookPick +spring.datasource.url=jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick +spring.datasource.username=nan7789 +spring.datasource.password=gustjq3735! +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.show_sql=False \ No newline at end of file diff --git a/src/test/java/BookPick/mvp/BookPickApplicationTests.java b/src/test/java/BookPick/mvp/BookPickApplicationTests.java new file mode 100644 index 0000000..87d7744 --- /dev/null +++ b/src/test/java/BookPick/mvp/BookPickApplicationTests.java @@ -0,0 +1,13 @@ +package BookPick.mvp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BookPickApplicationTests { + + @Test + void contextLoads() { + } + +} From b8fbdf3730e9bce2115698872ee294010c413243 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 6 Sep 2025 10:57:05 +0900 Subject: [PATCH 005/291] =?UTF-8?q?feat=20:=20DB=20=08=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20taste/mapping=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TasteAuthor - TasteBook - TasteGenre - TasteHabit - TasteKeyword - TasteMood --- .../taste/entity/mapping/TasteAuthorMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteBookMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteGenreMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteHabitMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteKeywordMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteMoodMap.java | 33 +++++++++++++++++++ 6 files changed, 198 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java new file mode 100644 index 0000000..a922184 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.book.entity.Author; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_author") +@Getter +@Setter +public class TasteAuthorMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "author_id", nullable = false) + private Author author; // 저자 ID (FK → author) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java new file mode 100644 index 0000000..57af200 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_book") +@Getter +@Setter +public class TasteBookMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "book_id", nullable = false) + private Book book; // 도서 ID (FK → book) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java new file mode 100644 index 0000000..bb7e114 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Genre; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_genre") +@Getter +@Setter +public class TasteGenreMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "genre_id", nullable = false) + private Genre genre; // 장르 ID (FK → genre) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java new file mode 100644 index 0000000..a76f267 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Habit; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_habit") +@Getter +@Setter +public class TasteHabitMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "habit_id", nullable = false) + private Habit habit; // 습관 ID (FK → habit) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java new file mode 100644 index 0000000..41a2416 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Keyword; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_keyword") +@Getter +@Setter +public class TasteKeywordMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "keyword_id", nullable = false) + private Keyword keyword; // 키워드 ID (FK → keyword) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java new file mode 100644 index 0000000..8bd1b43 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Mood; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_mood") +@Getter +@Setter +public class TasteMoodMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "mood_id", nullable = false) + private Mood mood; // 분위기 ID (FK → mood) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} From 6335eb864c7250a53cd32a7def8296bdf759cb2e Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 6 Sep 2025 10:57:05 +0900 Subject: [PATCH 006/291] =?UTF-8?q?feat=20:=20DB=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20taste/mapping=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TasteAuthor - TasteBook - TasteGenre - TasteHabit - TasteKeyword - TasteMood --- .../taste/entity/mapping/TasteAuthorMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteBookMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteGenreMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteHabitMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteKeywordMap.java | 33 +++++++++++++++++++ .../taste/entity/mapping/TasteMoodMap.java | 33 +++++++++++++++++++ 6 files changed, 198 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java create mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java new file mode 100644 index 0000000..a922184 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.book.entity.Author; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_author") +@Getter +@Setter +public class TasteAuthorMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "author_id", nullable = false) + private Author author; // 저자 ID (FK → author) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java new file mode 100644 index 0000000..57af200 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_book") +@Getter +@Setter +public class TasteBookMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "book_id", nullable = false) + private Book book; // 도서 ID (FK → book) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java new file mode 100644 index 0000000..bb7e114 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Genre; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_genre") +@Getter +@Setter +public class TasteGenreMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "genre_id", nullable = false) + private Genre genre; // 장르 ID (FK → genre) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java new file mode 100644 index 0000000..a76f267 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Habit; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_habit") +@Getter +@Setter +public class TasteHabitMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "habit_id", nullable = false) + private Habit habit; // 습관 ID (FK → habit) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java new file mode 100644 index 0000000..41a2416 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Keyword; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_keyword") +@Getter +@Setter +public class TasteKeywordMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "keyword_id", nullable = false) + private Keyword keyword; // 키워드 ID (FK → keyword) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java new file mode 100644 index 0000000..8bd1b43 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.taste.entity.mapping; + +import BookPick.mvp.domain.taste.entity.Mood; +import BookPick.mvp.domain.user.entity.UserTaste; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "taste_mood") +@Getter +@Setter +public class TasteMoodMap { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; // 고유 식별자 (PK) + + @ManyToOne + @JoinColumn(name = "taste_id", nullable = false) + private UserTaste userTaste; // 선호도 ID (FK → user_taste) + + @ManyToOne + @JoinColumn(name = "mood_id", nullable = false) + private Mood mood; // 분위기 ID (FK → mood) + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; // 생성 시각 +} From b312673c7148440f95ea7c76624594ef5c389c9a Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Sep 2025 11:48:32 +0900 Subject: [PATCH 007/291] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 ++ .../java/BookPick/mvp/common/ApiResponse.java | 37 ++++++ .../mvp/domain/book/entity/Author.java | 31 ----- .../BookPick/mvp/domain/book/entity/Book.java | 46 ------- .../mvp/domain/book/entity/Translator.java | 31 ----- .../mvp/domain/taste/entity/Genre.java | 31 ----- .../mvp/domain/taste/entity/Habit.java | 31 ----- .../mvp/domain/taste/entity/Keyword.java | 31 ----- .../mvp/domain/taste/entity/Mbti.java | 31 ----- .../mvp/domain/taste/entity/Mood.java | 31 ----- .../mvp/domain/taste/entity/ReadingStyle.java | 31 ----- .../taste/entity/mapping/TasteAuthorMap.java | 33 ----- .../taste/entity/mapping/TasteBookMap.java | 33 ----- .../taste/entity/mapping/TasteGenreMap.java | 33 ----- .../taste/entity/mapping/TasteHabitMap.java | 33 ----- .../taste/entity/mapping/TasteKeywordMap.java | 33 ----- .../taste/entity/mapping/TasteMoodMap.java | 33 ----- .../BookPick/mvp/domain/user/AuthService.java | 62 +++++++++ .../user/controller/AuthController.java | 48 +++++++ .../user/controller/UserController.java | 19 +++ .../mvp/domain/user/dto/AuthDtos.java | 54 ++++++++ .../mvp/domain/user/dto/UserDtos.java | 19 +++ .../BookPick/mvp/domain/user/entity/User.java | 5 +- .../exception/DuplicateEmailException.java | 7 ++ .../user/repository/UserRepository.java | 14 +++ .../mvp/domain/user/service/UserService.java | 36 ++++++ .../mvp/global/config/SecurityConfig.java | 118 +++++++----------- .../mvp/global/config/SwaggerConfig.java | 25 ++++ .../exception/GlobalExceptionHandler.java | 33 +++++ .../BookPick/mvp/global/util/JwtUtil.java | 100 +++++++-------- src/main/resources/application.properties | 11 +- 31 files changed, 468 insertions(+), 619 deletions(-) create mode 100644 src/main/java/BookPick/mvp/common/ApiResponse.java delete mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Author.java delete mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Book.java delete mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Translator.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Genre.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Habit.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/Mood.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java delete mode 100644 src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java create mode 100644 src/main/java/BookPick/mvp/domain/user/AuthService.java create mode 100644 src/main/java/BookPick/mvp/domain/user/controller/AuthController.java create mode 100644 src/main/java/BookPick/mvp/domain/user/controller/UserController.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java create mode 100644 src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/user/service/UserService.java create mode 100644 src/main/java/BookPick/mvp/global/config/SwaggerConfig.java create mode 100644 src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java diff --git a/build.gradle b/build.gradle index 06e4370..ffd7e5b 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,13 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5' + + } diff --git a/src/main/java/BookPick/mvp/common/ApiResponse.java b/src/main/java/BookPick/mvp/common/ApiResponse.java new file mode 100644 index 0000000..6259ac9 --- /dev/null +++ b/src/main/java/BookPick/mvp/common/ApiResponse.java @@ -0,0 +1,37 @@ +package BookPick.mvp.common; + +import lombok.Getter; + +// 응답 포멧 +// 이 포멧 안에 data 부분에 각각의 DTO + +@Getter +public class ApiResponse { + + private final int status; + private final String message; + private final T data; //DTO + + public ApiResponse(int status, String message, T data) { + this.status = status; + this.message = message; + this.data = data; + } + + // 정적 팩토리 메서드 + public static ApiResponse ok(T data) { + return new ApiResponse<>( 200, null, data) ; + } + + public static ApiResponse created(T data) { + return new ApiResponse<>(201, null, data); + } + + public static ApiResponse noContent() { + return new ApiResponse<>(204, null, null); + } + + public static ApiResponse error(int status, String message) { + return new ApiResponse<>(status, message,null); + } +} diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Author.java b/src/main/java/BookPick/mvp/domain/book/entity/Author.java deleted file mode 100644 index a09ab19..0000000 --- a/src/main/java/BookPick/mvp/domain/book/entity/Author.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.book.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "author") -@Getter -@Setter -public class Author { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 저자 ID (PK) - - @Column(nullable = false, length = 100) - private String name; // 저자명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java deleted file mode 100644 index ecfa738..0000000 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ /dev/null @@ -1,46 +0,0 @@ -package BookPick.mvp.domain.book.entity; - -import BookPick.mvp.domain.taste.entity.Genre; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - - - -@Entity -@Table(name = "book") -@Getter -@Setter -public class Book { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 도서 ID (PK) - - @Column(nullable = false, length = 255) - private String title; // 도서 제목 - - @ManyToOne - @JoinColumn(name = "author_id", nullable = false) - private Author author; // 저자 ID (FK → author 테이블) - - @ManyToOne - @JoinColumn(name = "translator_id") - private Translator translator; // 역자 ID (FK → translator 테이블) - - @ManyToOne - @JoinColumn(name = "genre_id") - private Genre genre; // 장르 ID (FK → genre 테이블) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Translator.java b/src/main/java/BookPick/mvp/domain/book/entity/Translator.java deleted file mode 100644 index 114d6b2..0000000 --- a/src/main/java/BookPick/mvp/domain/book/entity/Translator.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.book.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "translator") -@Getter -@Setter -public class Translator { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 역자 ID (PK) - - @Column(nullable = false, length = 100) - private String name; // 역자명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Genre.java b/src/main/java/BookPick/mvp/domain/taste/entity/Genre.java deleted file mode 100644 index 9d2321d..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/Genre.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.taste.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "genre") -@Getter -@Setter -public class Genre { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 장르 ID (PK) - - @Column(nullable = false, length = 50) - private String name; // 장르명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Habit.java b/src/main/java/BookPick/mvp/domain/taste/entity/Habit.java deleted file mode 100644 index a318473..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/Habit.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.taste.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "habit") -@Getter -@Setter -public class Habit { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 습관 ID (PK) - - @Column(nullable = false, length = 50) - private String name; // 습관명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java b/src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java deleted file mode 100644 index 5f58a31..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/Keyword.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.taste.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "keyword") -@Getter -@Setter -public class Keyword { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 키워드 ID (PK) - - @Column(nullable = false, length = 50) - private String name; // 키워드명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java b/src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java deleted file mode 100644 index ab3a4d9..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/Mbti.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.taste.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "mbti") -@Getter -@Setter -public class Mbti { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // MBTI ID (PK) - - @Column(nullable = false, length = 8) - private String name; // MBTI - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/Mood.java b/src/main/java/BookPick/mvp/domain/taste/entity/Mood.java deleted file mode 100644 index a5c96f5..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/Mood.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.taste.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "mood") -@Getter -@Setter -public class Mood { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 무드 ID (PK) - - @Column(nullable = false, length = 50) - private String name; // 무드명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java b/src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java deleted file mode 100644 index e75300e..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/ReadingStyle.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.taste.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "reading_style") -@Getter -@Setter -public class ReadingStyle { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 리딩 스타일 ID (PK) - - @Column(nullable = false, length = 50) - private String name; // 리딩 스타일 명 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java deleted file mode 100644 index a922184..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteAuthorMap.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.taste.entity.mapping; - -import BookPick.mvp.domain.book.entity.Author; -import BookPick.mvp.domain.user.entity.UserTaste; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "taste_author") -@Getter -@Setter -public class TasteAuthorMap { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 고유 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "taste_id", nullable = false) - private UserTaste userTaste; // 선호도 ID (FK → user_taste) - - @ManyToOne - @JoinColumn(name = "author_id", nullable = false) - private Author author; // 저자 ID (FK → author) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java deleted file mode 100644 index 57af200..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteBookMap.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.taste.entity.mapping; - -import BookPick.mvp.domain.book.entity.Book; -import BookPick.mvp.domain.user.entity.UserTaste; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "taste_book") -@Getter -@Setter -public class TasteBookMap { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 고유 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "taste_id", nullable = false) - private UserTaste userTaste; // 선호도 ID (FK → user_taste) - - @ManyToOne - @JoinColumn(name = "book_id", nullable = false) - private Book book; // 도서 ID (FK → book) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java deleted file mode 100644 index bb7e114..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteGenreMap.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.taste.entity.mapping; - -import BookPick.mvp.domain.taste.entity.Genre; -import BookPick.mvp.domain.user.entity.UserTaste; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "taste_genre") -@Getter -@Setter -public class TasteGenreMap { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 고유 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "taste_id", nullable = false) - private UserTaste userTaste; // 선호도 ID (FK → user_taste) - - @ManyToOne - @JoinColumn(name = "genre_id", nullable = false) - private Genre genre; // 장르 ID (FK → genre) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java deleted file mode 100644 index a76f267..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteHabitMap.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.taste.entity.mapping; - -import BookPick.mvp.domain.taste.entity.Habit; -import BookPick.mvp.domain.user.entity.UserTaste; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "taste_habit") -@Getter -@Setter -public class TasteHabitMap { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 고유 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "taste_id", nullable = false) - private UserTaste userTaste; // 선호도 ID (FK → user_taste) - - @ManyToOne - @JoinColumn(name = "habit_id", nullable = false) - private Habit habit; // 습관 ID (FK → habit) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java deleted file mode 100644 index 41a2416..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteKeywordMap.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.taste.entity.mapping; - -import BookPick.mvp.domain.taste.entity.Keyword; -import BookPick.mvp.domain.user.entity.UserTaste; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "taste_keyword") -@Getter -@Setter -public class TasteKeywordMap { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 고유 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "taste_id", nullable = false) - private UserTaste userTaste; // 선호도 ID (FK → user_taste) - - @ManyToOne - @JoinColumn(name = "keyword_id", nullable = false) - private Keyword keyword; // 키워드 ID (FK → keyword) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java b/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java deleted file mode 100644 index 8bd1b43..0000000 --- a/src/main/java/BookPick/mvp/domain/taste/entity/mapping/TasteMoodMap.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.taste.entity.mapping; - -import BookPick.mvp.domain.taste.entity.Mood; -import BookPick.mvp.domain.user.entity.UserTaste; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "taste_mood") -@Getter -@Setter -public class TasteMoodMap { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 고유 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "taste_id", nullable = false) - private UserTaste userTaste; // 선호도 ID (FK → user_taste) - - @ManyToOne - @JoinColumn(name = "mood_id", nullable = false) - private Mood mood; // 분위기 ID (FK → mood) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/user/AuthService.java b/src/main/java/BookPick/mvp/domain/user/AuthService.java new file mode 100644 index 0000000..28cbde7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/AuthService.java @@ -0,0 +1,62 @@ +package BookPick.mvp.domain.user; + +import BookPick.mvp.domain.user.dto.AuthDtos.*; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.global.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class AuthService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + + + @Transactional + public SignRes signUp(SignReq req) { + User user = new User(); + user.setEmail(req.email()); + user.setPassword(passwordEncoder.encode(req.password())); + user.setRole("normal_user"); // normal_user, curator + + User savedUser = userRepository.save(user); // 1. DB에 저장 + + return new SignRes(savedUser.getId()); + } + + + public AuthRes login(LoginReq req){ + + // 1. 이메일 검증 + User user = userRepository.findByEmail(req.email()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); + + // 2. 비밀번호 검증 + if (!passwordEncoder.matches(req.password(), user.getPassword())) { + throw new IllegalArgumentException("잘못된 이메일 혹은 비밀번호입니다."); + } + + // 3. JWT 토큰 발급 (❌ 비밀번호 넣지 말고) + String accessToken = jwtUtil.createToken(user); + + // refreshToken 발급도 필요하다면 JwtUtil에서 따로 만들기 -> MVP에선 미구현 + + // 4. AuthRes 응답 + return new AuthRes( + user.getId(), + user.getEmail(), + user.getNickname(), + user.getBio(), + user.getProfileImageUrl(), + accessToken + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java new file mode 100644 index 0000000..9de56c3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java @@ -0,0 +1,48 @@ +package BookPick.mvp.domain.user.controller; + +import BookPick.mvp.common.ApiResponse; +import BookPick.mvp.domain.user.dto.AuthDtos.*; +import BookPick.mvp.domain.user.AuthService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + // 1. 회원가입 + @PostMapping("/signup") + public ResponseEntity> signUp(@Valid @RequestBody SignReq req) { + SignRes res = authService.signUp(req); + System.out.println(res); + return ResponseEntity.status(HttpStatus.CREATED) // 201 + .body(ApiResponse.created(res)); // created 사용 + } + + // 2. 로그인 + @PostMapping("/login") + public ResponseEntity> login(@Valid @RequestBody LoginReq req){ + + AuthRes authRes = authService.login(req); + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.ok(authRes)) ; //data 에 DTO 주기 + } + + // 3. 로그아웃 + // Note : 서베에서는 별도 로직 x, 클라이언트가 토큰 지움 + @PostMapping("/logout") + public ResponseEntity> logout(){ + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.ok(null)); + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/UserController.java new file mode 100644 index 0000000..73c8d1a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/controller/UserController.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.controller; + +import BookPick.mvp.common.ApiResponse; +import BookPick.mvp.domain.user.dto.UserDtos; +import BookPick.mvp.domain.user.service.UserService; +import BookPick.mvp.domain.user.dto.UserDtos.*; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + + + + +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java new file mode 100644 index 0000000..5852461 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java @@ -0,0 +1,54 @@ +package BookPick.mvp.domain.user.dto; + + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class AuthDtos { + + + + // 1. 회원가입 + public record SignReq( + @NotBlank @Email String email, + @Size(min=8, max = 72 ) String password + ){ } + + public record SignRes( + long user_id + ){ + } + + + // 2. 로그인 + public record LoginReq( + @NotBlank @Email String email, + @Size(min=8, max = 72 ) String password + ){} + + //로그인시 + public record AuthRes( + long user_id, + String email, + String nickname, + String bio, + String profile_image_url, + String access_token +) {} + + + + + // 3. 로그아웃 + public record LogoutReq( + @NotBlank String refreshToken + ){ + } + + // 3. 토큰 재발급 요청 + public record RefreshToken( + @NotBlank String refreshToken + ){ + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java b/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java new file mode 100644 index 0000000..af07dfe --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.dto; + +import jakarta.validation.constraints.*; + + +public class UserDtos { + + public record Res( + Long id, + String nickName, + String email + ){} + + // 사용처 : 회원 가입, + public record CreateReq( + @Email @NotBlank String email, + @Size(min=8, max = 72 ) @NotBlank String password + ){} +} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index 458e5ef..f8608a3 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -23,11 +23,14 @@ public class User { private String email; // 로그인 ID, 고유 @Column(name = "login_password", nullable = false, length = 255) - private String passwordHash; // 비밀번호 해시 + private String password; // 비밀번호 해시 @Column(length = 50) private String nickname; // 프로필 닉네임 + @Column(length = 20) + private String role; // ROLE_USER, ROLE_ADMIN 등 + @Column(length = 255) private String bio; // 자기소개 문구 diff --git a/src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java b/src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java new file mode 100644 index 0000000..54f0c3d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java @@ -0,0 +1,7 @@ +package BookPick.mvp.domain.user.exception; + +public class DuplicateEmailException extends RuntimeException { + public DuplicateEmailException(String message) { + super(message); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java b/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java new file mode 100644 index 0000000..2659174 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java @@ -0,0 +1,14 @@ +package BookPick.mvp.domain.user.repository; + +import BookPick.mvp.domain.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + boolean existsByEmail(String email); + Optional findByEmail(String email); +} + diff --git a/src/main/java/BookPick/mvp/domain/user/service/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/UserService.java new file mode 100644 index 0000000..ef15d3b --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/service/UserService.java @@ -0,0 +1,36 @@ +package BookPick.mvp.domain.user.service; + +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.dto.UserDtos.*; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.format.DateTimeFormatter; +import java.time.ZoneOffset; + +@Service +@Transactional +@RequiredArgsConstructor +public class UserService { + + private final UserRepository repo; + private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_INSTANT; + private final PasswordEncoder passwordEncoder; + + + // 회원가입, DB에 추가 + public Res create(CreateReq req) { + if (repo.existsByEmail(req.email())) { + throw new IllegalArgumentException("email-duplicated"); + } + User u = new User(); + u.setEmail(req.email()); + u.setPassword(passwordEncoder.encode((req.password()))); // 실제로는 BCrypt 권장 + repo.save(u); + return new Res(u.getId(),u.getNickname(), u.getEmail()); + + } +} diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 02443ef..37dd43c 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -1,73 +1,45 @@ -//package BookPick.mvp.global.config; -// -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.http.HttpMethod; -//import org.springframework.security.config.Customizer; -//import org.springframework.security.config.annotation.web.builders.HttpSecurity; -//import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -//import org.springframework.security.config.http.SessionCreationPolicy; -//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -//import org.springframework.security.crypto.password.PasswordEncoder; -//import org.springframework.security.web.SecurityFilterChain; -//import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -// -//@Configuration -//@EnableWebSecurity -//public class SecurityConfig { -// -// private static final String[] SWAGGER_WHITELIST = { -// "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html" -// }; -// -// private static final String[] STATIC_RESOURCES = { -// "/css/**", "/js/**", "/images/**", "/webjars/**", "/favicon.ico" -// }; -// -// // 뷰 라우트(비로그인 허용 페이지) -// private static final String[] VIEW_PUBLIC = { -// "/", "/item/list", "/member/login", "/member/signup", "/member/add", -// "/member/logout", "/member/logout/jwt" // 필요시 POST도 허용 -// }; -// -// @Bean -// PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } -// -// @Bean -// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { -// http.csrf(csrf -> csrf.disable()); -// http.cors(Customizer.withDefaults()); -// -// // ✅ JWT 필터는 UsernamePasswordAuthenticationFilter 앞에 -// http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class); -// -// // JWT 사용 → 세션 미사용 -// http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); -// -// http.authorizeHttpRequests(auth -> auth -// // 프리플라이트 -// .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() -// // 정적/스웨거/공개 뷰 -// .requestMatchers(STATIC_RESOURCES).permitAll() -// .requestMatchers(SWAGGER_WHITELIST).permitAll() -// .requestMatchers(VIEW_PUBLIC).permitAll() -// // API는 인증 필수 -// .requestMatchers("/api/**").authenticated() -// // 나머지는 필요에 따라: 뷰를 전부 공개하려면 permitAll, 내부페이지면 authenticated -// .anyRequest().permitAll() -// ); -// -// // 폼 로그인 페이지(뷰용): 페이지 자체는 permitAll로 열려 있으니 유지 가능 -// http.formLogin(form -> form -// .loginPage("/member/login") -// .defaultSuccessUrl("/item/list", true) -// ); -// -// http.logout(logout -> logout -// .logoutUrl("/member/logout") -// .logoutSuccessUrl("/item/list") -// ); -// -// return http.build(); -// } -//} +package BookPick.mvp.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean // 이 메서드가 반환하는 객체(SecurityFilterChain)를 스프링 빈으로 등록 + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + return http + // CSRF(Cross Site Request Forgery, 사이트 간 위조 요청) 방어 기능 비활성화 + // REST API 서버에서는 세션을 사용하지 않으므로 보통 꺼둡니다. + .csrf(csrf -> csrf.disable()) + + // 세션 관리 정책 설정 + // STATELESS = 서버가 세션을 생성하거나 사용하지 않음 (JWT 기반 인증 전제 조건) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // URL 별 접근 권한 규칙 정의 + .authorizeHttpRequests(auth -> auth + // 회원가입, 로그인 요청은 인증 없이 누구나 접근 가능 + .requestMatchers("/api/auth/signup", "/api/auth/login").permitAll() + // 나머지 모든 요청은 인증된 사용자만 접근 가능 + .anyRequest().authenticated() + ) + + // 위 설정으로 SecurityFilterChain 객체 생성 + .build(); + } +} diff --git a/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java b/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java new file mode 100644 index 0000000..5dfb400 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java @@ -0,0 +1,25 @@ +package BookPick.mvp.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration // 스프링 실행시 설정파일 읽어드리기 위한 어노테이션 +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .components(new Components()) + .info(apiInfo()); + } + + private Info apiInfo() { + return new Info() + .title("CodeArena Swagger") + .description("CodeArena 유저 및 인증 , ps, 알림에 관한 REST API") + .version("1.0.0"); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..74b464d --- /dev/null +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -0,0 +1,33 @@ +package BookPick.mvp.global.exception; + +import BookPick.mvp.common.ApiResponse; +import BookPick.mvp.domain.user.exception.DuplicateEmailException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) // @Valid 실패 → 400 +public ResponseEntity> handleBadRequest(MethodArgumentNotValidException ex) { + return ResponseEntity.badRequest() + .body(ApiResponse.error(400, "invalid_request")); +} + +@ExceptionHandler(DuplicateEmailException.class) // 이메일 중복 → 409 +public ResponseEntity> handleConflict(DuplicateEmailException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ApiResponse.error(409, "duplicate_email")); +} + +@ExceptionHandler(Exception.class) // 그 외 모든 예외 → 500 +public ResponseEntity> handle500(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(500, "server_error")); +} + +} + diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 6d8be81..045c84e 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -1,52 +1,48 @@ -//package BookPick.mvp.global.util; -// -//import com.apple.shop.domain.member.service.MyUserDetailsService; -//import io.jsonwebtoken.Claims; -//import io.jsonwebtoken.Jwts; -//import io.jsonwebtoken.io.Decoders; -//import io.jsonwebtoken.security.Keys; -//import org.springframework.security.core.Authentication; -//import org.springframework.stereotype.Component; -// -//import javax.crypto.SecretKey; -//import java.util.Date; -//import java.util.stream.Collectors; -// -//@Component -//public class JwtUtil { -// // 1. 키발급 -// static final SecretKey key = -// Keys.hmacShaKeyFor(Decoders.BASE64.decode( -// "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" -// )); -// -// -// // 2. JWT 생성 -// public static String createToken(Authentication auth) { -// MyUserDetailsService.CustomUser usr = (MyUserDetailsService.CustomUser) auth.getPrincipal(); -// -// String authorities = auth.getAuthorities().stream() //getAuthorities -> List return -// .map(a->a.getAuthority()) // getAuthority() -> String return -// .collect(Collectors.joining(",")); -// -// -// -// String jwt = Jwts.builder() -// .claim("username", usr.getUsername()) -// .claim("displayName", usr.displayName ) -// .claim("authorities", authorities) -// .issuedAt(new Date(System.currentTimeMillis())) -// .expiration(new Date(System.currentTimeMillis() + 1000*60*60)) // expiration : 만료 -// .signWith(key) -// .compact(); -// return jwt; -// } -// -// -// //3. JWT 오픈 -// public static Claims extractToken(String token) { -// Claims claims = Jwts.parser().verifyWith(key).build() -// .parseSignedClaims(token).getPayload(); -// return claims; -// } -//} +package BookPick.mvp.global.util; + +import BookPick.mvp.domain.user.entity.User; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.*; + +import javax.crypto.SecretKey; +import java.util.Date; + + +@Component +public class JwtUtil { + + + // 1. 키발급 + static final SecretKey key = + Keys.hmacShaKeyFor(Decoders.BASE64.decode( + "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" + )); + + // 2. JWT 생성 + public static String createToken(User user) { + + + String jwt = Jwts.builder() + .claim("email", user.getEmail()) // 이메일 + .claim("role", user.getRole()) + .issuedAt(new Date(System.currentTimeMillis())) // 발급 시간 + .expiration(new Date(System.currentTimeMillis() + 1000*60*60)) // 만료 시간 + .signWith(key) // 비밀키 서명 + .compact(); + return jwt; + } + + + //3. JWT 오픈 + public static Claims extractToken(String token) { + Claims claims = Jwts.parser().verifyWith(key).build() + .parseSignedClaims(token).getPayload(); + return claims; + } + + + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 755d597..642894c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,5 +4,14 @@ spring.datasource.username=nan7789 spring.datasource.password=gustjq3735! spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +jwt.secret=yourSuperSecretKeyHere1234567890 +jwt.access.exp=3600000 + spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.show_sql=False \ No newline at end of file +spring.jpa.properties.hibernate.show_sql=False + +# ?? ?? ?? +logging.level.root=INFO +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.security=DEBUG +logging.level.BookPick=DEBUG From a7deee054e1c3851936a43e04f3c6cf0fc2534fd Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 23 Sep 2025 00:06:55 +0900 Subject: [PATCH 008/291] =?UTF-8?q?feat=20:=20JwtFilter=EC=97=90=EC=84=9C?= =?UTF-8?q?=20HTTP=20=EC=9A=94=EC=B2=AD=EB=A7=88=EB=8B=A4=20jwt=20Token=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 6148 bytes src/.DS_Store | Bin 0 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes src/main/java/.DS_Store | Bin 0 -> 6148 bytes src/main/java/BookPick/.DS_Store | Bin 0 -> 6148 bytes src/main/java/BookPick/mvp/.DS_Store | Bin 0 -> 6148 bytes .../mvp/domain/Author/entity/Author.java | 23 +++ .../Author/repository/AuthorRepository.java | 10 + .../BookPick/mvp/domain/Book/entity/Book.java | 32 +++ .../Book/repository/BookRepository.java | 10 + .../controller/PreferenceController.java | 31 +++ .../domain/preference/dto/PreferenceDtos.java | 80 ++++++++ .../preference/entity/UserPreference.java | 101 ++++++++++ .../repository/PreferenceRepository.java | 8 + .../preference/service/PreferenceService.java | 56 ++++++ .../user/controller/AuthController.java | 10 +- .../user/controller/UserController.java | 6 - .../mvp/domain/user/dto/AuthDtos.java | 17 +- .../mvp/domain/user/dto/UserDtos.java | 22 +-- .../BookPick/mvp/domain/user/entity/User.java | 4 +- .../mvp/domain/user/entity/UserTaste.java | 38 ---- .../mvp/domain/user/entity/follow.java | 31 --- .../user/exception/InvalidLoginException.java | 11 ++ .../user/{ => service}/AuthService.java | 14 +- .../mvp/domain/user/service/UserService.java | 11 -- .../mvp/{common => global}/ApiResponse.java | 12 +- .../mvp/global/config/CorsConfig.java | 24 +++ .../BookPick/mvp/global/config/JwtFilter.java | 185 +++++++++--------- .../mvp/global/config/SecurityConfig.java | 8 +- .../exception/GlobalExceptionHandler.java | 58 ++++-- .../BookPick/mvp/global/util/JwtUtil.java | 19 ++ src/main/resources/application.properties | 9 + 32 files changed, 598 insertions(+), 232 deletions(-) create mode 100644 src/.DS_Store create mode 100644 src/main/.DS_Store create mode 100644 src/main/java/.DS_Store create mode 100644 src/main/java/BookPick/.DS_Store create mode 100644 src/main/java/BookPick/mvp/.DS_Store create mode 100644 src/main/java/BookPick/mvp/domain/Author/entity/Author.java create mode 100644 src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/Book/entity/Book.java create mode 100644 src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java create mode 100644 src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java create mode 100644 src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java create mode 100644 src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/entity/follow.java create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java rename src/main/java/BookPick/mvp/domain/user/{ => service}/AuthService.java (81%) rename src/main/java/BookPick/mvp/{common => global}/ApiResponse.java (64%) create mode 100644 src/main/java/BookPick/mvp/global/config/CorsConfig.java diff --git a/.DS_Store b/.DS_Store index 79f93c21bc9d1e1f2dc762c9e41b05e7366be3d7..3b1833de48bcbb35cb3523e2831377fcb4ee2efc 100644 GIT binary patch delta 98 zcmZoMXfc=|#>B!ku~2NHo+2aL#(>?7i&&T#**5bqJ!NEPW+-MTVo07W!*pC2$}S5o w%FD^mOJ`tUVBA<}%D9=GgP#Lv!sd_6-B)qu~2NHo+2a5#(>?7j4YdZSe`O&7U2+P+04Qr!~vAwEXeVlc{0C< SBL@QzFfuT(Y>p6F!wdlR^9|?# diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9f7b6b204a0817d09f5e58c2ded526e50cef7b74 GIT binary patch literal 6148 zcmeHK!AiqG5S?wKZ7D(z3OxqA7Obrb#Y>3w2aM=Jr6#0kFwIJnnnNk%tUu(J_&v_- zZi}_{DpF@)_RY@DB+N_L-2njMPQpWg1^`r12}?B`eh^wGU6GRUScrn25#;G}5X%7P z?<>*l_=^nCx3gdbK17gw{eHo79Hm*i{UJ(aYiqlLQm@>3k23c%KbxekKfa;arIc|n zxBcKM8s>w>?wL$7KT3vUl@NtP47t6Il2GQZoFrkYay`9JLm4!h(`mPN+_I0lC$pA4 z?RDB{_d2s#t-7~=aC$L#ibsij)g%i1-<0eaoWmO$i+WzXaT3eq0euv2K?XxeA={Wb zO+DZ0oO8j*3@`)Cz&ZxZ9$Kxh3t&GpzzqCb255hF%7ItbHz2=iuPIuxPaj?cGMI0#20kIVow zu*^VNPaAappRe!#my>wL3@`&5#elH-Uf;!%%-On99G$f`>OCq6#T6PCDQKvp7-Q)u cZlWqdzfA_BW3bSO9u&R^C>nTR27Z-+Ps-hfbN~PV literal 0 HcmV?d00001 diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..9a0d26ac33d8fd04855b6aa351aa805a5a4bde4c GIT binary patch literal 6148 zcmeHK%}T>S5T0$TZ7D(z3Oz1(Em&I>ikDF93mDOZN=;1BXv|8}HiuHkSzpK}@p+ut z-4^@fRiw^D0*lQ3Vx?hXKm&L}tpr~*I5Am6r+_Hv8Gf3)`H}K* z{<#v(j{nF2eY*lYg9l^qettjyEeexiqwytj`NGzA5v5YRbDvb|4!z+h>3D-1nq4Xt z`7_(|ufl%XtL~nuc<6<3f1ne>ppPN9*I^u}w4+9Gkmy|3ER<1t)!JmzY9H6-QR`${ zmy>q0fp)t&ot8^``v<2Ny_e`UR_}&Hf&ZJ99g8#gKx1Cd*d4@?iXYKO@nsl59}-Y7 zLhFI-4XJodWlOcs_+exQm;q*h7_fV2sls$H1Iz$3u*3lE4-%Eov6vauTL%uh1wf>0 zq!zTPm!KSJ(Xp5r#1#}_QV~t6uq}o#>FAd>&as#oH0dC0^C9e;h3!y;emg#2>TnQ_ zK^~a_W?+$lyqQ+%{6GJ_|6fev88g5PY!m~c&~>{VEXkg&E5*@SD^c%JNhmHeI8Q-C iAH^6;M{x~R3;HD)h>pd~AbL>vBA{vDff@Ky2EGAOri!ir literal 0 HcmV?d00001 diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..8884665b15509f6e9dd9d6565856e016c2249a50 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8O({YS3Oz1(Em&I>ikA@UU5w~Kr6#6mXv~(THHT8jSzpK}@p+ut z-GHSzcoMNQW%rw%pWVy{-5V;Vf`-UZDG@Z6x+*3Zk*hf_Si~OLILOjT z$wYtAgx_v4mr3}I*|+ZxVi|z%BbdZdI_q>kd8Jz0+OC5%>-XNX%)FUDo2RZny~WX$ zlyOk#esCR)vZ1|uCX<;TC8McIh{6$s+}%V;C^J{ilQ31eo^~`qhIVVQ==G1=;;47B zY>P#|+X21bT`n8e-u}Vq}@bbL!73WJWpTqAfu zxK0JssoXpPe>H_h!~iky&lupffj4lWD08;1m4|1ofc5|l1>piV*6EfnQ+Y3njWtVE_OC literal 0 HcmV?d00001 diff --git a/src/main/java/BookPick/.DS_Store b/src/main/java/BookPick/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..668a7c8403d73432075d3f59d6d8294b90acbbd9 GIT binary patch literal 6148 zcmeHK-AcnS6i(c98$;-Y!Y%{e4xF0`!<$m)3s}($mD$pv#V#3Zw_c1vul0p|5}(I& zk`x^FTEv|L$#;H}=7Z*kF~+?`bi$a!7;8d9wWT_T77q~0n%qe<0EcGC~jVJlo zIk;3Z3t~B$>w-9%K+3~iEF+bBY9XUk*ZL;V0vS7<)oL(2>x$FC`MN7s!+sC+VSl}D zH4l%DFRsV0$yBO0LnjBewd`9g;T;sanx#LNiIUIYDYMHgLSldzAO?tm?PkE93(fX+ z(?F{y28e+Q25^56&=6gVg+aY_K!?|7jMouSK*zTPqOj;%EDS;fgqu=8Q_AfVgPU^j z3!CR!EDV}*#`VlFj-9!Dyl_1`_=Qeq+%-r&F+dD#GEg(7gXjMh{4$k~{LK=whyh~Y zpE1DeBY)(S5Z<-5KNKMcg&r5Y7Obrb#Y>3w1&ruHr6#0kFx@RpYYwH5v%Zi|;`2DO zyEy~{-bCyS?0&QJvzz%K`@%YOfp@6_tMjV4H^`QSgR%wGn}Rq6$cI~-jrm4t;J zgg5atn>hOyN-l#~P8T{Mj;0WDe;dn4Wu98eDAl>XakN1uPItW?j!t{xWO%meiS=mE z2YocyY}&1Z!=v-7$xAYm>dg?zfpaZ;77KU>rBu_azmSQNPhc-{idaHofEXYKh=J{6 zz@7!I&i0i~6%zx*zz+=I{ve0lQ&&b62uH0g}%m0=#ca`AZKdUdc19nQFGka}W(7$`GPGeZZ@|4aB~Dj)gf z6dDl&#K1pefY-Z7 literal 0 HcmV?d00001 diff --git a/src/main/java/BookPick/mvp/domain/Author/entity/Author.java b/src/main/java/BookPick/mvp/domain/Author/entity/Author.java new file mode 100644 index 0000000..0477980 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/Author/entity/Author.java @@ -0,0 +1,23 @@ +package BookPick.mvp.domain.Author.entity; + + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Author { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String name; + + @Column(length = 200) + private String bio; // 간단한 소개 +} diff --git a/src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java b/src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java new file mode 100644 index 0000000..7e7485e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.Author.repository; + +import BookPick.mvp.domain.Author.entity.Author; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AuthorRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/Book/entity/Book.java b/src/main/java/BookPick/mvp/domain/Book/entity/Book.java new file mode 100644 index 0000000..75ce0e6 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/Book/entity/Book.java @@ -0,0 +1,32 @@ +package BookPick.mvp.domain.Book.entity; + + +import BookPick.mvp.domain.Author.entity.Author; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id", nullable = false) + private Author author; // 책:작가 = N:1 + + @Column(length = 200) + private String description; + + @Column(length = 50) + private String genre; +} + diff --git a/src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java b/src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java new file mode 100644 index 0000000..7e95d18 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.Book.repository; + +import BookPick.mvp.domain.Book.entity.Book; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BookRepository extends JpaRepository { + Optional findByTitle(String title); +} diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java new file mode 100644 index 0000000..baed872 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.preference.controller; + +import BookPick.mvp.global.ApiResponse; +import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; +import BookPick.mvp.domain.preference.service.PreferenceService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; + + +@RestController +@RequestMapping("/api/users/{id}/preferences") +@RequiredArgsConstructor +public class PreferenceController { + private final PreferenceService preferenceService; + + @PostMapping + public ResponseEntity> create( + @PathVariable("id") Long userId, + @Valid @RequestBody CreateReq req + ) { + PreferenceRes res = preferenceService.create(userId, req); + URI location = URI.create("/api/users/" + userId + "/preferences"); + + return ResponseEntity.created(location) + .body(ApiResponse.success("success", res)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java new file mode 100644 index 0000000..f7fb080 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java @@ -0,0 +1,80 @@ +package BookPick.mvp.domain.preference.dto; + +import BookPick.mvp.domain.Author.entity.Author; +import BookPick.mvp.domain.Book.entity.Book; +import BookPick.mvp.domain.preference.entity.UserPreference; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public class PreferenceDtos { + + + // 1. 취향 설정 + + // Req + public record CreateReq( + @NotBlank String mbti, + @NotNull List favoriteAuthors, // 좋아하는 작가 + @NotNull List favoriteBooks, // 좋아하는 작가 + @NotNull List selectionCriteria, // 독서 선호 분위기 + @NotNull List readingHabits, // 독서 습관 + @NotNull List preferredGenres, // 선호 장르 + @NotNull List keywords, // 키워드 + @NotNull List recommendedTrends // + ){} + + + + // 2. 취향 수정 + public record UpdateReq( + @NotBlank String mbti, + @NotNull List favoriteAuthors, + @NotNull List selectionCriteria, + @NotNull List readingHabits, + @NotNull List preferredGenres, + @NotNull List keywords, + @NotNull List recommendedTrends + ) {} + + + // PreferenceRes (MVP용 단순화) + public record PreferenceRes( + Long id, + String mbti, + List favoriteAuthors, + List favoriteBooks, + List selectionCriteria, + List readingHabits, + List preferredGenres, + List keywords, + List recommendedTrends + ) { + public static PreferenceRes from(UserPreference p) { + return new PreferenceRes( + p.getId(), + p.getMbti(), + p.getFavoriteAuthors().stream() + .map(Author::getName) // Author → String + .toList(), + p.getFavoriteBooks().stream() + .map(Book::getTitle) // Book → String + .toList(), + p.getSelectionCriteria(), + p.getReadingHabits(), + p.getPreferredGenres(), + p.getKeywords(), + p.getRecommendedTrends() + ); +} + + } + + + +} + + + + diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java new file mode 100644 index 0000000..12aa11d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java @@ -0,0 +1,101 @@ +package BookPick.mvp.domain.preference.entity; + +import BookPick.mvp.domain.Author.entity.Author; +import BookPick.mvp.domain.Book.entity.Book; +import BookPick.mvp.domain.preference.dto.PreferenceDtos.CreateReq; +import BookPick.mvp.domain.preference.dto.PreferenceDtos.UpdateReq; +import BookPick.mvp.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + + + + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class UserPreference { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 유저와 1:1 관계 (유저당 하나만) + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "userId", unique = true, nullable = false) + private User user; + + private String mbti; + + @ManyToMany + @JoinTable( + name = "preference_authors", + joinColumns = @JoinColumn(name = "preference_id"), + inverseJoinColumns = @JoinColumn(name = "author_id") + ) + private List favoriteAuthors; + + + @ManyToMany + @JoinTable( + name = "preference_books", + joinColumns = @JoinColumn(name = "preference_id"), + inverseJoinColumns = @JoinColumn(name = "book_id") + ) + private List favoriteBooks; + + @ElementCollection + @CollectionTable(name = "preference_selection_criteria", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "criteria") + private List selectionCriteria; + + @ElementCollection + @CollectionTable(name = "preference_reading_habits", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "habit") + private List readingHabits; + + @ElementCollection + @CollectionTable(name = "preference_genres", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "genre") + private List preferredGenres; + + @ElementCollection + @CollectionTable(name = "preference_keywords", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "keyword") + private List keywords; + + @ElementCollection + @CollectionTable(name = "preference_trends", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "trend") + private List recommendedTrends; + + // ---- 팩토리 메서드 ---- + public static UserPreference from(CreateReq req, User user) { + UserPreference pref = new UserPreference(); + pref.user = user; + pref.mbti = req.mbti(); + // Author/Book 매핑은 별도 Repository에서 가져와야 함 + pref.selectionCriteria = req.selectionCriteria(); + pref.readingHabits = req.readingHabits(); + pref.preferredGenres = req.preferredGenres(); + pref.keywords = req.keywords(); + pref.recommendedTrends = req.recommendedTrends(); + return pref; + } + + // ---- 수정 적용 ---- + public void apply(UpdateReq req) { + this.mbti = req.mbti(); + this.selectionCriteria = req.selectionCriteria(); + this.readingHabits = req.readingHabits(); + this.preferredGenres = req.preferredGenres(); + this.keywords = req.keywords(); + this.recommendedTrends = req.recommendedTrends(); + } + +} diff --git a/src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java b/src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java new file mode 100644 index 0000000..db675a2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java @@ -0,0 +1,8 @@ +package BookPick.mvp.domain.preference.repository; + +import BookPick.mvp.domain.preference.entity.UserPreference; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PreferenceRepository extends JpaRepository { + boolean existsByUserId(Long userId); +} diff --git a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java new file mode 100644 index 0000000..c1d00d2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java @@ -0,0 +1,56 @@ +package BookPick.mvp.domain.preference.service; + +import BookPick.mvp.domain.Author.entity.Author; +import BookPick.mvp.domain.Author.repository.AuthorRepository; +import BookPick.mvp.domain.Book.entity.Book; +import BookPick.mvp.domain.Book.repository.BookRepository; +import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; +import BookPick.mvp.domain.preference.entity.UserPreference; +import BookPick.mvp.domain.preference.repository.PreferenceRepository; +import BookPick.mvp.domain.user.entity.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PreferenceService { + private final PreferenceRepository preferenceRepository; + private final AuthorRepository authorRepository; + private final BookRepository bookRepository; + + @Transactional + public PreferenceRes create(Long userId, CreateReq req) { + User user = new User(); + user.setId(userId); + + if (preferenceRepository.existsByUserId(userId)) { + throw new IllegalStateException("이미 등록된 선호정보가 있습니다."); + } + + // --- 문자열 요청 → 엔티티 변환 --- + List authors = req.favoriteAuthors().stream() + .map(name -> authorRepository.findByName((name)) + .orElseGet(() -> authorRepository.save( + Author.builder().name(name).build() + )) + ).toList(); + + List books = req.favoriteBooks().stream() + .map(title -> bookRepository.findByTitle((title)) + .orElseGet(() -> bookRepository.save( + Book.builder().title(title).build() + )) + ).toList(); + + // --- UserPreference 생성 --- + UserPreference pref = UserPreference.from(req, user); + pref.setFavoriteAuthors(authors); + pref.setFavoriteBooks(books); + + UserPreference saved = preferenceRepository.save(pref); + return PreferenceRes.from(saved); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java index 9de56c3..15b1cfd 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java @@ -1,8 +1,8 @@ package BookPick.mvp.domain.user.controller; -import BookPick.mvp.common.ApiResponse; +import BookPick.mvp.global.ApiResponse; import BookPick.mvp.domain.user.dto.AuthDtos.*; -import BookPick.mvp.domain.user.AuthService; +import BookPick.mvp.domain.user.service.AuthService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -24,7 +24,7 @@ public ResponseEntity> signUp(@Valid @RequestBody SignReq r SignRes res = authService.signUp(req); System.out.println(res); return ResponseEntity.status(HttpStatus.CREATED) // 201 - .body(ApiResponse.created(res)); // created 사용 + .body(ApiResponse.created("create",res)); // created 사용 } // 2. 로그인 @@ -33,7 +33,7 @@ public ResponseEntity> login(@Valid @RequestBody LoginReq r AuthRes authRes = authService.login(req); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.ok(authRes)) ; //data 에 DTO 주기 + .body(ApiResponse.success("success", authRes)) ; //data 에 DTO 주기 } // 3. 로그아웃 @@ -41,7 +41,7 @@ public ResponseEntity> login(@Valid @RequestBody LoginReq r @PostMapping("/logout") public ResponseEntity> logout(){ return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.ok(null)); + .body(ApiResponse.success("success", null)); } diff --git a/src/main/java/BookPick/mvp/domain/user/controller/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/UserController.java index 73c8d1a..daba4c4 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/UserController.java @@ -1,11 +1,5 @@ package BookPick.mvp.domain.user.controller; -import BookPick.mvp.common.ApiResponse; -import BookPick.mvp.domain.user.dto.UserDtos; -import BookPick.mvp.domain.user.service.UserService; -import BookPick.mvp.domain.user.dto.UserDtos.*; -import jakarta.validation.Valid; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController diff --git a/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java index 5852461..1275bf8 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java @@ -10,37 +10,40 @@ public class AuthDtos { // 1. 회원가입 - public record SignReq( + //Req + public record SignReq( @NotBlank @Email String email, @Size(min=8, max = 72 ) String password ){ } + //res public record SignRes( - long user_id + long userId ){ } // 2. 로그인 + //Req public record LoginReq( @NotBlank @Email String email, @Size(min=8, max = 72 ) String password ){} - //로그인시 + //Res public record AuthRes( - long user_id, + long userId, String email, String nickname, String bio, - String profile_image_url, - String access_token + String profileImageUrl, + String accessToken ) {} - // 3. 로그아웃 + // 3. 로그아 public record LogoutReq( @NotBlank String refreshToken ){ diff --git a/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java b/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java index af07dfe..39a1784 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java @@ -5,15 +5,15 @@ public class UserDtos { - public record Res( - Long id, - String nickName, - String email - ){} - - // 사용처 : 회원 가입, - public record CreateReq( - @Email @NotBlank String email, - @Size(min=8, max = 72 ) @NotBlank String password - ){} +// public record Res( +// Long id, +// String nickName, +// String email +// ){} +// +// // 사용처 : 회원 가입, +// public record CreateReq( +// @Email @NotBlank String email, +// @Size(min=8, max = 72 ) @NotBlank String password +// ){} } diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index f8608a3..8529d31 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -16,7 +16,7 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id") + @Column(name = "userId") private Long id; // 내부 식별자 (PK) @Column(name = "login_email", nullable = false, unique = true, length = 255) @@ -34,7 +34,7 @@ public class User { @Column(length = 255) private String bio; // 자기소개 문구 - @Column(name = "profile_image_url", length = 500) + @Column(name = "profileImageUrl", length = 500) private String profileImageUrl; // 프로필 사진 경로 @CreationTimestamp diff --git a/src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java b/src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java deleted file mode 100644 index e79c233..0000000 --- a/src/main/java/BookPick/mvp/domain/user/entity/UserTaste.java +++ /dev/null @@ -1,38 +0,0 @@ -package BookPick.mvp.domain.user.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "user_taste") -@Getter -@Setter -public class UserTaste { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 사용자 선호도 ID (PK) - - @OneToOne - @JoinColumn(name = "user_id", nullable = false, unique = true) // UNIQUE + FK - private User user; // user 테이블 참조 - - @Column(length = 4) - private String mbti; // MBTI - - @Column(name = "reading_style", length = 50) - private String readingStyle; // 독서 스타일 - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 생성 시각 - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; // 수정 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/follow.java b/src/main/java/BookPick/mvp/domain/user/entity/follow.java deleted file mode 100644 index 54a5510..0000000 --- a/src/main/java/BookPick/mvp/domain/user/entity/follow.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.user.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "follow") -@Getter -@Setter -public class follow { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; // 내부 식별자 (PK) - - @ManyToOne - @JoinColumn(name = "follower_id", nullable = false) - private User follower; // 팔로우를 건 유저 (FK → User.user_id) - - @ManyToOne - @JoinColumn(name = "following_id", nullable = false) - private User following; // 팔로우 당한 유저 (FK → User.user_id) - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; // 팔로우 생성 시각 -} diff --git a/src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java b/src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java new file mode 100644 index 0000000..5501b75 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.user.exception; + +public class InvalidLoginException extends RuntimeException { + public InvalidLoginException() { + super("잘못된 로그인 시도입니다."); + } + + public InvalidLoginException(String message) { + super(message); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/AuthService.java b/src/main/java/BookPick/mvp/domain/user/service/AuthService.java similarity index 81% rename from src/main/java/BookPick/mvp/domain/user/AuthService.java rename to src/main/java/BookPick/mvp/domain/user/service/AuthService.java index 28cbde7..fba7569 100644 --- a/src/main/java/BookPick/mvp/domain/user/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/AuthService.java @@ -1,7 +1,8 @@ -package BookPick.mvp.domain.user; +package BookPick.mvp.domain.user.service; import BookPick.mvp.domain.user.dto.AuthDtos.*; import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.DuplicateEmailException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.global.util.JwtUtil; import lombok.RequiredArgsConstructor; @@ -22,17 +23,26 @@ public class AuthService { @Transactional public SignRes signUp(SignReq req) { + // 1. 이메일 중복 확인 + if (userRepository.existsByEmail(req.email())) { + throw new DuplicateEmailException("이미 존재하는 이메일입니다."); + } + + // 2. 신규 유저 생성 User user = new User(); user.setEmail(req.email()); user.setPassword(passwordEncoder.encode(req.password())); user.setRole("normal_user"); // normal_user, curator - User savedUser = userRepository.save(user); // 1. DB에 저장 + // 3. DB 저장 + User savedUser = userRepository.save(user); + // 4. 응답 return new SignRes(savedUser.getId()); } + public AuthRes login(LoginReq req){ // 1. 이메일 검증 diff --git a/src/main/java/BookPick/mvp/domain/user/service/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/UserService.java index ef15d3b..2cf7469 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/UserService.java @@ -21,16 +21,5 @@ public class UserService { private final PasswordEncoder passwordEncoder; - // 회원가입, DB에 추가 - public Res create(CreateReq req) { - if (repo.existsByEmail(req.email())) { - throw new IllegalArgumentException("email-duplicated"); - } - User u = new User(); - u.setEmail(req.email()); - u.setPassword(passwordEncoder.encode((req.password()))); // 실제로는 BCrypt 권장 - repo.save(u); - return new Res(u.getId(),u.getNickname(), u.getEmail()); - } } diff --git a/src/main/java/BookPick/mvp/common/ApiResponse.java b/src/main/java/BookPick/mvp/global/ApiResponse.java similarity index 64% rename from src/main/java/BookPick/mvp/common/ApiResponse.java rename to src/main/java/BookPick/mvp/global/ApiResponse.java index 6259ac9..41d1c4c 100644 --- a/src/main/java/BookPick/mvp/common/ApiResponse.java +++ b/src/main/java/BookPick/mvp/global/ApiResponse.java @@ -1,4 +1,4 @@ -package BookPick.mvp.common; +package BookPick.mvp.global; import lombok.Getter; @@ -19,15 +19,15 @@ public ApiResponse(int status, String message, T data) { } // 정적 팩토리 메서드 - public static ApiResponse ok(T data) { - return new ApiResponse<>( 200, null, data) ; + public static ApiResponse success(String msg, T data) { + return new ApiResponse<>( 200, msg, data) ; } - public static ApiResponse created(T data) { - return new ApiResponse<>(201, null, data); + public static ApiResponse created(String msg, T data) { + return new ApiResponse<>(201, msg, data); } - public static ApiResponse noContent() { + public static ApiResponse noContent() { //헤더만 주는놈 return new ApiResponse<>(204, null, null); } diff --git a/src/main/java/BookPick/mvp/global/config/CorsConfig.java b/src/main/java/BookPick/mvp/global/config/CorsConfig.java new file mode 100644 index 0000000..cb956cb --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/CorsConfig.java @@ -0,0 +1,24 @@ +package BookPick.mvp.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("https://nationally-sizes-carmen-press.trycloudflare.com") + .allowedMethods("GET","POST","PUT","DELETE","PATCH","OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } +} diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 5ebd86c..a0809ed 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,95 +1,90 @@ -//package BookPick.mvp.global.config; -// -//import com.apple.shop.domain.member.service.MyUserDetailsService.CustomUser; -//import com.apple.shop.global.util.JwtUtil; -//import io.jsonwebtoken.Claims; -//import io.jsonwebtoken.ExpiredJwtException; -//import io.jsonwebtoken.JwtException; -//import jakarta.servlet.FilterChain; -//import jakarta.servlet.ServletException; -//import jakarta.servlet.http.Cookie; -//import jakarta.servlet.http.HttpServletRequest; -//import jakarta.servlet.http.HttpServletResponse; -//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -//import org.springframework.security.core.authority.SimpleGrantedAuthority; -//import org.springframework.security.core.context.SecurityContextHolder; -//import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -//import org.springframework.web.filter.OncePerRequestFilter; -// -//import java.io.IOException; -//import java.util.Arrays; -// -//public class JwtFilter extends OncePerRequestFilter { -// -// @Override -// protected void doFilterInternal( -// HttpServletRequest request, HttpServletResponse response, FilterChain filterChain -// ) throws ServletException, IOException { -// -// Cookie jwtCookie = findCookie(request, "jwt"); -// if (jwtCookie == null || isAuthenticatedAlready()) { -// filterChain.doFilter(request, response); -// return; -// } -// -// try { -// // 1) 파싱/검증 -// Claims claim = JwtUtil.extractToken(jwtCookie.getValue()); -// -// // 2) 인증 세팅 -// String[] arr = claim.get("authorities").toString().split(","); -// var authorities = Arrays.stream(arr).map(SimpleGrantedAuthority::new).toList(); -// -// String username = String.valueOf(claim.get("username")); -// CustomUser principal = new CustomUser(username, "", authorities); -// principal.displayName = String.valueOf(claim.get("displayName")); -// -// var authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); -// authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); -// SecurityContextHolder.getContext().setAuthentication(authToken); -// -// filterChain.doFilter(request, response); -// } catch (ExpiredJwtException e) { -// // 만료 토큰: 쿠키 제거 -// clearJwtCookie(response); -// // API 경로면 401, 뷰/정적은 계속 통과 -// if (isApi(request)) { -// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// return; -// } -// filterChain.doFilter(request, response); -// } catch (JwtException | IllegalArgumentException e) { -// // 서명 불일치/손상 등: 쿠키 제거 후 동일 처리 -// clearJwtCookie(response); -// if (isApi(request)) { -// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// return; -// } -// filterChain.doFilter(request, response); -// } -// } -// -// private boolean isAuthenticatedAlready() { -// return SecurityContextHolder.getContext().getAuthentication() != null; -// } -// -// private boolean isApi(HttpServletRequest req) { -// String uri = req.getRequestURI(); -// return uri != null && uri.startsWith("/api/"); -// } -// -// private Cookie findCookie(HttpServletRequest request, String name) { -// Cookie[] cookies = request.getCookies(); -// if (cookies == null) return null; -// for (Cookie c : cookies) if (name.equals(c.getName())) return c; -// return null; -// } -// -// private void clearJwtCookie(HttpServletResponse res) { -// Cookie c = new Cookie("jwt", null); -// c.setPath("/"); -// c.setMaxAge(0); -// c.setHttpOnly(true); -// res.addCookie(c); -// } -//} +package BookPick.mvp.global.config; + +import BookPick.mvp.global.ApiResponse; +import BookPick.mvp.global.util.JwtUtil; // JWT 파싱/검증 유틸 +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; // JWT Payload(클레임) 타입 +import jakarta.servlet.FilterChain; // 필터 체인 (다음 필터 호출용) +import jakarta.servlet.ServletException; // 서블릿 예외 +import jakarta.servlet.http.HttpServletRequest; // HTTP 요청 +import jakarta.servlet.http.HttpServletResponse; // HTTP 응답 +import lombok.RequiredArgsConstructor; // 생성자 자동 생성 +import org.springframework.security.authentication.AuthenticationServiceException; // 인증 관련 예외 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; // 인증 객체 구현체 +import org.springframework.security.core.authority.SimpleGrantedAuthority; // 권한 객체 +import org.springframework.security.core.context.SecurityContextHolder; // 인증 정보를 담는 컨텍스트 +import org.springframework.stereotype.Component; // 스프링 빈 등록 +import org.springframework.web.filter.OncePerRequestFilter; // 요청마다 한 번 실행되는 필터 + +import java.io.IOException; // IO 예외 +import java.util.List; // 권한 리스트 + +/** + * JWT 필터 (MVP 버전, 리프레시 없음) + * - Authorization 헤더에서 토큰 추출 + * - JwtUtil.parse(token)으로 만료/서명 검증 + * - 성공하면 SecurityContext에 인증 정보 세팅 + * - 실패하면 401 Unauthorized 반환 + */ +@Component // 스프링 컴포넌트 스캔에 의해 빈 등록 +@RequiredArgsConstructor // final 필드 생성자 자동 주입 +public class JwtFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; // JwtUtil 주입받음 + + private static final String AUTH_HEADER = "Authorization"; // 토큰이 담기는 헤더 이름 + private static final String BEARER = "Bearer "; // 토큰 앞에 붙는 접두어 + + @Override + protected void doFilterInternal(HttpServletRequest request, // 들어온 요청 + HttpServletResponse response, // 응답 + FilterChain chain) // 필터 체인 : 필터들을 관리하는 객체 , chain.dofilter -> 다음필터 실행 + throws ServletException, IOException { + + // 1. 요청 헤더에서 Authorization 값 꺼냄 + String header = request.getHeader(AUTH_HEADER); + + // 2. 헤더가 없거나 Bearer로 시작하지 않으면 → 그냥 다음 필터로 넘김 + if (header == null || !header.startsWith(BEARER)) { + chain.doFilter(request, response); // 다음 필터로 넘기는 코드 + return; + } + + // 3. "Bearer " 부분 제거하고 실제 토큰만 추출 + String token = header.substring(BEARER.length()).trim(); + + try { + // 4. JwtUtil로 토큰 검증 (만료·서명 체크 포함) + Claims claims = jwtUtil.parse(token); + + // 5. 클레임에서 사용자 정보 꺼내기 + String email = claims.get("email", String.class); + String role = claims.get("role", String.class); + + // 6. 권한 객체 생성 (ROLE_ 접두어 보정) + SimpleGrantedAuthority authority = + (role != null && role.startsWith("ROLE_")) + ? new SimpleGrantedAuthority(role) + : new SimpleGrantedAuthority("ROLE_" + role); + + // 7. 인증 객체 생성 (principal: email, credentials: null, authorities: 권한) + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(email, null, List.of(authority)); + + // 8. SecurityContext에 인증 객체 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 9. 다음 필터로 진행 + chain.doFilter(request, response); + + } catch (AuthenticationServiceException ex) { + // 10. 토큰이 만료되었거나 유효하지 않음 + SecurityContextHolder.clearContext(); // 기존 인증 정보 제거 + ApiResponse body = new ApiResponse<>(401, ex.getMessage(), null); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 반환 + response.setContentType("application/json;charset=UTF-8"); // JSON 응답 + new ObjectMapper().writeValue(response.getWriter(), body); + return; // 컨트롤러로 요청 안 넘어감 + } + } +} diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 37dd43c..374766a 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @@ -20,6 +21,8 @@ PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + private final JwtFilter jwtFilter; + @Bean // 이 메서드가 반환하는 객체(SecurityFilterChain)를 스프링 빈으로 등록 SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http @@ -34,10 +37,13 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // URL 별 접근 권한 규칙 정의 .authorizeHttpRequests(auth -> auth // 회원가입, 로그인 요청은 인증 없이 누구나 접근 가능 - .requestMatchers("/api/auth/signup", "/api/auth/login").permitAll() + .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/logout").permitAll() + .requestMatchers("/api/users/*/preferences").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() // 나머지 모든 요청은 인증된 사용자만 접근 가능 .anyRequest().authenticated() ) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // 위 설정으로 SecurityFilterChain 객체 생성 .build(); diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index 74b464d..d3ebb86 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -1,7 +1,9 @@ package BookPick.mvp.global.exception; -import BookPick.mvp.common.ApiResponse; +import BookPick.mvp.global.ApiResponse; import BookPick.mvp.domain.user.exception.DuplicateEmailException; +import BookPick.mvp.domain.user.exception.InvalidLoginException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -11,23 +13,45 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(MethodArgumentNotValidException.class) // @Valid 실패 → 400 -public ResponseEntity> handleBadRequest(MethodArgumentNotValidException ex) { - return ResponseEntity.badRequest() - .body(ApiResponse.error(400, "invalid_request")); -} + // @Valid 검증 실패 → 400 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { + return ResponseEntity.badRequest() + .body(ApiResponse.error(400, "invalid_request")); + } -@ExceptionHandler(DuplicateEmailException.class) // 이메일 중복 → 409 -public ResponseEntity> handleConflict(DuplicateEmailException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(ApiResponse.error(409, "duplicate_email")); -} + // 잘못된 인자 (기타 비즈니스 로직에서 발생) → 400 + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) { + return ResponseEntity.badRequest() + .body(ApiResponse.error(400, "invalid_request")); + } -@ExceptionHandler(Exception.class) // 그 외 모든 예외 → 500 -public ResponseEntity> handle500(Exception ex) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(500, "server_error")); -} + // 로그인 실패 → 401 + @ExceptionHandler(InvalidLoginException.class) + public ResponseEntity> handleInvalidLogin(InvalidLoginException ex) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ApiResponse.error(401, "invalid_credentials")); + } -} + // 이메일 중복 (직접 던진 경우) → 409 + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity> handleDuplicateEmail(DuplicateEmailException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ApiResponse.error(409, "duplicate_email")); + } + + // DB Unique 제약 등 무결성 위반 → 409 + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity> handleUniqueViolation(DataIntegrityViolationException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(ApiResponse.error(409, "duplicate_email")); + } + // 그 외 모든 예외 → 500 + @ExceptionHandler(Exception.class) + public ResponseEntity> handle500(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(500, "server_error")); + } +} diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 045c84e..ca3fc33 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -1,8 +1,10 @@ package BookPick.mvp.global.util; + import BookPick.mvp.domain.user.entity.User; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Component; import io.jsonwebtoken.*; @@ -43,6 +45,23 @@ public static Claims extractToken(String token) { return claims; } + // 4. JWT 검증 + public Claims parse(String token) { + try { + return Jwts.parser() + .verifyWith(key) // 서명 검증 + .clockSkewSeconds(60) // 시계 오차 허용(선택) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { + throw new AuthenticationServiceException("TOKEN_EXPIRED", e); + } catch (JwtException e) { + throw new AuthenticationServiceException("TOKEN_INVALID", e); + } +} + + } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 642894c..6b2bc66 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,3 +15,12 @@ logging.level.root=INFO logging.level.org.springframework.web=DEBUG logging.level.org.springframework.security=DEBUG logging.level.BookPick=DEBUG + +/ openApi ?? ??? +springdoc.api-docs.enabled=true +// ui ??? +springdoc.swagger-ui.enabled=true +// ?? ?? ?? ?? +springdoc.swagger-ui.path=/swagger-ui.html +// ???? ?? ?? ?? ?? +springdoc.override-with-generic-response=false \ No newline at end of file From 4ec900ae0c42dfb30e7f8d18c95d2c4b37baa3dc Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 26 Sep 2025 00:09:54 +0900 Subject: [PATCH 009/291] =?UTF-8?q?feat:=20[Auth]=20JwtUtil=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AuthController.java | 4 +- .../domain/{user => auth}/dto/AuthDtos.java | 7 +- .../exception/DuplicateEmailException.java | 2 +- .../exception/InvalidLoginException.java | 2 +- .../{Author => author}/entity/Author.java | 2 +- .../repository/AuthorRepository.java | 4 +- .../domain/{Book => book}/entity/Book.java | 4 +- .../repository/BookRepository.java | 4 +- .../domain/preference/dto/PreferenceDtos.java | 4 +- .../preference/entity/UserPreference.java | 4 +- .../preference/service/PreferenceService.java | 8 +- .../mvp/domain/user/service/AuthService.java | 4 +- .../exception/GlobalExceptionHandler.java | 4 +- .../BookPick/mvp/global/util/JwtUtil.java | 125 ++++++++++++------ src/main/resources/application.properties | 14 +- 15 files changed, 119 insertions(+), 73 deletions(-) rename src/main/java/BookPick/mvp/domain/{user => auth}/controller/AuthController.java (94%) rename src/main/java/BookPick/mvp/domain/{user => auth}/dto/AuthDtos.java (84%) rename src/main/java/BookPick/mvp/domain/{user => auth}/exception/DuplicateEmailException.java (77%) rename src/main/java/BookPick/mvp/domain/{user => auth}/exception/InvalidLoginException.java (84%) rename src/main/java/BookPick/mvp/domain/{Author => author}/entity/Author.java (90%) rename src/main/java/BookPick/mvp/domain/{Author => author}/repository/AuthorRepository.java (68%) rename src/main/java/BookPick/mvp/domain/{Book => book}/entity/Book.java (86%) rename src/main/java/BookPick/mvp/domain/{Book => book}/repository/BookRepository.java (69%) diff --git a/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java similarity index 94% rename from src/main/java/BookPick/mvp/domain/user/controller/AuthController.java rename to src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 15b1cfd..f055e8f 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -1,7 +1,7 @@ -package BookPick.mvp.domain.user.controller; +package BookPick.mvp.domain.auth.controller; import BookPick.mvp.global.ApiResponse; -import BookPick.mvp.domain.user.dto.AuthDtos.*; +import BookPick.mvp.domain.auth.dto.AuthDtos.*; import BookPick.mvp.domain.user.service.AuthService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java rename to src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index 1275bf8..cc82377 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.dto; +package BookPick.mvp.domain.auth.dto; import jakarta.validation.constraints.Email; @@ -37,7 +37,10 @@ public record AuthRes( String nickname, String bio, String profileImageUrl, - String accessToken + String accessToken, + String refreshToken, // 👈 추가 + long expiresIn // 👈 선택: Access 만료(초) + ) {} diff --git a/src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java b/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java similarity index 77% rename from src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java rename to src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java index 54f0c3d..2a5f21e 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/DuplicateEmailException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.auth.exception; public class DuplicateEmailException extends RuntimeException { public DuplicateEmailException(String message) { diff --git a/src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java rename to src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java index 5501b75..3db3b95 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/InvalidLoginException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.auth.exception; public class InvalidLoginException extends RuntimeException { public InvalidLoginException() { diff --git a/src/main/java/BookPick/mvp/domain/Author/entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java similarity index 90% rename from src/main/java/BookPick/mvp/domain/Author/entity/Author.java rename to src/main/java/BookPick/mvp/domain/author/entity/Author.java index 0477980..20e50a8 100644 --- a/src/main/java/BookPick/mvp/domain/Author/entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/entity/Author.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.Author.entity; +package BookPick.mvp.domain.author.entity; import jakarta.persistence.*; diff --git a/src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java similarity index 68% rename from src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java rename to src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java index 7e7485e..b1dfb99 100644 --- a/src/main/java/BookPick/mvp/domain/Author/repository/AuthorRepository.java +++ b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.Author.repository; +package BookPick.mvp.domain.author.repository; -import BookPick.mvp.domain.Author.entity.Author; +import BookPick.mvp.domain.author.entity.Author; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/src/main/java/BookPick/mvp/domain/Book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java similarity index 86% rename from src/main/java/BookPick/mvp/domain/Book/entity/Book.java rename to src/main/java/BookPick/mvp/domain/book/entity/Book.java index 75ce0e6..7c3833f 100644 --- a/src/main/java/BookPick/mvp/domain/Book/entity/Book.java +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -1,7 +1,7 @@ -package BookPick.mvp.domain.Book.entity; +package BookPick.mvp.domain.book.entity; -import BookPick.mvp.domain.Author.entity.Author; +import BookPick.mvp.domain.author.entity.Author; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java similarity index 69% rename from src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java rename to src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java index 7e95d18..7202360 100644 --- a/src/main/java/BookPick/mvp/domain/Book/repository/BookRepository.java +++ b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.Book.repository; +package BookPick.mvp.domain.book.repository; -import BookPick.mvp.domain.Book.entity.Book; +import BookPick.mvp.domain.book.entity.Book; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java index f7fb080..adc33b4 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.preference.dto; -import BookPick.mvp.domain.Author.entity.Author; -import BookPick.mvp.domain.Book.entity.Book; +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.preference.entity.UserPreference; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java index 12aa11d..dfb05ce 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.preference.entity; -import BookPick.mvp.domain.Author.entity.Author; -import BookPick.mvp.domain.Book.entity.Book; +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.preference.dto.PreferenceDtos.CreateReq; import BookPick.mvp.domain.preference.dto.PreferenceDtos.UpdateReq; import BookPick.mvp.domain.user.entity.User; diff --git a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java index c1d00d2..44f2325 100644 --- a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java @@ -1,9 +1,9 @@ package BookPick.mvp.domain.preference.service; -import BookPick.mvp.domain.Author.entity.Author; -import BookPick.mvp.domain.Author.repository.AuthorRepository; -import BookPick.mvp.domain.Book.entity.Book; -import BookPick.mvp.domain.Book.repository.BookRepository; +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.author.repository.AuthorRepository; +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.repository.BookRepository; import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; import BookPick.mvp.domain.preference.entity.UserPreference; import BookPick.mvp.domain.preference.repository.PreferenceRepository; diff --git a/src/main/java/BookPick/mvp/domain/user/service/AuthService.java b/src/main/java/BookPick/mvp/domain/user/service/AuthService.java index fba7569..366393f 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/AuthService.java @@ -1,8 +1,8 @@ package BookPick.mvp.domain.user.service; -import BookPick.mvp.domain.user.dto.AuthDtos.*; +import BookPick.mvp.domain.auth.dto.AuthDtos.*; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.DuplicateEmailException; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.global.util.JwtUtil; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index d3ebb86..fef1968 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -1,8 +1,8 @@ package BookPick.mvp.global.exception; import BookPick.mvp.global.ApiResponse; -import BookPick.mvp.domain.user.exception.DuplicateEmailException; -import BookPick.mvp.domain.user.exception.InvalidLoginException; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; +import BookPick.mvp.domain.auth.exception.InvalidLoginException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index ca3fc33..53d34a4 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -1,67 +1,112 @@ package BookPick.mvp.global.util; - import BookPick.mvp.domain.user.entity.User; -import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Component; -import io.jsonwebtoken.*; - import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Date; - +import java.util.Map; @Component public class JwtUtil { + @Value("${jwt.access.secret}") + private String accessSecret; - // 1. 키발급 - static final SecretKey key = - Keys.hmacShaKeyFor(Decoders.BASE64.decode( - "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" - )); + @Value("${jwt.access.expiration}") + private Duration accessExpiration; // 15m -> Duration 으로 자동 변환 - // 2. JWT 생성 - public static String createToken(User user) { + @Value("${jwt.refresh.secret}") + private String refreshSecret; + @Value("${jwt.refresh.expiration}") + private Duration refreshExpiration; // 7d -> Duration 으로 자동 변환 - String jwt = Jwts.builder() - .claim("email", user.getEmail()) // 이메일 - .claim("role", user.getRole()) - .issuedAt(new Date(System.currentTimeMillis())) // 발급 시간 - .expiration(new Date(System.currentTimeMillis() + 1000*60*60)) // 만료 시간 - .signWith(key) // 비밀키 서명 - .compact(); - return jwt; + private static final int CLOCK_SKEW_SECONDS = 60; + + private SecretKey getAccessKey() { + return Keys.hmacShaKeyFor(accessSecret.getBytes(StandardCharsets.UTF_8)); + } + + private SecretKey getRefreshKey() { + return Keys.hmacShaKeyFor(refreshSecret.getBytes(StandardCharsets.UTF_8)); } + /* ====== 토큰 생성 ====== */ - //3. JWT 오픈 - public static Claims extractToken(String token) { - Claims claims = Jwts.parser().verifyWith(key).build() - .parseSignedClaims(token).getPayload(); - return claims; + public String createAccessToken(User user) { + return buildToken(user, getAccessKey(), accessExpiration, "access"); } - // 4. JWT 검증 - public Claims parse(String token) { - try { - return Jwts.parser() - .verifyWith(key) // 서명 검증 - .clockSkewSeconds(60) // 시계 오차 허용(선택) - .build() - .parseSignedClaims(token) - .getPayload(); - } catch (ExpiredJwtException e) { - throw new AuthenticationServiceException("TOKEN_EXPIRED", e); - } catch (JwtException e) { - throw new AuthenticationServiceException("TOKEN_INVALID", e); + public String createRefreshToken(User user) { + return buildToken(user, getRefreshKey(), refreshExpiration, "refresh"); } -} + public Map createTokenPair(User user) { + return Map.of( + "accessToken", createAccessToken(user), + "refreshToken", createRefreshToken(user) + ); + } + private String buildToken(User user, SecretKey key, Duration ttl, String typ) { + long now = System.currentTimeMillis(); + return Jwts.builder() + .subject(user.getEmail()) + .claim("email", user.getEmail()) + .claim("role", user.getRole()) + .claim("typ", typ) + .issuedAt(new Date(now)) + .expiration(new Date(now + ttl.toMillis())) // Duration 을 millis 로 변환 + .signWith(key) + .compact(); + } + /* ====== 파싱/검증 ====== */ + + public Claims parseAccess(String accessToken) { + try { + Claims claims = Jwts.parser() + .verifyWith(getAccessKey()) + .clockSkewSeconds(CLOCK_SKEW_SECONDS) + .build() + .parseSignedClaims(accessToken) + .getPayload(); + + if (!"access".equals(claims.get("typ"))) { + throw new AuthenticationServiceException("TOKEN_TYPE_MISMATCH"); + } + return claims; + } catch (ExpiredJwtException e) { + throw new AuthenticationServiceException("TOKEN_EXPIRED", e); + } catch (JwtException e) { + throw new AuthenticationServiceException("TOKEN_INVALID", e); + } + } -} \ No newline at end of file + public Claims parseRefresh(String refreshToken) { + try { + Claims claims = Jwts.parser() + .verifyWith(getRefreshKey()) + .clockSkewSeconds(CLOCK_SKEW_SECONDS) + .build() + .parseSignedClaims(refreshToken) + .getPayload(); + + if (!"refresh".equals(claims.get("typ"))) { + throw new AuthenticationServiceException("TOKEN_TYPE_MISMATCH"); + } + return claims; + } catch (ExpiredJwtException e) { + throw new AuthenticationServiceException("TOKEN_EXPIRED", e); + } catch (JwtException e) { + throw new AuthenticationServiceException("TOKEN_INVALID", e); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6b2bc66..0468f30 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -16,11 +16,9 @@ logging.level.org.springframework.web=DEBUG logging.level.org.springframework.security=DEBUG logging.level.BookPick=DEBUG -/ openApi ?? ??? -springdoc.api-docs.enabled=true -// ui ??? -springdoc.swagger-ui.enabled=true -// ?? ?? ?? ?? -springdoc.swagger-ui.path=/swagger-ui.html -// ???? ?? ?? ?? ?? -springdoc.override-with-generic-response=false \ No newline at end of file +# JWT ?? +jwt.access.secret=accesssecret123accesssecret123accesssecret123accesssecret123 +jwt.access.expiration=15m + +jwt.refresh.secret=refreshsecret123refreshsecret123refreshsecret123refreshsecret123 +jwt.refresh.expiration=7d From 10665ffea1477d84f423ac3bbe88175ad06815ce Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 28 Sep 2025 03:38:19 +0900 Subject: [PATCH 010/291] =?UTF-8?q?feat=20:=20[=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?]=20access=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/domain/auth/Roles.java | 18 ++ .../auth/controller/AuthController.java | 7 +- .../mvp/domain/auth/dto/AuthDtos.java | 5 +- .../mvp/domain/auth/service/AuthService.java | 89 +++++++++ .../auth/service/MyUserDetailsService.java | 77 ++++++++ .../BookPick/mvp/domain/user/entity/User.java | 4 +- .../user/repository/UserRepository.java | 2 + .../mvp/domain/user/service/AuthService.java | 72 ------- .../BookPick/mvp/global/config/JwtFilter.java | 180 ++++++++++-------- .../mvp/global/config/SecurityConfig.java | 2 + .../BookPick/mvp/global/util/JwtUtil.java | 118 ++++-------- 11 files changed, 333 insertions(+), 241 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/auth/Roles.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/AuthService.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/service/AuthService.java diff --git a/src/main/java/BookPick/mvp/domain/auth/Roles.java b/src/main/java/BookPick/mvp/domain/auth/Roles.java new file mode 100644 index 0000000..7f98e4e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/Roles.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.auth; + +public enum Roles { + ROLE_USER("일반 사용자"), + ROLE_CURATOR("큐레이터"), + ROLE_OWNER("서점 주인"), + ROLE_ADMIN("운영자"); + + private final String description; + + Roles(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index f055e8f..dd5ef9f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -2,7 +2,8 @@ import BookPick.mvp.global.ApiResponse; import BookPick.mvp.domain.auth.dto.AuthDtos.*; -import BookPick.mvp.domain.user.service.AuthService; +import BookPick.mvp.domain.auth.service.AuthService; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -29,9 +30,9 @@ public ResponseEntity> signUp(@Valid @RequestBody SignReq r // 2. 로그인 @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody LoginReq req){ + public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res){ - AuthRes authRes = authService.login(req); + AuthRes authRes = authService.login(req, res); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success("success", authRes)) ; //data 에 DTO 주기 } diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index cc82377..13df7d6 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -37,10 +37,7 @@ public record AuthRes( String nickname, String bio, String profileImageUrl, - String accessToken, - String refreshToken, // 👈 추가 - long expiresIn // 👈 선택: Access 만료(초) - + String access ) {} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java new file mode 100644 index 0000000..f106ae6 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -0,0 +1,89 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.dto.AuthDtos.*; +import BookPick.mvp.domain.auth.exception.InvalidLoginException; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.global.util.JwtUtil; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class AuthService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + + @Transactional + public SignRes signUp(SignReq req) { + // 1. 이메일 중복 확인 + if (userRepository.existsByEmail(req.email())) { + throw new DuplicateEmailException("이미 존재하는 이메일입니다."); + } + + // 2. 신규 유저 생성 + User user = new User(); + user.setEmail(req.email()); + user.setPassword(passwordEncoder.encode(req.password())); + user.setRole(Roles.ROLE_USER); // normal_user, curator + + // 3. DB 저장 + User savedUser = userRepository.save(user); + + // 4. 응답 + return new SignRes(savedUser.getId()); + } + + + + + // access Token 만 전송, refresh x + @Transactional(readOnly = true) + public AuthRes login(LoginReq req, HttpServletResponse res) { + var authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); + try { + var auth = authenticationManagerBuilder.getObject().authenticate(authToken); + + // Access 토큰만 발급 + String access = JwtUtil.createAccessToken(auth); + + var principal = (MyUserDetailsService.CustomUser) auth.getPrincipal(); + + // 선택: 응답 헤더에도 추가해주면 프론트가 꺼내 쓰기 쉬움 + res.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + access); + + return new AuthRes( + principal.getId(), + principal.getUsername(), // email + principal.getNickname(), + principal.getBio(), + principal.getProfileImageUrl(), + access // 프론트가 Authorization: Bearer 로 전송 + ); + } catch (BadCredentialsException | UsernameNotFoundException e) { + throw new InvalidLoginException("아이디 또는 비밀번호가 잘못되었습니다."); + } catch (AuthenticationException e) { + throw new InvalidLoginException("로그인 실패"); + } + } + +} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java new file mode 100644 index 0000000..a0b1d50 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -0,0 +1,77 @@ +package BookPick.mvp.domain.auth.service; + + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import javax.management.relation.Role; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.apache.coyote.http11.Constants.a; + + +// 스프링 시큐리티에서 자동으로 호출 + +@Service +@RequiredArgsConstructor +public class MyUserDetailsService implements UserDetailsService { + private final UserRepository UserRepo; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + var customeUserOpt = UserRepo.findByEmail(email); + List auth= new ArrayList<>(); + if(customeUserOpt.isEmpty()){ + throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email); + } + + + + if (customeUserOpt.get().getRole().equals(Roles.ROLE_USER)) { + auth.add(new SimpleGrantedAuthority(Roles.ROLE_USER.name())); + } + + var customUser=new CustomUser(customeUserOpt.get(), auth); //username = email, passowrd, authorities 등록 + customUser.setId(customeUserOpt.get().getId()); + customUser.setNickname(customeUserOpt.get().getNickname()); + customUser.setBio(customeUserOpt.get().getBio()); + customUser.setProfileImageUrl(customeUserOpt.get().getProfileImageUrl()); + + return customUser; + + } + + @Getter + @Setter + public static class CustomUser extends User{ + private Long id; + private String nickname; + private String bio; + private String profileImageUrl; + + public CustomUser( + BookPick.mvp.domain.user.entity.User user, + Collection authorities + ){ + super(user.getEmail(), user.getPassword(),authorities); + this.bio = user.getBio(); + this.profileImageUrl=user.getProfileImageUrl(); + } + + } + + +} + diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index 8529d31..b22d25d 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.user.entity; +import BookPick.mvp.domain.auth.Roles; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @@ -28,8 +29,9 @@ public class User { @Column(length = 50) private String nickname; // 프로필 닉네임 + @Enumerated(EnumType.STRING) @Column(length = 20) - private String role; // ROLE_USER, ROLE_ADMIN 등 + private Roles role; // ROLE_USER, ROLE_ADMIN 등 @Column(length = 255) private String bio; // 자기소개 문구 diff --git a/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java b/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java index 2659174..2dd292a 100644 --- a/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java +++ b/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java @@ -10,5 +10,7 @@ public interface UserRepository extends JpaRepository { boolean existsByEmail(String email); Optional findByEmail(String email); + + Object findFirstByEmail(String email); } diff --git a/src/main/java/BookPick/mvp/domain/user/service/AuthService.java b/src/main/java/BookPick/mvp/domain/user/service/AuthService.java deleted file mode 100644 index 366393f..0000000 --- a/src/main/java/BookPick/mvp/domain/user/service/AuthService.java +++ /dev/null @@ -1,72 +0,0 @@ -package BookPick.mvp.domain.user.service; - -import BookPick.mvp.domain.auth.dto.AuthDtos.*; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.auth.exception.DuplicateEmailException; -import BookPick.mvp.domain.user.repository.UserRepository; -import BookPick.mvp.global.util.JwtUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@Transactional -@RequiredArgsConstructor -public class AuthService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtUtil jwtUtil; - - - @Transactional - public SignRes signUp(SignReq req) { - // 1. 이메일 중복 확인 - if (userRepository.existsByEmail(req.email())) { - throw new DuplicateEmailException("이미 존재하는 이메일입니다."); - } - - // 2. 신규 유저 생성 - User user = new User(); - user.setEmail(req.email()); - user.setPassword(passwordEncoder.encode(req.password())); - user.setRole("normal_user"); // normal_user, curator - - // 3. DB 저장 - User savedUser = userRepository.save(user); - - // 4. 응답 - return new SignRes(savedUser.getId()); - } - - - - public AuthRes login(LoginReq req){ - - // 1. 이메일 검증 - User user = userRepository.findByEmail(req.email()) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다.")); - - // 2. 비밀번호 검증 - if (!passwordEncoder.matches(req.password(), user.getPassword())) { - throw new IllegalArgumentException("잘못된 이메일 혹은 비밀번호입니다."); - } - - // 3. JWT 토큰 발급 (❌ 비밀번호 넣지 말고) - String accessToken = jwtUtil.createToken(user); - - // refreshToken 발급도 필요하다면 JwtUtil에서 따로 만들기 -> MVP에선 미구현 - - // 4. AuthRes 응답 - return new AuthRes( - user.getId(), - user.getEmail(), - user.getNickname(), - user.getBio(), - user.getProfileImageUrl(), - accessToken - ); - } -} diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index a0809ed..9b0a0cc 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,90 +1,120 @@ package BookPick.mvp.global.config; +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.ApiResponse; -import BookPick.mvp.global.util.JwtUtil; // JWT 파싱/검증 유틸 +import BookPick.mvp.global.util.JwtUtil; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.Claims; // JWT Payload(클레임) 타입 -import jakarta.servlet.FilterChain; // 필터 체인 (다음 필터 호출용) -import jakarta.servlet.ServletException; // 서블릿 예외 -import jakarta.servlet.http.HttpServletRequest; // HTTP 요청 -import jakarta.servlet.http.HttpServletResponse; // HTTP 응답 -import lombok.RequiredArgsConstructor; // 생성자 자동 생성 -import org.springframework.security.authentication.AuthenticationServiceException; // 인증 관련 예외 -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; // 인증 객체 구현체 -import org.springframework.security.core.authority.SimpleGrantedAuthority; // 권한 객체 -import org.springframework.security.core.context.SecurityContextHolder; // 인증 정보를 담는 컨텍스트 -import org.springframework.stereotype.Component; // 스프링 빈 등록 -import org.springframework.web.filter.OncePerRequestFilter; // 요청마다 한 번 실행되는 필터 - -import java.io.IOException; // IO 예외 -import java.util.List; // 권한 리스트 - -/** - * JWT 필터 (MVP 버전, 리프레시 없음) - * - Authorization 헤더에서 토큰 추출 - * - JwtUtil.parse(token)으로 만료/서명 검증 - * - 성공하면 SecurityContext에 인증 정보 세팅 - * - 실패하면 401 Unauthorized 반환 - */ -@Component // 스프링 컴포넌트 스캔에 의해 빈 등록 -@RequiredArgsConstructor // final 필드 생성자 자동 주입 +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Component +@RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { - private final JwtUtil jwtUtil; // JwtUtil 주입받음 - - private static final String AUTH_HEADER = "Authorization"; // 토큰이 담기는 헤더 이름 - private static final String BEARER = "Bearer "; // 토큰 앞에 붙는 접두어 - @Override - protected void doFilterInternal(HttpServletRequest request, // 들어온 요청 - HttpServletResponse response, // 응답 - FilterChain chain) // 필터 체인 : 필터들을 관리하는 객체 , chain.dofilter -> 다음필터 실행 - throws ServletException, IOException { - - // 1. 요청 헤더에서 Authorization 값 꺼냄 - String header = request.getHeader(AUTH_HEADER); + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain + ) throws ServletException, IOException { - // 2. 헤더가 없거나 Bearer로 시작하지 않으면 → 그냥 다음 필터로 넘김 - if (header == null || !header.startsWith(BEARER)) { - chain.doFilter(request, response); // 다음 필터로 넘기는 코드 + Cookie jwtCookie = findCookie(request, "jwt"); + if (jwtCookie == null || isAuthenticatedAlready()) { + filterChain.doFilter(request, response); return; } - // 3. "Bearer " 부분 제거하고 실제 토큰만 추출 - String token = header.substring(BEARER.length()).trim(); - try { - // 4. JwtUtil로 토큰 검증 (만료·서명 체크 포함) - Claims claims = jwtUtil.parse(token); - - // 5. 클레임에서 사용자 정보 꺼내기 - String email = claims.get("email", String.class); - String role = claims.get("role", String.class); - - // 6. 권한 객체 생성 (ROLE_ 접두어 보정) - SimpleGrantedAuthority authority = - (role != null && role.startsWith("ROLE_")) - ? new SimpleGrantedAuthority(role) - : new SimpleGrantedAuthority("ROLE_" + role); - - // 7. 인증 객체 생성 (principal: email, credentials: null, authorities: 권한) - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(email, null, List.of(authority)); - - // 8. SecurityContext에 인증 객체 저장 - SecurityContextHolder.getContext().setAuthentication(authentication); - - // 9. 다음 필터로 진행 - chain.doFilter(request, response); - - } catch (AuthenticationServiceException ex) { - // 10. 토큰이 만료되었거나 유효하지 않음 - SecurityContextHolder.clearContext(); // 기존 인증 정보 제거 - ApiResponse body = new ApiResponse<>(401, ex.getMessage(), null); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 반환 - response.setContentType("application/json;charset=UTF-8"); // JSON 응답 - new ObjectMapper().writeValue(response.getWriter(), body); - return; // 컨트롤러로 요청 안 넘어감 + // 1) 파싱/검증 + Claims claim = JwtUtil.extractToken(jwtCookie.getValue()); + + // 2) 인증 세팅 + String[] arr = claim.get("authorities").toString().split(","); + var authorities = Arrays.stream(arr).map(SimpleGrantedAuthority::new).toList(); + + String email = String.valueOf(claim.get("email")); + + AuthPrincipal principal = new AuthPrincipal(email, authorities); + + var authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + + filterChain.doFilter(request, response); + } catch (ExpiredJwtException e) { + // 만료 토큰: 쿠키 제거 + clearJwtCookie(response); + // API 경로면 401, 뷰/정적은 계속 통과 + if (isApi(request)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + filterChain.doFilter(request, response); + } catch (JwtException | IllegalArgumentException e) { + // 서명 불일치/손상 등: 쿠키 제거 후 동일 처리 + clearJwtCookie(response); + if (isApi(request)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + filterChain.doFilter(request, response); + } + } + + private boolean isAuthenticatedAlready() { + return SecurityContextHolder.getContext().getAuthentication() != null; + } + + private boolean isApi(HttpServletRequest req) { + String uri = req.getRequestURI(); + return uri != null && uri.startsWith("/api/"); + } + + private Cookie findCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) return null; + for (Cookie c : cookies) if (name.equals(c.getName())) return c; + return null; + } + + private void clearJwtCookie(HttpServletResponse res) { + Cookie c = new Cookie("jwt", null); + c.setPath("/"); + c.setMaxAge(0); + c.setHttpOnly(true); + res.addCookie(c); + } + + public static class AuthPrincipal extends User { + private Long id; + private String email; + + public AuthPrincipal( + String email, + Collection authorities + ){ + super(email, "",authorities); } + } + } diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 374766a..4880da6 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -40,9 +40,11 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/logout").permitAll() .requestMatchers("/api/users/*/preferences").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() + .requestMatchers("/error").permitAll() // 나머지 모든 요청은 인증된 사용자만 접근 가능 .anyRequest().authenticated() ) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // 위 설정으로 SecurityFilterChain 객체 생성 diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 53d34a4..946a1d2 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -1,112 +1,58 @@ package BookPick.mvp.global.util; +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.domain.user.entity.User; import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.Date; -import java.util.Map; +import java.util.stream.Collectors; -@Component -public class JwtUtil { - @Value("${jwt.access.secret}") - private String accessSecret; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; - @Value("${jwt.access.expiration}") - private Duration accessExpiration; // 15m -> Duration 으로 자동 변환 - @Value("${jwt.refresh.secret}") - private String refreshSecret; - - @Value("${jwt.refresh.expiration}") - private Duration refreshExpiration; // 7d -> Duration 으로 자동 변환 +@Component +public class JwtUtil { + // 1. 키발급 + static final SecretKey key = + Keys.hmacShaKeyFor(Decoders.BASE64.decode( + "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" + )); - private static final int CLOCK_SKEW_SECONDS = 60; - private SecretKey getAccessKey() { - return Keys.hmacShaKeyFor(accessSecret.getBytes(StandardCharsets.UTF_8)); - } + // 2. JWT 생성 + public static String createAccessToken(Authentication auth) { + CustomUser usr = (CustomUser) auth.getPrincipal(); - private SecretKey getRefreshKey() { - return Keys.hmacShaKeyFor(refreshSecret.getBytes(StandardCharsets.UTF_8)); - } + String authorities = auth.getAuthorities().stream() //getAuthorities -> List return + .map(a->a.getAuthority()) // getAuthority() -> String return + .collect(Collectors.joining(",")); - /* ====== 토큰 생성 ====== */ - public String createAccessToken(User user) { - return buildToken(user, getAccessKey(), accessExpiration, "access"); - } - public String createRefreshToken(User user) { - return buildToken(user, getRefreshKey(), refreshExpiration, "refresh"); - } - - public Map createTokenPair(User user) { - return Map.of( - "accessToken", createAccessToken(user), - "refreshToken", createRefreshToken(user) - ); - } - - private String buildToken(User user, SecretKey key, Duration ttl, String typ) { - long now = System.currentTimeMillis(); - return Jwts.builder() - .subject(user.getEmail()) - .claim("email", user.getEmail()) - .claim("role", user.getRole()) - .claim("typ", typ) - .issuedAt(new Date(now)) - .expiration(new Date(now + ttl.toMillis())) // Duration 을 millis 로 변환 + String jwt = Jwts.builder() + .claim("email", usr.getUsername()) + .claim("authorities", authorities) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + 1000*60*60)) // expiration : 만료 .signWith(key) .compact(); + return jwt; } - /* ====== 파싱/검증 ====== */ - - public Claims parseAccess(String accessToken) { - try { - Claims claims = Jwts.parser() - .verifyWith(getAccessKey()) - .clockSkewSeconds(CLOCK_SKEW_SECONDS) - .build() - .parseSignedClaims(accessToken) - .getPayload(); - - if (!"access".equals(claims.get("typ"))) { - throw new AuthenticationServiceException("TOKEN_TYPE_MISMATCH"); - } - return claims; - } catch (ExpiredJwtException e) { - throw new AuthenticationServiceException("TOKEN_EXPIRED", e); - } catch (JwtException e) { - throw new AuthenticationServiceException("TOKEN_INVALID", e); - } - } - public Claims parseRefresh(String refreshToken) { - try { - Claims claims = Jwts.parser() - .verifyWith(getRefreshKey()) - .clockSkewSeconds(CLOCK_SKEW_SECONDS) - .build() - .parseSignedClaims(refreshToken) - .getPayload(); - - if (!"refresh".equals(claims.get("typ"))) { - throw new AuthenticationServiceException("TOKEN_TYPE_MISMATCH"); - } - return claims; - } catch (ExpiredJwtException e) { - throw new AuthenticationServiceException("TOKEN_EXPIRED", e); - } catch (JwtException e) { - throw new AuthenticationServiceException("TOKEN_INVALID", e); - } + + //3. JWT 오픈 + public static Claims extractToken(String token) { + Claims claims = Jwts.parser().verifyWith(key).build() + .parseSignedClaims(token).getPayload(); + return claims; } } + From 1e1185a2c75bb573f9370cd7f0779bc531b05994 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 29 Sep 2025 20:40:40 +0900 Subject: [PATCH 011/291] =?UTF-8?q?fix=20:=20login=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=ED=8F=AC=EB=A9=A7=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refresh 토큰 추가 --- .DS_Store | Bin 6148 -> 6148 bytes build.gradle | 39 +++++----- gradlew 17-57-10-142 => gradlew | 0 .../auth/controller/AuthController.java | 12 +--- .../mvp/domain/auth/dto/AuthDtos.java | 9 ++- .../mvp/domain/auth/service/AuthService.java | 7 +- .../mvp/domain/author/entity/Author.java | 23 ------ .../author/repository/AuthorRepository.java | 10 --- .../BookPick/mvp/domain/book/entity/Book.java | 32 --------- .../book/repository/BookRepository.java | 10 --- .../domain/preference/dto/PreferenceDtos.java | 11 +-- .../preference/entity/UserPreference.java | 24 ++----- .../preference/service/PreferenceService.java | 21 +----- .../BookPick/mvp/domain/user/entity/User.java | 3 + .../BookPick/mvp/global/config/JwtFilter.java | 68 ++++++++++-------- .../mvp/global/config/SecurityConfig.java | 29 ++++++++ .../BookPick/mvp/global/config/WebConfig.java | 18 +++++ .../BookPick/mvp/global/util/JwtUtil.java | 24 ++++++- 18 files changed, 158 insertions(+), 182 deletions(-) rename gradlew 17-57-10-142 => gradlew (100%) delete mode 100644 src/main/java/BookPick/mvp/domain/author/entity/Author.java delete mode 100644 src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java delete mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Book.java delete mode 100644 src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java create mode 100644 src/main/java/BookPick/mvp/global/config/WebConfig.java diff --git a/.DS_Store b/.DS_Store index 3b1833de48bcbb35cb3523e2831377fcb4ee2efc..0075cff9f05a81aa22d0b73fc06f370f1e9a8125 100644 GIT binary patch delta 67 zcmZoMXfc=|#>B)qu~2NHo+2a5#(>?7j4YFRSUzw5$STIPvEdWbW_AvK4xp0Ff*jwO VC-aLqaxee^BLf4=<_M8B%m8-B5Y+$x delta 323 zcmb7=ze>YU6vltKx3r-ax`fX905ypyf}7%~(nWDKQEJsTF^LwZE`0z?AHmf{GWs-q zgMx#rUxJgH%i;TTzH`sH^I#sl_DrL_c!;91+*(lnXJRG0$}z{~> login(@Valid @RequestBody LoginReq r .body(ApiResponse.success("success", authRes)) ; //data 에 DTO 주기 } - // 3. 로그아웃 - // Note : 서베에서는 별도 로직 x, 클라이언트가 토큰 지움 - @PostMapping("/logout") - public ResponseEntity> logout(){ - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success("success", null)); - } } diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index 13df7d6..8a0177f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -23,6 +23,7 @@ public record SignRes( } + // 2. 로그인 //Req public record LoginReq( @@ -38,7 +39,13 @@ public record AuthRes( String bio, String profileImageUrl, String access -) {} +) { + public AuthRes(long userId){ + this(userId,null,null,null,null,null); + } + } + + AuthRes authRes = new AuthRes(1); diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index f106ae6..4327d01 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -8,6 +8,7 @@ import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.global.util.JwtUtil; import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -56,6 +57,7 @@ public SignRes signUp(SignReq req) { + // access Token 만 전송, refresh x @Transactional(readOnly = true) public AuthRes login(LoginReq req, HttpServletResponse res) { @@ -65,6 +67,7 @@ public AuthRes login(LoginReq req, HttpServletResponse res) { // Access 토큰만 발급 String access = JwtUtil.createAccessToken(auth); + String refresh = JwtUtil.createRefreshToken(auth); var principal = (MyUserDetailsService.CustomUser) auth.getPrincipal(); @@ -77,7 +80,7 @@ public AuthRes login(LoginReq req, HttpServletResponse res) { principal.getNickname(), principal.getBio(), principal.getProfileImageUrl(), - access // 프론트가 Authorization: Bearer 로 전송 + refresh // 프론트가 Authorization: Bearer 로 전송 ); } catch (BadCredentialsException | UsernameNotFoundException e) { throw new InvalidLoginException("아이디 또는 비밀번호가 잘못되었습니다."); @@ -86,4 +89,6 @@ public AuthRes login(LoginReq req, HttpServletResponse res) { } } + + } diff --git a/src/main/java/BookPick/mvp/domain/author/entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java deleted file mode 100644 index 20e50a8..0000000 --- a/src/main/java/BookPick/mvp/domain/author/entity/Author.java +++ /dev/null @@ -1,23 +0,0 @@ -package BookPick.mvp.domain.author.entity; - - -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -public class Author { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 50) - private String name; - - @Column(length = 200) - private String bio; // 간단한 소개 -} diff --git a/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java deleted file mode 100644 index b1dfb99..0000000 --- a/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package BookPick.mvp.domain.author.repository; - -import BookPick.mvp.domain.author.entity.Author; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface AuthorRepository extends JpaRepository { - Optional findByName(String name); -} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java deleted file mode 100644 index 7c3833f..0000000 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ /dev/null @@ -1,32 +0,0 @@ -package BookPick.mvp.domain.book.entity; - - -import BookPick.mvp.domain.author.entity.Author; -import jakarta.persistence.*; -import lombok.*; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -public class Book { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false, length = 100) - private String title; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "author_id", nullable = false) - private Author author; // 책:작가 = N:1 - - @Column(length = 200) - private String description; - - @Column(length = 50) - private String genre; -} - diff --git a/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java deleted file mode 100644 index 7202360..0000000 --- a/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package BookPick.mvp.domain.book.repository; - -import BookPick.mvp.domain.book.entity.Book; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface BookRepository extends JpaRepository { - Optional findByTitle(String title); -} diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java index adc33b4..7217d1c 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java @@ -1,7 +1,6 @@ package BookPick.mvp.domain.preference.dto; -import BookPick.mvp.domain.author.entity.Author; -import BookPick.mvp.domain.book.entity.Book; + import BookPick.mvp.domain.preference.entity.UserPreference; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -55,12 +54,8 @@ public static PreferenceRes from(UserPreference p) { return new PreferenceRes( p.getId(), p.getMbti(), - p.getFavoriteAuthors().stream() - .map(Author::getName) // Author → String - .toList(), - p.getFavoriteBooks().stream() - .map(Book::getTitle) // Book → String - .toList(), + p.getFavoriteAuthors(), + p.getFavoriteBooks(), p.getSelectionCriteria(), p.getReadingHabits(), p.getPreferredGenres(), diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java index dfb05ce..9aa8a81 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java @@ -1,7 +1,5 @@ package BookPick.mvp.domain.preference.entity; -import BookPick.mvp.domain.author.entity.Author; -import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.preference.dto.PreferenceDtos.CreateReq; import BookPick.mvp.domain.preference.dto.PreferenceDtos.UpdateReq; import BookPick.mvp.domain.user.entity.User; @@ -32,22 +30,12 @@ public class UserPreference { private String mbti; - @ManyToMany - @JoinTable( - name = "preference_authors", - joinColumns = @JoinColumn(name = "preference_id"), - inverseJoinColumns = @JoinColumn(name = "author_id") - ) - private List favoriteAuthors; - - - @ManyToMany - @JoinTable( - name = "preference_books", - joinColumns = @JoinColumn(name = "preference_id"), - inverseJoinColumns = @JoinColumn(name = "book_id") - ) - private List favoriteBooks; + + private List favoriteAuthors; + + + + private List favoriteBooks; @ElementCollection @CollectionTable(name = "preference_selection_criteria", joinColumns = @JoinColumn(name = "preference_id")) diff --git a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java index 44f2325..15b4b74 100644 --- a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java @@ -1,9 +1,5 @@ package BookPick.mvp.domain.preference.service; -import BookPick.mvp.domain.author.entity.Author; -import BookPick.mvp.domain.author.repository.AuthorRepository; -import BookPick.mvp.domain.book.entity.Book; -import BookPick.mvp.domain.book.repository.BookRepository; import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; import BookPick.mvp.domain.preference.entity.UserPreference; import BookPick.mvp.domain.preference.repository.PreferenceRepository; @@ -18,8 +14,6 @@ @RequiredArgsConstructor public class PreferenceService { private final PreferenceRepository preferenceRepository; - private final AuthorRepository authorRepository; - private final BookRepository bookRepository; @Transactional public PreferenceRes create(Long userId, CreateReq req) { @@ -31,19 +25,8 @@ public PreferenceRes create(Long userId, CreateReq req) { } // --- 문자열 요청 → 엔티티 변환 --- - List authors = req.favoriteAuthors().stream() - .map(name -> authorRepository.findByName((name)) - .orElseGet(() -> authorRepository.save( - Author.builder().name(name).build() - )) - ).toList(); - - List books = req.favoriteBooks().stream() - .map(title -> bookRepository.findByTitle((title)) - .orElseGet(() -> bookRepository.save( - Book.builder().title(title).build() - )) - ).toList(); + List authors = req.favoriteAuthors(); // ["1", "2", "3"] + List books = req.favoriteBooks(); // ["1", "2", "3"] // --- UserPreference 생성 --- UserPreference pref = UserPreference.from(req, user); diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index b22d25d..798bd80 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -46,4 +46,7 @@ public class User { @UpdateTimestamp @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; // 수정 시각 + + + } diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 9b0a0cc..24d9242 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -13,6 +13,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -32,52 +33,49 @@ @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { + private static final String BEARER = "Bearer"; + + + + @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { - Cookie jwtCookie = findCookie(request, "jwt"); - if (jwtCookie == null || isAuthenticatedAlready()) { + + // 이미 인증된 상태면 패스 + if (SecurityContextHolder.getContext().getAuthentication() != null) { filterChain.doFilter(request, response); return; } - try { - // 1) 파싱/검증 - Claims claim = JwtUtil.extractToken(jwtCookie.getValue()); + String token = resolveAccessToken(request); + if (token == null) { // 토큰 없으면 그냥 통과 + filterChain.doFilter(request, response); + return; + } - // 2) 인증 세팅 - String[] arr = claim.get("authorities").toString().split(","); - var authorities = Arrays.stream(arr).map(SimpleGrantedAuthority::new).toList(); + try { + Claims claims = JwtUtil.extractToken(token); - String email = String.valueOf(claim.get("email")); + var authorities = Arrays.stream( + claims.get("authorities").toString().split(",") + ).map(SimpleGrantedAuthority::new).toList(); - AuthPrincipal principal = new AuthPrincipal(email, authorities); + var auth = new UsernamePasswordAuthenticationToken( + claims.get("email"), null, authorities + ); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - var authToken = new UsernamePasswordAuthenticationToken(principal, null, authorities); - authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authToken); + SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - } catch (ExpiredJwtException e) { - // 만료 토큰: 쿠키 제거 - clearJwtCookie(response); - // API 경로면 401, 뷰/정적은 계속 통과 - if (isApi(request)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - filterChain.doFilter(request, response); - } catch (JwtException | IllegalArgumentException e) { - // 서명 불일치/손상 등: 쿠키 제거 후 동일 처리 - clearJwtCookie(response); - if (isApi(request)) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - filterChain.doFilter(request, response); + } catch (ExpiredJwtException e) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; } + + filterChain.doFilter(request, response); } private boolean isAuthenticatedAlready() { @@ -117,4 +115,12 @@ public AuthPrincipal( } + private String resolveAccessToken(HttpServletRequest request) { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header != null && header.startsWith(BEARER)) { + return header.substring(BEARER.length()).trim(); + } + return null; + } + } diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 4880da6..39cafa5 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -10,6 +10,11 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @EnableWebSecurity @@ -50,4 +55,28 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 위 설정으로 SecurityFilterChain 객체 생성 .build(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + + // allowCredentials(true) 를 쓰면 "*" 와일드카드는 안 됩니다. 꼭 구체적으로! + config.setAllowedOrigins(List.of( + "http://localhost:5173" + )); + // 터널 도메인이 자주 바뀌면 패턴 허용도 가능 (Boot 3+) + config.setAllowedOriginPatterns(List.of( + "https://*.trycloudflare.com" + )); + + config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS")); + config.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin")); + config.setAllowCredentials(true); + // 필요 시 preflight 캐시 시간(초) + // config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } } diff --git a/src/main/java/BookPick/mvp/global/config/WebConfig.java b/src/main/java/BookPick/mvp/global/config/WebConfig.java new file mode 100644 index 0000000..e968b1e --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/WebConfig.java @@ -0,0 +1,18 @@ +package BookPick.mvp.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOrigins("http://localhost:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } +} + diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 946a1d2..01d37a7 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -5,6 +5,8 @@ import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -25,6 +27,9 @@ public class JwtUtil { "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" )); + // (추가) 토큰 수명 상수 + private static final long ACCESS_TTL_MS = 1000L * 60 * 60; // 1시간 + private static final long REFRESH_TTL_MS = 1000L * 60 * 60 * 24 * 14; // 14일 // 2. JWT 생성 public static String createAccessToken(Authentication auth) { @@ -46,6 +51,19 @@ public static String createAccessToken(Authentication auth) { return jwt; } + // ✅ 2-1. Refresh 토큰 생성 (여기 추가) + public static String createRefreshToken(Authentication auth) { + CustomUser usr = (CustomUser) auth.getPrincipal(); + + // refresh 토큰에는 최소 정보만: subject/email + typ 정도만 권장 + return Jwts.builder() + .claim("email", usr.getUsername()) + .claim("typ", "refresh") // (권장) 토큰 타입 명시 + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + REFRESH_TTL_MS)) + .signWith(key) + .compact(); + } //3. JWT 오픈 @@ -54,5 +72,9 @@ public static Claims extractToken(String token) { .parseSignedClaims(token).getPayload(); return claims; } -} + + + } + + From 226ae10cb1311443113dd9d5747c3d2f97223494 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:20:25 +0900 Subject: [PATCH 012/291] =?UTF-8?q?chore=20:=20=20gitignore=20=EA=B8=B0?= =?UTF-8?q?=ED=83=80=20=EC=82=AC=ED=95=AD=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc..cb1d8f2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,84 @@ out/ ### VS Code ### .vscode/ + +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +# AWS related files +aws/ + + + From 31f024f8775c8d4912bbb9717bfeec0ce8d2886c Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:21:46 +0900 Subject: [PATCH 013/291] =?UTF-8?q?feat(global)=20:=20API=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=ED=95=84=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/global/api/ApiResponse.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/BookPick/mvp/global/api/ApiResponse.java diff --git a/src/main/java/BookPick/mvp/global/api/ApiResponse.java b/src/main/java/BookPick/mvp/global/api/ApiResponse.java new file mode 100644 index 0000000..f0d2d80 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/api/ApiResponse.java @@ -0,0 +1,37 @@ +package BookPick.mvp.global.api; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class ApiResponse { + private String code; // ex. DUPLICATE_EMIAL (개발용 친화) + private String message; // ex. 이미 존재하는 이메일입니다. (사람용 친화) + private T data; + + + // -- Success -- + public static ApiResponse success(SuccessCode successCode, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 + return new ApiResponse(successCode.getCode(), successCode.getMessage(), data); + } + + + // -- Client Error -- + public static ApiResponse clientError(ErrorCode errorCode) { + return new ApiResponse( + errorCode.getCode(), + errorCode.getMessage(), // @Valid 같은 데서 넘어온 메시지 + null + ); + } + + // -- Server Error -- + public static ApiResponse serverError(ErrorCode errorCode) { + return new ApiResponse( + errorCode.getCode(), + errorCode.getMessage(), + null); + } +} From aa2c728fa355bfc7f981f2a7e932c4557c09c7c2 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:23:32 +0900 Subject: [PATCH 014/291] =?UTF-8?q?feat(auth)=20:=20api=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EA=B4=80=EB=A6=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/AuthController.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 24822fb..35f2ecc 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -1,8 +1,9 @@ package BookPick.mvp.domain.auth.controller; -import BookPick.mvp.global.ApiResponse; +import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.domain.auth.dto.AuthDtos.*; import BookPick.mvp.domain.auth.service.AuthService; +import BookPick.mvp.global.api.SuccessCode; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -11,7 +12,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/auth") +@RequestMapping("/api/v1/auth") @RequiredArgsConstructor public class AuthController { @@ -19,10 +20,10 @@ public class AuthController { // 1. 회원가입 @PostMapping("/signup") public ResponseEntity> signUp(@Valid @RequestBody SignReq req) { - SignRes res = authService.signUp(req); - System.out.println(res); + + return ResponseEntity.status(HttpStatus.CREATED) // 201 - .body(ApiResponse.created("create",res)); // created 사용 + .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS,authService.signUp(req))); // code, message, DTO 반환 } // 2. 로그인 @@ -31,9 +32,13 @@ public ResponseEntity> login(@Valid @RequestBody LoginReq r AuthRes authRes = authService.login(req, res); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success("success", authRes)) ; //data 에 DTO 주기 + .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, authRes)) ; //data 에 DTO 주기 } } + +// 제목 +// 내용 +// \ No newline at end of file From 1cdd7b5ce2ebed2d342136f3ae74e7985f35587b Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:26:40 +0900 Subject: [PATCH 015/291] =?UTF-8?q?chore(global)=20:=20docker=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6168db6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM eclipse-temurin:17-jdk-alpine + +ARG JAR_FILE=build/libs/*.jar +COPY ${JAR_FILE} app.jar +ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file From 76d7dad576c6f16a114ff0256a6a4f754009ad1c Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:27:55 +0900 Subject: [PATCH 016/291] =?UTF-8?q?feat(auth)=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/dto/AuthDtos.java | 2 +- .../mvp/domain/auth/service/AuthService.java | 14 ++--- .../auth/service/MyUserDetailsService.java | 2 +- .../BookPick/mvp/domain/user/entity/User.java | 8 +++ .../user/repository/UserRepository.java | 2 + .../BookPick/mvp/global/api/ErrorCode.java | 27 +++++++++ .../BookPick/mvp/global/api/SuccessCode.java | 40 +++++++++++++ .../BookPick/mvp/global/config/JwtFilter.java | 45 --------------- .../mvp/global/config/SecurityConfig.java | 57 +++++++++++-------- .../BookPick/mvp/global/util/JwtUtil.java | 5 +- 10 files changed, 120 insertions(+), 82 deletions(-) create mode 100644 src/main/java/BookPick/mvp/global/api/ErrorCode.java create mode 100644 src/main/java/BookPick/mvp/global/api/SuccessCode.java diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index 8a0177f..f774d42 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -38,7 +38,7 @@ public record AuthRes( String nickname, String bio, String profileImageUrl, - String access + String accessToken ) { public AuthRes(long userId){ this(userId,null,null,null,null,null); diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index 4327d01..7dd2182 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -4,11 +4,10 @@ import BookPick.mvp.domain.auth.dto.AuthDtos.*; import BookPick.mvp.domain.auth.exception.InvalidLoginException; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.auth.exception.DuplicateEmailException; import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.custom.DuplicateResourceException; import BookPick.mvp.global.util.JwtUtil; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,7 +16,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -36,9 +34,9 @@ public class AuthService { @Transactional public SignRes signUp(SignReq req) { - // 1. 이메일 중복 확인 - if (userRepository.existsByEmail(req.email())) { - throw new DuplicateEmailException("이미 존재하는 이메일입니다."); + // 1. 중복 확인 + if (userRepository.existsAllByEmail((req.email()))) { + throw new DuplicateResourceException(ErrorCode.DUPLICATE_EMAIL); } // 2. 신규 유저 생성 @@ -80,7 +78,7 @@ public AuthRes login(LoginReq req, HttpServletResponse res) { principal.getNickname(), principal.getBio(), principal.getProfileImageUrl(), - refresh // 프론트가 Authorization: Bearer 로 전송 + "Bearer " + access ); } catch (BadCredentialsException | UsernameNotFoundException e) { throw new InvalidLoginException("아이디 또는 비밀번호가 잘못되었습니다."); diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index a0b1d50..98ef7b3 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -43,7 +43,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep auth.add(new SimpleGrantedAuthority(Roles.ROLE_USER.name())); } - var customUser=new CustomUser(customeUserOpt.get(), auth); //username = email, passowrd, authorities 등록 + var customUser = new CustomUser(customeUserOpt.get(), auth); //username = email, passowrd, authorities 등록 customUser.setId(customeUserOpt.get().getId()); customUser.setNickname(customeUserOpt.get().getNickname()); customUser.setBio(customeUserOpt.get().getBio()); diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index 798bd80..38b71ef 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -2,6 +2,9 @@ import BookPick.mvp.domain.auth.Roles; import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; @@ -15,18 +18,22 @@ @Setter public class User { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "userId") private Long id; // 내부 식별자 (PK) @Column(name = "login_email", nullable = false, unique = true, length = 255) + @Email(message = "올바른 이메일 형식이여야 합니다.") private String email; // 로그인 ID, 고유 + @Column(name = "login_password", nullable = false, length = 255) private String password; // 비밀번호 해시 @Column(length = 50) + @Size(min = 2, max = 10, message = "닉네임은 2~10자여야 합니다.") private String nickname; // 프로필 닉네임 @Enumerated(EnumType.STRING) @@ -34,6 +41,7 @@ public class User { private Roles role; // ROLE_USER, ROLE_ADMIN 등 @Column(length = 255) + @Size(message = "자기소개는 255자 이하여야 합니다.") private String bio; // 자기소개 문구 @Column(name = "profileImageUrl", length = 500) diff --git a/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java b/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java index 2dd292a..978fafb 100644 --- a/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java +++ b/src/main/java/BookPick/mvp/domain/user/repository/UserRepository.java @@ -12,5 +12,7 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); Object findFirstByEmail(String email); + + boolean existsAllByEmail(String email); } diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java new file mode 100644 index 0000000..b6ee90f --- /dev/null +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -0,0 +1,27 @@ +package BookPick.mvp.global.api; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum ErrorCode { + + + // -- Auth -- + INVALID_REQUEST(400, "INVALID_REQUEST", "잘못된 요청입니다."), // 400 + AUTHENTICATION_FAILED(401, "AUTHENTICATION_FAILED", "아이디 또는 비밀번호가 올바르지 않습니다."), // 401 로그인 + DUPLICATE_EMAIL(409, "DUPLICATE_EMAIL", "중복된 이메일 입니다."), // 409 회원가입 + + // -- User -- + + // -- Post -- + POST_NOT_FOUND(404, "POST_NOT_FOUND", "해당 게시글을 찾을 수 없습니다."); + + + private final int status; + private final String code; + private final String message; + + +} diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java new file mode 100644 index 0000000..3e4cca7 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -0,0 +1,40 @@ +package BookPick.mvp.global.api; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum SuccessCode { + + // -- Auth -- + REGISTER_SUCCESS(HttpStatus.CREATED, "REGISTER_SUCCESS", "회원가입을 성공하였습니다."), + LOGIN_SUCCESS(HttpStatus.OK, "LOGIN_SUCCESS", "로그인에 성공했습니다"), + + // -- User -- + + // -- Post -- + POST_CREATE_SUCCESS(HttpStatus.CREATED, "POST_CREATE_SUCCESS", "게시글을 성공적으로 등록하였습니다."), + POST_READ_SUCCESS(HttpStatus.OK, "POST_READ_SUCCESS", "게시글 조회를 성공하였습니다."), + POST_UPDATE_SUCCESS(HttpStatus.OK, "POST_UPDATE_SUCCESS", "게시글을 성공적으로 수정하였습니다."), + POST_DELETE_SUCCESS(HttpStatus.OK, "POST_DELETE_SUCCESS", "게시글을 성공적으로 삭제하였습니다."), + + // -- Pre + PREFERENCE_CREATED(HttpStatus.CREATED, "PREFERENCE_CREATED", "사용자의 독서 취향 정보를 성공적으로 등록하였습니다."), + PREFERENCE_READ_SUCCESS(HttpStatus.OK, "PREFERENCE_READ_SUCCESS", "사용자의 독서 취향 정보를 성공적으로 조회하였습니다."), + PREFERENCE_UPDATED(HttpStatus.OK, "PREFERENCE_UPDATED", "사용자의 독서 취향 정보를 성공적으로 수정하였습니다."), + PREFERENCE_DELETED(HttpStatus.OK, "PREFERENCE_DELETED", "사용자의 독서 취향 정보를 성공적으로 삭제하였습니다."); + + + + + private final HttpStatus status; // 상태 코드 번호 + private final String code; // 상태 설명 (개발용) + private final String message; // 상태 설명 (사용자 친화적) + + + + +} diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 24d9242..5523722 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,33 +1,23 @@ package BookPick.mvp.global.config; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; -import BookPick.mvp.global.ApiResponse; import BookPick.mvp.global.util.JwtUtil; -import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; -import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.User; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.Arrays; -import java.util.Collection; -import java.util.List; @Component @RequiredArgsConstructor @@ -78,42 +68,7 @@ protected void doFilterInternal( filterChain.doFilter(request, response); } - private boolean isAuthenticatedAlready() { - return SecurityContextHolder.getContext().getAuthentication() != null; - } - - private boolean isApi(HttpServletRequest req) { - String uri = req.getRequestURI(); - return uri != null && uri.startsWith("/api/"); - } - - private Cookie findCookie(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) return null; - for (Cookie c : cookies) if (name.equals(c.getName())) return c; - return null; - } - private void clearJwtCookie(HttpServletResponse res) { - Cookie c = new Cookie("jwt", null); - c.setPath("/"); - c.setMaxAge(0); - c.setHttpOnly(true); - res.addCookie(c); - } - - public static class AuthPrincipal extends User { - private Long id; - private String email; - - public AuthPrincipal( - String email, - Collection authorities - ){ - super(email, "",authorities); - } - - } private String resolveAccessToken(HttpServletRequest request) { String header = request.getHeader(HttpHeaders.AUTHORIZATION); diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 39cafa5..a60e5a3 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -31,30 +32,40 @@ PasswordEncoder passwordEncoder() { @Bean // 이 메서드가 반환하는 객체(SecurityFilterChain)를 스프링 빈으로 등록 SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http - // CSRF(Cross Site Request Forgery, 사이트 간 위조 요청) 방어 기능 비활성화 - // REST API 서버에서는 세션을 사용하지 않으므로 보통 꺼둡니다. - .csrf(csrf -> csrf.disable()) + .cors(Customizer.withDefaults()) + // CSRF(Cross Site Request Forgery, 사이트 간 위조 요청) 방어 기능 비활성화 + // REST API 서버에서는 세션을 사용하지 않으므로 보통 꺼둡니다. + .csrf(csrf -> csrf.disable()) - // 세션 관리 정책 설정 - // STATELESS = 서버가 세션을 생성하거나 사용하지 않음 (JWT 기반 인증 전제 조건) - .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 세션 관리 정책 설정 + // STATELESS = 서버가 세션을 생성하거나 사용하지 않음 (JWT 기반 인증 전제 조건) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - // URL 별 접근 권한 규칙 정의 - .authorizeHttpRequests(auth -> auth - // 회원가입, 로그인 요청은 인증 없이 누구나 접근 가능 - .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/logout").permitAll() - .requestMatchers("/api/users/*/preferences").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() - .requestMatchers("/error").permitAll() - // 나머지 모든 요청은 인증된 사용자만 접근 가능 - .anyRequest().authenticated() - ) + // URL 별 접근 권한 규칙 정의 + .authorizeHttpRequests(auth -> auth + // 회원가입, 로그인 요청은 인증 없이 누구나 접근 가능 + .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/logout").permitAll() + .requestMatchers("/api/users/*/preferences").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() + .requestMatchers("/error").permitAll() + // 나머지 모든 요청은 인증된 사용자만 접근 가능 + .anyRequest().authenticated() + ) - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + // ✅ Security에 '로그아웃 URL'을 명시해 CORS/필터 체인 타도록 + .logout(logout -> logout + .logoutUrl("/api/auth/logout") // 프론트가 POST로 호출 + .clearAuthentication(true) + .invalidateHttpSession(true) + .logoutSuccessHandler((req, res, auth) -> { + res.setStatus(org.springframework.http.HttpStatus.OK.value()); // 200 OK + })) - // 위 설정으로 SecurityFilterChain 객체 생성 - .build(); - } + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + + // 위 설정으로 SecurityFilterChain 객체 생성 + .build(); + } @Bean public CorsConfigurationSource corsConfigurationSource() { @@ -65,9 +76,9 @@ public CorsConfigurationSource corsConfigurationSource() { "http://localhost:5173" )); // 터널 도메인이 자주 바뀌면 패턴 허용도 가능 (Boot 3+) - config.setAllowedOriginPatterns(List.of( - "https://*.trycloudflare.com" - )); +// config.setAllowedOriginPatterns(List.of( +// "https://*.trycloudflare.com" +// )); config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS")); config.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin")); diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 01d37a7..2ff703c 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -1,12 +1,8 @@ package BookPick.mvp.global.util; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; -import BookPick.mvp.domain.user.entity.User; -import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import jakarta.servlet.http.HttpServletRequest; -import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -42,6 +38,7 @@ public static String createAccessToken(Authentication auth) { String jwt = Jwts.builder() + .claim("userId", usr.getId()) .claim("email", usr.getUsername()) .claim("authorities", authorities) .issuedAt(new Date(System.currentTimeMillis())) From 55176893ab52174c89ecea8d1adc9cb8c665cdd3 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:28:28 +0900 Subject: [PATCH 017/291] =?UTF-8?q?feat(Error)=20:=20=EA=B8=80=EB=A1=9C?= =?UTF-8?q?=EB=B2=8C=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=9F=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=BB=A4=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/DuplicateResourceException.java | 18 +++++ .../global/exception/BusinessException.java | 14 ++++ .../exception/DuplicateResourceException.java | 17 +++++ .../exception/GlobalExceptionHandler.java | 74 +++++++++---------- .../custom/DuplicateResourceException.java | 17 +++++ 5 files changed, 103 insertions(+), 37 deletions(-) create mode 100644 src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java create mode 100644 src/main/java/BookPick/mvp/global/exception/BusinessException.java create mode 100644 src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java create mode 100644 src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java diff --git a/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java new file mode 100644 index 0000000..69a6e78 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java @@ -0,0 +1,18 @@ +package BookPick.mvp.global.api; + + +import BookPick.mvp.global.exception.BusinessException; +import lombok.Getter; +import lombok.Setter; +import BookPick.mvp.global.api.ErrorCode.*; + + +@Getter +@Setter +public class DuplicateResourceException extends BusinessException { + ErrorCode errorCode; + + public DuplicateResourceException(ErrorCode errorCode){ + super(ErrorCode.DUPLICATE_EMAIL); // Throwable의 detailMessage에 message 저장 + } +} diff --git a/src/main/java/BookPick/mvp/global/exception/BusinessException.java b/src/main/java/BookPick/mvp/global/exception/BusinessException.java new file mode 100644 index 0000000..56e9ecb --- /dev/null +++ b/src/main/java/BookPick/mvp/global/exception/BusinessException.java @@ -0,0 +1,14 @@ +package BookPick.mvp.global.exception; + +import BookPick.mvp.global.api.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException{ + ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode){ + super(errorCode.getMessage()); // 나중에 로그 확인을 위해 런타임 예외 디테일 message 에 저장 + this.errorCode=errorCode; + } +} diff --git a/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java new file mode 100644 index 0000000..6a1cb57 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java @@ -0,0 +1,17 @@ +package BookPick.mvp.global.exception; + + +import BookPick.mvp.global.api.ErrorCode; +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +public class DuplicateResourceException extends BusinessException{ + ErrorCode errorCode; + + public DuplicateResourceException(ErrorCode errorCode){ + super(ErrorCode.DUPLICATE_EMAIL); // Throwable의 detailMessage에 message 저장 + } +} diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index fef1968..1319fb1 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -1,9 +1,8 @@ package BookPick.mvp.global.exception; -import BookPick.mvp.global.ApiResponse; -import BookPick.mvp.domain.auth.exception.DuplicateEmailException; -import BookPick.mvp.domain.auth.exception.InvalidLoginException; -import org.springframework.dao.DataIntegrityViolationException; + +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.ErrorCode; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -13,45 +12,46 @@ @RestControllerAdvice public class GlobalExceptionHandler { - // @Valid 검증 실패 → 400 - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { - return ResponseEntity.badRequest() - .body(ApiResponse.error(400, "invalid_request")); - } - // 잘못된 인자 (기타 비즈니스 로직에서 발생) → 400 - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) { - return ResponseEntity.badRequest() - .body(ApiResponse.error(400, "invalid_request")); - } - // 로그인 실패 → 401 - @ExceptionHandler(InvalidLoginException.class) - public ResponseEntity> handleInvalidLogin(InvalidLoginException ex) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(ApiResponse.error(401, "invalid_credentials")); + // -- BusinessException -- + @ExceptionHandler(BusinessException.class) + public ResponseEntity> handleBusinessException(BusinessException e){ + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(ApiResponse.clientError(e.getErrorCode())); } - // 이메일 중복 (직접 던진 경우) → 409 - @ExceptionHandler(DuplicateEmailException.class) - public ResponseEntity> handleDuplicateEmail(DuplicateEmailException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(ApiResponse.error(409, "duplicate_email")); - } - // DB Unique 제약 등 무결성 위반 → 409 - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity> handleUniqueViolation(DataIntegrityViolationException ex) { - return ResponseEntity.status(HttpStatus.CONFLICT) - .body(ApiResponse.error(409, "duplicate_email")); + + + // -- Common -- + // 400 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { + // 여러 필드 중 첫 번째 에러만 가져오기 (필요하면 전체 리스트로 가공 가능) + String errorMessage = e.getBindingResult() + .getFieldErrors() + .stream() + .findFirst() + .map(fieldError -> fieldError.getDefaultMessage()) // DTO의 @Email(message="...") 내용 + .orElse("잘못된 요청입니다."); + + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) // 400 + .body(ApiResponse.clientError(ErrorCode.INVALID_REQUEST)); } - // 그 외 모든 예외 → 500 - @ExceptionHandler(Exception.class) - public ResponseEntity> handle500(Exception ex) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(500, "server_error")); + + //409 + @ExceptionHandler(DuplicateResourceException.class) + public ResponseEntity> hanlderDuplicateResource(DuplicateResourceException e){ + return ResponseEntity + .status(HttpStatus.CONFLICT) // 409 + .body(ApiResponse.clientError(ErrorCode.DUPLICATE_EMAIL)); // code : DUPLICATE_EMAIL, message : 이미 존재하는 이메일입니다 } + + + + } diff --git a/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java new file mode 100644 index 0000000..6df2516 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java @@ -0,0 +1,17 @@ +package BookPick.mvp.global.exception.custom; + + +import BookPick.mvp.global.api.ErrorCode; +import lombok.Getter; +import lombok.Setter; + + +@Getter +@Setter +public class DuplicateResourceException extends RuntimeException{ + ErrorCode errorCode; + + public DuplicateResourceException(ErrorCode errorCode){ + super(errorCode.getMessage()); // Throwable의 detailMessage에 message 저장 + } +} From eed87fb488ddcc5bc194403cc72b0b59742a45e5 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:28:44 +0900 Subject: [PATCH 018/291] chore: meaningless commit --- build.gradle | 42 +++++++++++------- .../controller/PreferenceController.java | 11 +++-- .../java/BookPick/mvp/global/ApiResponse.java | 37 ---------------- src/main/resources/application.properties | 24 ----------- src/main/resources/application.yml | 43 +++++++++++++++++++ 5 files changed, 75 insertions(+), 82 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/global/ApiResponse.java delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/build.gradle b/build.gradle index d2fe623..7d32541 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.5.5' - id 'io.spring.dependency-management' version '1.1.7' + id 'java' + id 'org.springframework.boot' version '3.5.5' + id 'io.spring.dependency-management' version '1.1.7' } group = 'BookPick' @@ -9,49 +9,61 @@ version = '0.0.1-SNAPSHOT' description = 'Book-Pick Project MVP version' java { - toolchain { - languageVersion = JavaLanguageVersion.of(17) - } + toolchain { + languageVersion = JavaLanguageVersion.of(17) // Boot 3.5.x는 17+ OK + } } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } + // 필요 시 예전 springfox 끼어드는 것 전역 차단 (주석 해제해서 사용) + // all { + // exclude group: 'io.springfox' + // } } repositories { - mavenCentral() + mavenCentral() } dependencies { + // Spring Boot starters implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + // Thymeleaf + Spring Security 6 integration implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' - // ✅ Boot 3.x / Spring 6.x와 호환되는 springdoc 2.6.x + // ✅ Spring Framework 6.2.x / Boot 3.5.x 호환 springdoc implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.11' + // (WebFlux면 위를 webflux-ui로 교체) - // JWT + // (선택) 개발 편의 - 자동 재시작/리로드 + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // JWT (JJWT 0.12.x) implementation 'io.jsonwebtoken:jjwt-api:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5' // Gson 사용 중. Jackson 쓰려면 jjwt-jackson로 교체 + // DB runtimeOnly 'com.mysql:mysql-connector-j' + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } - tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java index baed872..41eefb8 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java @@ -1,8 +1,9 @@ package BookPick.mvp.domain.preference.controller; -import BookPick.mvp.global.ApiResponse; +import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; import BookPick.mvp.domain.preference.service.PreferenceService; +import BookPick.mvp.global.api.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -15,17 +16,15 @@ @RequestMapping("/api/users/{id}/preferences") @RequiredArgsConstructor public class PreferenceController { + private final PreferenceService preferenceService; @PostMapping - public ResponseEntity> create( - @PathVariable("id") Long userId, - @Valid @RequestBody CreateReq req - ) { + public ResponseEntity> create(@PathVariable("id") Long userId, @Valid @RequestBody CreateReq req) { PreferenceRes res = preferenceService.create(userId, req); URI location = URI.create("/api/users/" + userId + "/preferences"); return ResponseEntity.created(location) - .body(ApiResponse.success("success", res)); + .body(ApiResponse.success(SuccessCode.PREFERENCE_CREATED, res)); } } diff --git a/src/main/java/BookPick/mvp/global/ApiResponse.java b/src/main/java/BookPick/mvp/global/ApiResponse.java deleted file mode 100644 index 41d1c4c..0000000 --- a/src/main/java/BookPick/mvp/global/ApiResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package BookPick.mvp.global; - -import lombok.Getter; - -// 응답 포멧 -// 이 포멧 안에 data 부분에 각각의 DTO - -@Getter -public class ApiResponse { - - private final int status; - private final String message; - private final T data; //DTO - - public ApiResponse(int status, String message, T data) { - this.status = status; - this.message = message; - this.data = data; - } - - // 정적 팩토리 메서드 - public static ApiResponse success(String msg, T data) { - return new ApiResponse<>( 200, msg, data) ; - } - - public static ApiResponse created(String msg, T data) { - return new ApiResponse<>(201, msg, data); - } - - public static ApiResponse noContent() { //헤더만 주는놈 - return new ApiResponse<>(204, null, null); - } - - public static ApiResponse error(int status, String message) { - return new ApiResponse<>(status, message,null); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 0468f30..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,24 +0,0 @@ -spring.application.name=BookPick -spring.datasource.url=jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick -spring.datasource.username=nan7789 -spring.datasource.password=gustjq3735! -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - -jwt.secret=yourSuperSecretKeyHere1234567890 -jwt.access.exp=3600000 - -spring.jpa.hibernate.ddl-auto=update -spring.jpa.properties.hibernate.show_sql=False - -# ?? ?? ?? -logging.level.root=INFO -logging.level.org.springframework.web=DEBUG -logging.level.org.springframework.security=DEBUG -logging.level.BookPick=DEBUG - -# JWT ?? -jwt.access.secret=accesssecret123accesssecret123accesssecret123accesssecret123 -jwt.access.expiration=15m - -jwt.refresh.secret=refreshsecret123refreshsecret123refreshsecret123refreshsecret123 -jwt.refresh.expiration=7d diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..96f1c54 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,43 @@ +spring: + application: + name: BookPick + + datasource: + url: jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick + username: nan7789 + password: gustjq3735! + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + show_sql: false + +server: + port: 8081 + error: + include-message: always + include-binding-errors: always + include-stacktrace: always + include-exception: true + + +logging: + level: + root: INFO + org.springframework.web: DEBUG + org.springframework.security: DEBUG + BookPick: DEBUG + +jwt: + secret: yourSuperSecretKeyHere1234567890 + access: + secret: accesssecret123accesssecret123accesssecret123accesssecret123 + expiration: 15m + refresh: + secret: refreshsecret123refreshsecret123refreshsecret123refreshsecret123 + expiration: 7d + + From 0f5d1ca65ce8b05247adc542521bb44c75705816 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 20:31:47 +0900 Subject: [PATCH 019/291] =?UTF-8?q?chore=20:=20.DS=5FStore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cb1d8f2..0a0b8cd 100644 --- a/.gitignore +++ b/.gitignore @@ -114,5 +114,6 @@ out/ # AWS related files aws/ - +# macOS system files +.DS_Store From 2d2620c279dd5cf92b7e21bb2d089cc3358aac12 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 22:19:01 +0900 Subject: [PATCH 020/291] =?UTF-8?q?fix(cors)=20:=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=EC=9C=BC=EB=A1=9C=20CORS=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/global/config/SecurityConfig.java | 60 +++++++------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index a60e5a3..580349a 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -21,70 +21,52 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { + private final JwtFilter jwtFilter; + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - private final JwtFilter jwtFilter; - @Bean // 이 메서드가 반환하는 객체(SecurityFilterChain)를 스프링 빈으로 등록 + @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http - .cors(Customizer.withDefaults()) - // CSRF(Cross Site Request Forgery, 사이트 간 위조 요청) 방어 기능 비활성화 - // REST API 서버에서는 세션을 사용하지 않으므로 보통 꺼둡니다. + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - - // 세션 관리 정책 설정 - // STATELESS = 서버가 세션을 생성하거나 사용하지 않음 (JWT 기반 인증 전제 조건) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - - // URL 별 접근 권한 규칙 정의 .authorizeHttpRequests(auth -> auth - // 회원가입, 로그인 요청은 인증 없이 누구나 접근 가능 - .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/logout").permitAll() - .requestMatchers("/api/users/*/preferences").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() - .requestMatchers("/error").permitAll() - // 나머지 모든 요청은 인증된 사용자만 접근 가능 - .anyRequest().authenticated() + .requestMatchers("/api/v1/auth/signup", "/api/v1/auth/login", "/api/v1/auth/logout").permitAll() + .requestMatchers("/api/v1/users/*/preferences").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() + .requestMatchers("/error").permitAll() + .anyRequest().authenticated() ) - - // ✅ Security에 '로그아웃 URL'을 명시해 CORS/필터 체인 타도록 .logout(logout -> logout - .logoutUrl("/api/auth/logout") // 프론트가 POST로 호출 - .clearAuthentication(true) - .invalidateHttpSession(true) - .logoutSuccessHandler((req, res, auth) -> { - res.setStatus(org.springframework.http.HttpStatus.OK.value()); // 200 OK - })) - + .logoutUrl("/api/v1/auth/logout") + .clearAuthentication(true) + .invalidateHttpSession(true) + .logoutSuccessHandler((req, res, auth) -> { + res.setStatus(org.springframework.http.HttpStatus.OK.value()); // 200 OK + })) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - - // 위 설정으로 SecurityFilterChain 객체 생성 .build(); - } + } + @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - // allowCredentials(true) 를 쓰면 "*" 와일드카드는 안 됩니다. 꼭 구체적으로! config.setAllowedOrigins(List.of( - "http://localhost:5173" + "http://localhost:5173", + "https://bookpick-front.vercel.app" )); - // 터널 도메인이 자주 바뀌면 패턴 허용도 가능 (Boot 3+) -// config.setAllowedOriginPatterns(List.of( -// "https://*.trycloudflare.com" -// )); - config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS")); - config.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin")); config.setAllowCredentials(true); - // 필요 시 preflight 캐시 시간(초) - // config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); From 55df2f776687066f843bebfb6e056cb899411cc7 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 22:23:14 +0900 Subject: [PATCH 021/291] =?UTF-8?q?chore(gitIgnore)=20:=20.DS=5FStore=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=EC=BA=90=EC=8B=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 0075cff9f05a81aa22d0b73fc06f370f1e9a8125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5T0#oe<(r^3OxqA7Obrb#Y>3w1&ruHr6#7-V9b^#wTDv3SzpK}@p+ut z-HNgGD*nvC>^D0*lQ3VF-3$N-XBzAQGytHAN?54jutsQ|bVUlrQ$iH#84(QqSu*w` z>90hy<0mpe->wWJn1BZXV)>+r6u9SJ2Ih#bp}bQ_xUX hF~-tW+(cD^ev1r5+hC> Date: Tue, 14 Oct 2025 22:20:39 +0900 Subject: [PATCH 022/291] =?UTF-8?q?fix(cors)=20:=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=94=EB=93=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=EC=9C=BC=EB=A1=9C=20CORS=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/global/config/SecurityConfig.java | 57 +++++++------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index a60e5a3..3fea4fc 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -29,62 +29,43 @@ PasswordEncoder passwordEncoder() { private final JwtFilter jwtFilter; - @Bean // 이 메서드가 반환하는 객체(SecurityFilterChain)를 스프링 빈으로 등록 + @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http - .cors(Customizer.withDefaults()) - // CSRF(Cross Site Request Forgery, 사이트 간 위조 요청) 방어 기능 비활성화 - // REST API 서버에서는 세션을 사용하지 않으므로 보통 꺼둡니다. + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - - // 세션 관리 정책 설정 - // STATELESS = 서버가 세션을 생성하거나 사용하지 않음 (JWT 기반 인증 전제 조건) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - - // URL 별 접근 권한 규칙 정의 .authorizeHttpRequests(auth -> auth - // 회원가입, 로그인 요청은 인증 없이 누구나 접근 가능 - .requestMatchers("/api/auth/signup", "/api/auth/login", "/api/auth/logout").permitAll() - .requestMatchers("/api/users/*/preferences").permitAll() - .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() - .requestMatchers("/error").permitAll() - // 나머지 모든 요청은 인증된 사용자만 접근 가능 - .anyRequest().authenticated() + .requestMatchers("/api/v1/auth/signup", "/api/v1/auth/login", "/api/v1/auth/logout").permitAll() + .requestMatchers("/api/v1/users/*/preferences").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() + .requestMatchers("/error").permitAll() + .anyRequest().authenticated() ) - - // ✅ Security에 '로그아웃 URL'을 명시해 CORS/필터 체인 타도록 .logout(logout -> logout - .logoutUrl("/api/auth/logout") // 프론트가 POST로 호출 - .clearAuthentication(true) - .invalidateHttpSession(true) - .logoutSuccessHandler((req, res, auth) -> { - res.setStatus(org.springframework.http.HttpStatus.OK.value()); // 200 OK - })) - + .logoutUrl("/api/v1/auth/logout") + .clearAuthentication(true) + .invalidateHttpSession(true) + .logoutSuccessHandler((req, res, auth) -> { + res.setStatus(org.springframework.http.HttpStatus.OK.value()); // 200 OK + })) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - - // 위 설정으로 SecurityFilterChain 객체 생성 .build(); - } + } + @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - // allowCredentials(true) 를 쓰면 "*" 와일드카드는 안 됩니다. 꼭 구체적으로! config.setAllowedOrigins(List.of( - "http://localhost:5173" + "http://localhost:5173", + "https://bookpick-front.vercel.app" )); - // 터널 도메인이 자주 바뀌면 패턴 허용도 가능 (Boot 3+) -// config.setAllowedOriginPatterns(List.of( -// "https://*.trycloudflare.com" -// )); - config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS")); - config.setAllowedHeaders(List.of("Authorization","Content-Type","X-Requested-With","Accept","Origin")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin")); config.setAllowCredentials(true); - // 필요 시 preflight 캐시 시간(초) - // config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); From 0c60a5b20a7919b87dc679a3d87cca54dcf2b5c9 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 23:37:40 +0900 Subject: [PATCH 023/291] =?UTF-8?q?feat(ApiResponse)=20:=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=9E=98=ED=8D=BC=20=ED=98=95=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (code, message, data) -> (status, message, data) --- .../BookPick/mvp/global/api/ApiResponse.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/api/ApiResponse.java b/src/main/java/BookPick/mvp/global/api/ApiResponse.java index f0d2d80..4a58784 100644 --- a/src/main/java/BookPick/mvp/global/api/ApiResponse.java +++ b/src/main/java/BookPick/mvp/global/api/ApiResponse.java @@ -1,37 +1,43 @@ package BookPick.mvp.global.api; + +import lombok.*; +import org.springframework.http.HttpStatus; + + import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; + @Getter @Setter @AllArgsConstructor public class ApiResponse { - private String code; // ex. DUPLICATE_EMIAL (개발용 친화) + private int status; // ex. DUPLICATE_EMIAL (개발용 친화) private String message; // ex. 이미 존재하는 이메일입니다. (사람용 친화) private T data; // -- Success -- - public static ApiResponse success(SuccessCode successCode, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 - return new ApiResponse(successCode.getCode(), successCode.getMessage(), data); + public static ApiResponse success(SuccessCode successMessage, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 + return new ApiResponse(successMessage.getStatus().value(), successMessage.getMessage(), data); } - - // -- Client Error -- - public static ApiResponse clientError(ErrorCode errorCode) { + // -- Error -- + public static ApiResponse error(ErrorCode errorCode) { return new ApiResponse( - errorCode.getCode(), + errorCode.getStatus().value(), errorCode.getMessage(), // @Valid 같은 데서 넘어온 메시지 null ); } - // -- Server Error -- - public static ApiResponse serverError(ErrorCode errorCode) { + public static ApiResponse customError(HttpStatus httpStatus, String message, T data) { return new ApiResponse( - errorCode.getCode(), - errorCode.getMessage(), - null); + httpStatus.value(), + message, // @Valid 같은 데서 넘어온 메시지 + data + ); } } + From dda4e549a2c4469f54f6153600824578b0fd60df Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 23:39:08 +0900 Subject: [PATCH 024/291] =?UTF-8?q?feat(Error=20Handler)=20:=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EB=B0=8F=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=EC=97=90=EB=9F=AC=20=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/DuplicateEmailException.java | 9 +++-- .../auth/exception/InvalidLoginException.java | 13 ++++---- .../controller/PreferenceController.java | 2 +- .../exception/GlobalExceptionHandler.java | 33 ++++++++----------- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java b/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java index 2a5f21e..70ec89c 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java @@ -1,7 +1,10 @@ package BookPick.mvp.domain.auth.exception; -public class DuplicateEmailException extends RuntimeException { - public DuplicateEmailException(String message) { - super(message); +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class DuplicateEmailException extends BusinessException { + public DuplicateEmailException() { + super(ErrorCode.DUPLICATE_EMAIL); } } diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java index 3db3b95..8ab2eed 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java @@ -1,11 +1,12 @@ package BookPick.mvp.domain.auth.exception; -public class InvalidLoginException extends RuntimeException { - public InvalidLoginException() { - super("잘못된 로그인 시도입니다."); - } +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; - public InvalidLoginException(String message) { - super(message); + +// 401 +public class InvalidLoginException extends BusinessException { + public InvalidLoginException() { + super(ErrorCode.AUTHENTICATION_FAILED); } } diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java index 41eefb8..c0e9fc4 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java @@ -25,6 +25,6 @@ public ResponseEntity> create(@PathVariable("id") Lon URI location = URI.create("/api/users/" + userId + "/preferences"); return ResponseEntity.created(location) - .body(ApiResponse.success(SuccessCode.PREFERENCE_CREATED, res)); + .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); } } diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index 1319fb1..3bef20a 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -12,24 +12,19 @@ @RestControllerAdvice public class GlobalExceptionHandler { - - - // -- BusinessException -- @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusinessException(BusinessException e){ + public ResponseEntity> handleBusinessException(BusinessException e) { + ErrorCode errorCode = e.getErrorCode(); + return ResponseEntity - .status(e.getErrorCode().getStatus()) - .body(ApiResponse.clientError(e.getErrorCode())); + .status(errorCode.getStatus()) + .body(ApiResponse.error(errorCode)); } - - - // -- Common -- - // 400 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { - // 여러 필드 중 첫 번째 에러만 가져오기 (필요하면 전체 리스트로 가공 가능) + String errorMessage = e.getBindingResult() .getFieldErrors() .stream() @@ -38,20 +33,18 @@ public ResponseEntity> handleValidationException(MethodArgumen .orElse("잘못된 요청입니다."); return ResponseEntity - .status(HttpStatus.BAD_REQUEST) // 400 - .body(ApiResponse.clientError(ErrorCode.INVALID_REQUEST)); + .status(ErrorCode.INVALID_REQUEST.getStatus()) // 400 + .body(ApiResponse.customError(ErrorCode.INVALID_REQUEST.getStatus(), errorMessage, null)); } - //409 @ExceptionHandler(DuplicateResourceException.class) - public ResponseEntity> hanlderDuplicateResource(DuplicateResourceException e){ - return ResponseEntity - .status(HttpStatus.CONFLICT) // 409 - .body(ApiResponse.clientError(ErrorCode.DUPLICATE_EMAIL)); // code : DUPLICATE_EMAIL, message : 이미 존재하는 이메일입니다 + public ResponseEntity> handleDuplicateResource(DuplicateResourceException e) { + ErrorCode errorCode = ErrorCode.DUPLICATE_EMAIL; + return ResponseEntity + .status(errorCode.getStatus()) // 409 + .body(ApiResponse.error(errorCode)); // code : DUPLICATE_EMAIL, message : 이미 존재하는 이메일입니다 } - - } From 5d575407f72aad464433c88e8a635dec152b5076 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 23:39:36 +0900 Subject: [PATCH 025/291] =?UTF-8?q?feat(Response=20Code)=20:=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=BD=94=EB=93=9C=20=ED=98=95=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/global/api/ErrorCode.java | 22 +++++++----- .../BookPick/mvp/global/api/SuccessCode.java | 36 ++++++++++--------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java index b6ee90f..32b2988 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -2,26 +2,30 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.http.HttpStatus; @AllArgsConstructor @Getter public enum ErrorCode { - - // -- Auth -- - INVALID_REQUEST(400, "INVALID_REQUEST", "잘못된 요청입니다."), // 400 - AUTHENTICATION_FAILED(401, "AUTHENTICATION_FAILED", "아이디 또는 비밀번호가 올바르지 않습니다."), // 401 로그인 - DUPLICATE_EMAIL(409, "DUPLICATE_EMAIL", "중복된 이메일 입니다."), // 409 회원가입 + // -- Auth + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), // 400 회원 가입 + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "중복된 이메일 입니다."), // 409 회원 가입 + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘 못 되었습니다."), // 401 로그인 // -- User -- + User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 + // -- Post -- - POST_NOT_FOUND(404, "POST_NOT_FOUND", "해당 게시글을 찾을 수 없습니다."); + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), //404 - private final int status; - private final String code; - private final String message; + // -- Comment -- + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글 찾을 수 없습니다."); //404 + private final HttpStatus status; + private final String message; } + diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index 3e4cca7..abda168 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -5,36 +5,40 @@ import lombok.Getter; import org.springframework.http.HttpStatus; + @AllArgsConstructor @Getter public enum SuccessCode { // -- Auth -- - REGISTER_SUCCESS(HttpStatus.CREATED, "REGISTER_SUCCESS", "회원가입을 성공하였습니다."), - LOGIN_SUCCESS(HttpStatus.OK, "LOGIN_SUCCESS", "로그인에 성공했습니다"), + REGISTER_SUCCESS(HttpStatus.OK, "회원가입을 성공하였습니다."), + LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다"), // -- User -- + GET_USERS_SUCCESS(HttpStatus.OK, "사용자 목록 조회를 성공하였습니다."), - // -- Post -- - POST_CREATE_SUCCESS(HttpStatus.CREATED, "POST_CREATE_SUCCESS", "게시글을 성공적으로 등록하였습니다."), - POST_READ_SUCCESS(HttpStatus.OK, "POST_READ_SUCCESS", "게시글 조회를 성공하였습니다."), - POST_UPDATE_SUCCESS(HttpStatus.OK, "POST_UPDATE_SUCCESS", "게시글을 성공적으로 수정하였습니다."), - POST_DELETE_SUCCESS(HttpStatus.OK, "POST_DELETE_SUCCESS", "게시글을 성공적으로 삭제하였습니다."), - - // -- Pre - PREFERENCE_CREATED(HttpStatus.CREATED, "PREFERENCE_CREATED", "사용자의 독서 취향 정보를 성공적으로 등록하였습니다."), - PREFERENCE_READ_SUCCESS(HttpStatus.OK, "PREFERENCE_READ_SUCCESS", "사용자의 독서 취향 정보를 성공적으로 조회하였습니다."), - PREFERENCE_UPDATED(HttpStatus.OK, "PREFERENCE_UPDATED", "사용자의 독서 취향 정보를 성공적으로 수정하였습니다."), - PREFERENCE_DELETED(HttpStatus.OK, "PREFERENCE_DELETED", "사용자의 독서 취향 정보를 성공적으로 삭제하였습니다."); + // -- Quration -- + CURATION_CREATE_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."), + CURATION_LIST_READ_SUCCESS(HttpStatus.OK, "큐레이션 목록을 성공적으로 조회하였습니다."), + CURATION_DETAIL_READ_SUCCESS(HttpStatus.OK, "큐레이션을 성공하였습니다."), + CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."), + CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."), + // -- Comment -- + COMMENT_CREATE_SUCCESS(HttpStatus.CREATED, "댓글을 성공적으로 등록하였습니다."), + COMMENT_LIST_READ_SUCCESS(HttpStatus.OK, "댓글 목록을 성공적으로 조회하였습니다."), + COMMENT_LIST_EMPTY(HttpStatus.OK, "댓글이 없습니다."), + COMMENT_READ_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 조회하였습니다."), + COMMENT_UPDATE_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 수정하였습니다."), + COMMENT_DELETE_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 삭제하였습니다."); - private final HttpStatus status; // 상태 코드 번호 - private final String code; // 상태 설명 (개발용) + private final HttpStatus status; private final String message; // 상태 설명 (사용자 친화적) +} + -} From 2b6b30c2a1ee698219390d39b6f407c82ebfa73a Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 23:43:34 +0900 Subject: [PATCH 026/291] =?UTF-8?q?fix(exception)=20:=20DuplicateResourceE?= =?UTF-8?q?xception=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/custom/DuplicateResourceException.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java index 6df2516..1210dae 100644 --- a/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java +++ b/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java @@ -11,7 +11,7 @@ public class DuplicateResourceException extends RuntimeException{ ErrorCode errorCode; - public DuplicateResourceException(ErrorCode errorCode){ - super(errorCode.getMessage()); // Throwable의 detailMessage에 message 저장 + public DuplicateResourceException(){ + super(ErrorCode.DUPLICATE_EMAIL.getMessage()); } } From b863a04e785bc0ad71370020654301bfdb727e56 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 14 Oct 2025 23:57:08 +0900 Subject: [PATCH 027/291] =?UTF-8?q?feat(auth)=20:=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 10 ++-- .../mvp/domain/auth/dto/AuthDtos.java | 56 ++++++++----------- .../mvp/domain/auth/service/AuthService.java | 27 +++++---- .../BookPick/mvp/domain/user/entity/User.java | 7 ++- 4 files changed, 44 insertions(+), 56 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 35f2ecc..6c3a3be 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -17,16 +17,16 @@ public class AuthController { private final AuthService authService; - // 1. 회원가입 + @PostMapping("/signup") public ResponseEntity> signUp(@Valid @RequestBody SignReq req) { + SignRes signRes = authService.signUp(req); - - return ResponseEntity.status(HttpStatus.CREATED) // 201 - .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS,authService.signUp(req))); // code, message, DTO 반환 + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS, signRes)); } - // 2. 로그인 + @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res){ diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index f774d42..c08457b 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -8,57 +8,45 @@ public class AuthDtos { - - // 1. 회원가입 - //Req - public record SignReq( + // -- SignUp -- + public record SignReq( @NotBlank @Email String email, - @Size(min=8, max = 72 ) String password - ){ } - - //res + @Size(min = 8, max = 72) String password + ) {} public record SignRes( long userId - ){ + ) { + public static SignRes from(long userId) { + return new SignRes(userId); + } } - - // 2. 로그인 - //Req + // -- Login -- public record LoginReq( @NotBlank @Email String email, - @Size(min=8, max = 72 ) String password - ){} - - //Res + @Size(min = 8, max = 72) String password + ) {} public record AuthRes( - long userId, - String email, - String nickname, - String bio, - String profileImageUrl, - String accessToken -) { - public AuthRes(long userId){ - this(userId,null,null,null,null,null); - } + long userId, + String email, + String nickname, + String bio, + String profileImageUrl, + String accessToken + ) { } - AuthRes authRes = new AuthRes(1); - - - // 3. 로그아 public record LogoutReq( - @NotBlank String refreshToken - ){ + @NotBlank String refreshToken + ) { } // 3. 토큰 재발급 요청 public record RefreshToken( - @NotBlank String refreshToken - ){ + @NotBlank String refreshToken + ) { } } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index 7dd2182..f5c66d1 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -2,6 +2,7 @@ import BookPick.mvp.domain.auth.Roles; import BookPick.mvp.domain.auth.dto.AuthDtos.*; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; import BookPick.mvp.domain.auth.exception.InvalidLoginException; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.repository.UserRepository; @@ -34,28 +35,27 @@ public class AuthService { @Transactional public SignRes signUp(SignReq req) { - // 1. 중복 확인 + + // 1. 중복 확인 if (userRepository.existsAllByEmail((req.email()))) { - throw new DuplicateResourceException(ErrorCode.DUPLICATE_EMAIL); + throw new DuplicateEmailException(); } // 2. 신규 유저 생성 - User user = new User(); - user.setEmail(req.email()); - user.setPassword(passwordEncoder.encode(req.password())); - user.setRole(Roles.ROLE_USER); // normal_user, curator + User user = User.builder() + .email(req.email()) + .password(passwordEncoder.encode(req.password())) + .role(Roles.ROLE_USER) + .build(); // 3. DB 저장 - User savedUser = userRepository.save(user); + User saved = userRepository.save(user); // 4. 응답 - return new SignRes(savedUser.getId()); + return SignRes.from(saved.getId()); } - - - // access Token 만 전송, refresh x @Transactional(readOnly = true) public AuthRes login(LoginReq req, HttpServletResponse res) { @@ -81,12 +81,11 @@ public AuthRes login(LoginReq req, HttpServletResponse res) { "Bearer " + access ); } catch (BadCredentialsException | UsernameNotFoundException e) { - throw new InvalidLoginException("아이디 또는 비밀번호가 잘못되었습니다."); + throw new InvalidLoginException(); } catch (AuthenticationException e) { - throw new InvalidLoginException("로그인 실패"); + throw new InvalidLoginException(); } } - } diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index 38b71ef..193c5b5 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -5,8 +5,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; -import lombok.Getter; -import lombok.Setter; +import lombok.*; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -15,7 +14,9 @@ @Entity @Table(name = "user") @Getter -@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor public class User { From 1dd9e869c72a8fc3208d38febc150b1fda195094 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 15 Oct 2025 00:01:02 +0900 Subject: [PATCH 028/291] chore: meaningless commit --- .../mvp/domain/auth/controller/AuthController.java | 11 +++++------ .../BookPick/mvp/domain/auth/service/AuthService.java | 7 ++++--- .../domain/preference/service/PreferenceService.java | 7 ++++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 6c3a3be..a49af72 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -18,9 +18,9 @@ public class AuthController { private final AuthService authService; - @PostMapping("/signup") + @PostMapping("/signup") public ResponseEntity> signUp(@Valid @RequestBody SignReq req) { - SignRes signRes = authService.signUp(req); + SignRes signRes = authService.signUp(req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS, signRes)); @@ -28,15 +28,14 @@ public ResponseEntity> signUp(@Valid @RequestBody SignReq r @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res){ - + public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res) { AuthRes authRes = authService.login(req, res); + return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, authRes)) ; //data 에 DTO 주기 + .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, authRes)); } - } // 제목 diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index f5c66d1..d78bc7a 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -52,14 +52,15 @@ public SignRes signUp(SignReq req) { User saved = userRepository.save(user); // 4. 응답 - return SignRes.from(saved.getId()); + return SignRes.from(saved.getId()); } - // access Token 만 전송, refresh x + // access Token O, refresh X @Transactional(readOnly = true) public AuthRes login(LoginReq req, HttpServletResponse res) { - var authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); + var authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // -> UserDetailsService.loadUserByUsername() + try { var auth = authenticationManagerBuilder.getObject().authenticate(authToken); diff --git a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java index 15b4b74..aab2daf 100644 --- a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java @@ -17,8 +17,9 @@ public class PreferenceService { @Transactional public PreferenceRes create(Long userId, CreateReq req) { - User user = new User(); - user.setId(userId); + User user = User.builder() + .id(userId) + .build(); if (preferenceRepository.existsByUserId(userId)) { throw new IllegalStateException("이미 등록된 선호정보가 있습니다."); @@ -26,7 +27,7 @@ public PreferenceRes create(Long userId, CreateReq req) { // --- 문자열 요청 → 엔티티 변환 --- List authors = req.favoriteAuthors(); // ["1", "2", "3"] - List books = req.favoriteBooks(); // ["1", "2", "3"] + List books = req.favoriteBooks(); // ["1", "2", "3"] // --- UserPreference 생성 --- UserPreference pref = UserPreference.from(req, user); From 43644c19ac7e422a2a479825250ca22e07ee7ff0 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 15 Oct 2025 01:34:32 +0900 Subject: [PATCH 029/291] =?UTF-8?q?feat(auth)=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 12 ++---- .../mvp/domain/auth/dto/AuthDtos.java | 42 ++++++++++++------- .../mvp/domain/auth/service/AuthService.java | 27 ++++-------- .../auth/service/MyUserDetailsService.java | 39 ++++++++--------- .../domain/preference/dto/PreferenceDtos.java | 1 + .../preference/entity/UserPreference.java | 25 +++++------ .../user/exception/UserNotFoundException.java | 11 +++++ .../BookPick/mvp/global/api/ErrorCode.java | 2 +- .../BookPick/mvp/global/util/JwtUtil.java | 4 +- src/main/resources/application.yml | 2 +- 10 files changed, 87 insertions(+), 78 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index a49af72..3bd3df4 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -28,16 +28,10 @@ public ResponseEntity> signUp(@Valid @RequestBody SignReq r @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res) { - AuthRes authRes = authService.login(req, res); + public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res) { + LoginRes loginRes = authService.login(req, res); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, authRes)); + .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, loginRes)); } - - } - -// 제목 -// 내용 -// \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index c08457b..c5080a1 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -1,18 +1,19 @@ package BookPick.mvp.domain.auth.dto; +import BookPick.mvp.domain.auth.service.MyUserDetailsService; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public class AuthDtos { - // -- SignUp -- public record SignReq( @NotBlank @Email String email, @Size(min = 8, max = 72) String password - ) {} + ) { + } public record SignRes( long userId ) { @@ -26,8 +27,9 @@ public static SignRes from(long userId) { public record LoginReq( @NotBlank @Email String email, @Size(min = 8, max = 72) String password - ) {} - public record AuthRes( + ) { + } + public record LoginRes( long userId, String email, String nickname, @@ -35,18 +37,30 @@ public record AuthRes( String profileImageUrl, String accessToken ) { - } + + public static LoginRes from(MyUserDetailsService.CustomUserDetails customUserDetails, String accessToken) { + return new LoginRes( + customUserDetails.getId(), + customUserDetails.getUsername(), // username = email + customUserDetails.getNickname(), + customUserDetails.getBio(), + customUserDetails.getProfileImageUrl(), + accessToken + ); + } - // 3. 로그아 - public record LogoutReq( - @NotBlank String refreshToken - ) { - } + // 3. 로그아 + public record LogoutReq( + @NotBlank String refreshToken + ) { + } - // 3. 토큰 재발급 요청 - public record RefreshToken( - @NotBlank String refreshToken - ) { + // 3. 토큰 재발급 요청 + public record RefreshToken( + @NotBlank String refreshToken + ) { + } } } + diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index d78bc7a..c434007 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -16,6 +16,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; @@ -58,29 +59,19 @@ public SignRes signUp(SignReq req) { // access Token O, refresh X @Transactional(readOnly = true) - public AuthRes login(LoginReq req, HttpServletResponse res) { - var authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // -> UserDetailsService.loadUserByUsername() + public LoginRes login(LoginReq req, HttpServletResponse res) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 try { - var auth = authenticationManagerBuilder.getObject().authenticate(authToken); + Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 + String accessToken = JwtUtil.createAccessToken(auth); // Access O + String refreshToken = JwtUtil.createRefreshToken(auth); // Refresh X + res.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); - // Access 토큰만 발급 - String access = JwtUtil.createAccessToken(auth); - String refresh = JwtUtil.createRefreshToken(auth); + MyUserDetailsService.CustomUserDetails customUserDetails = (MyUserDetailsService.CustomUserDetails) auth.getPrincipal(); - var principal = (MyUserDetailsService.CustomUser) auth.getPrincipal(); + return LoginRes.from(customUserDetails, "Bearer " + accessToken); - // 선택: 응답 헤더에도 추가해주면 프론트가 꺼내 쓰기 쉬움 - res.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + access); - - return new AuthRes( - principal.getId(), - principal.getUsername(), // email - principal.getNickname(), - principal.getBio(), - principal.getProfileImageUrl(), - "Bearer " + access - ); } catch (BadCredentialsException | UsernameNotFoundException e) { throw new InvalidLoginException(); } catch (AuthenticationException e) { diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 98ef7b3..153a4dd 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -2,10 +2,12 @@ import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; @@ -27,51 +29,46 @@ @Service @RequiredArgsConstructor public class MyUserDetailsService implements UserDetailsService { - private final UserRepository UserRepo; + private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - var customeUserOpt = UserRepo.findByEmail(email); - List auth= new ArrayList<>(); - if(customeUserOpt.isEmpty()){ - throw new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email); - } + List auth = new ArrayList<>(); + BookPick.mvp.domain.user.entity.User user = userRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); - if (customeUserOpt.get().getRole().equals(Roles.ROLE_USER)) { + if (user.getRole().equals(Roles.ROLE_USER)) { auth.add(new SimpleGrantedAuthority(Roles.ROLE_USER.name())); } - var customUser = new CustomUser(customeUserOpt.get(), auth); //username = email, passowrd, authorities 등록 - customUser.setId(customeUserOpt.get().getId()); - customUser.setNickname(customeUserOpt.get().getNickname()); - customUser.setBio(customeUserOpt.get().getBio()); - customUser.setProfileImageUrl(customeUserOpt.get().getProfileImageUrl()); + CustomUserDetails customUserDetails = new CustomUserDetails(user, auth); // email, passWord, authorities 등록 + customUserDetails.setId(user.getId()); + customUserDetails.setNickname(user.getNickname()); + customUserDetails.setBio(user.getBio()); + customUserDetails.setProfileImageUrl(user.getProfileImageUrl()); - return customUser; + return customUserDetails; } @Getter @Setter - public static class CustomUser extends User{ + public static class CustomUserDetails extends User { private Long id; private String nickname; private String bio; private String profileImageUrl; - public CustomUser( + public CustomUserDetails( BookPick.mvp.domain.user.entity.User user, Collection authorities - ){ - super(user.getEmail(), user.getPassword(),authorities); + ) { + super(user.getEmail(), user.getPassword(), authorities); this.bio = user.getBio(); - this.profileImageUrl=user.getProfileImageUrl(); + this.profileImageUrl = user.getProfileImageUrl(); } - } - - } diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java index 7217d1c..ee02c02 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java @@ -30,6 +30,7 @@ public record CreateReq( public record UpdateReq( @NotBlank String mbti, @NotNull List favoriteAuthors, + @NotNull List favoriteBooks, @NotNull List selectionCriteria, @NotNull List readingHabits, @NotNull List preferredGenres, diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java index 9aa8a81..f401648 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java @@ -10,9 +10,6 @@ import java.util.List; - - - @Entity @Getter @Setter @@ -23,18 +20,21 @@ public class UserPreference { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // 유저와 1:1 관계 (유저당 하나만) - @OneToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "userId", unique = true, nullable = false) + // ✅ 유저와 1:1 관계 (유저 삭제 시 preference도 자동 삭제) + @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.REMOVE) + @JoinColumn(name = "user_id", unique = true, nullable = false) private User user; private String mbti; - + @ElementCollection + @CollectionTable(name = "preference_favorite_authors", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "author") private List favoriteAuthors; - - + @ElementCollection + @CollectionTable(name = "preference_favorite_books", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "book") private List favoriteBooks; @ElementCollection @@ -67,7 +67,8 @@ public static UserPreference from(CreateReq req, User user) { UserPreference pref = new UserPreference(); pref.user = user; pref.mbti = req.mbti(); - // Author/Book 매핑은 별도 Repository에서 가져와야 함 + pref.favoriteAuthors = req.favoriteAuthors(); + pref.favoriteBooks = req.favoriteBooks(); pref.selectionCriteria = req.selectionCriteria(); pref.readingHabits = req.readingHabits(); pref.preferredGenres = req.preferredGenres(); @@ -76,14 +77,14 @@ public static UserPreference from(CreateReq req, User user) { return pref; } - // ---- 수정 적용 ---- public void apply(UpdateReq req) { this.mbti = req.mbti(); + this.favoriteAuthors = req.favoriteAuthors(); + this.favoriteBooks = req.favoriteBooks(); this.selectionCriteria = req.selectionCriteria(); this.readingHabits = req.readingHabits(); this.preferredGenres = req.preferredGenres(); this.keywords = req.keywords(); this.recommendedTrends = req.recommendedTrends(); } - } diff --git a/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java b/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java new file mode 100644 index 0000000..3f0d2c4 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.user.exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class UserNotFoundException extends BusinessException { + public UserNotFoundException(){ + super(ErrorCode.User_NOT_FOUND); + } + +} diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java index 32b2988..10057ed 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -10,7 +10,7 @@ public enum ErrorCode { // -- Auth INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), // 400 회원 가입 - DUPLICATE_EMAIL(HttpStatus.CONFLICT, "중복된 이메일 입니다."), // 409 회원 가입 + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일 입니다."), // 409 회원 가입 AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘 못 되었습니다."), // 401 로그인 // -- User -- diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 2ff703c..943710f 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -29,7 +29,7 @@ public class JwtUtil { // 2. JWT 생성 public static String createAccessToken(Authentication auth) { - CustomUser usr = (CustomUser) auth.getPrincipal(); + CustomUserDetails usr = (CustomUserDetails) auth.getPrincipal(); String authorities = auth.getAuthorities().stream() //getAuthorities -> List return .map(a->a.getAuthority()) // getAuthority() -> String return @@ -50,7 +50,7 @@ public static String createAccessToken(Authentication auth) { // ✅ 2-1. Refresh 토큰 생성 (여기 추가) public static String createRefreshToken(Authentication auth) { - CustomUser usr = (CustomUser) auth.getPrincipal(); + CustomUserDetails usr = (CustomUserDetails) auth.getPrincipal(); // refresh 토큰에는 최소 정보만: subject/email + typ 정도만 권장 return Jwts.builder() diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 96f1c54..8ee3c14 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: none properties: hibernate: show_sql: false From 69cc40970c5afc3b67d98a14660b4a61f9b12d9a Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 16 Oct 2025 00:40:57 +0900 Subject: [PATCH 030/291] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=8B=9C,=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=98=20=EC=B2=AB?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=9C=A0=EB=AC=B4=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20Body=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/auth/dto/AuthDtos.java | 5 +++++ .../mvp/domain/auth/service/AuthService.java | 18 ++++++++++++++++-- .../auth/service/MyUserDetailsService.java | 2 ++ .../BookPick/mvp/domain/user/entity/User.java | 9 ++++++++- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index c5080a1..7247630 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -14,6 +14,7 @@ public record SignReq( @Size(min = 8, max = 72) String password ) { } + public record SignRes( long userId ) { @@ -29,12 +30,15 @@ public record LoginReq( @Size(min = 8, max = 72) String password ) { } + public record LoginRes( long userId, String email, String nickname, String bio, String profileImageUrl, + boolean isFirstLogin, + String accessToken ) { @@ -45,6 +49,7 @@ public static LoginRes from(MyUserDetailsService.CustomUserDetails customUserDet customUserDetails.getNickname(), customUserDetails.getBio(), customUserDetails.getProfileImageUrl(), + customUserDetails.isFirstLogin(), accessToken ); } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index c434007..9a5da8b 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.auth.exception.DuplicateEmailException; import BookPick.mvp.domain.auth.exception.InvalidLoginException; import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.custom.DuplicateResourceException; @@ -58,19 +59,23 @@ public SignRes signUp(SignReq req) { // access Token O, refresh X - @Transactional(readOnly = true) + @Transactional public LoginRes login(LoginReq req, HttpServletResponse res) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 try { Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 + + firstLoginHandle(req.email()); + String accessToken = JwtUtil.createAccessToken(auth); // Access O String refreshToken = JwtUtil.createRefreshToken(auth); // Refresh X + res.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); MyUserDetailsService.CustomUserDetails customUserDetails = (MyUserDetailsService.CustomUserDetails) auth.getPrincipal(); - return LoginRes.from(customUserDetails, "Bearer " + accessToken); + return LoginRes.from(customUserDetails, "Bearer " + accessToken); } catch (BadCredentialsException | UsernameNotFoundException e) { throw new InvalidLoginException(); @@ -79,5 +84,14 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { } } + @Transactional + void firstLoginHandle(String email){ + User user = userRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); + + if(user.isFirstLogin()){ + user.isNotFirstLogin(); + } + } } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 153a4dd..664b133 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -48,6 +48,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep customUserDetails.setNickname(user.getNickname()); customUserDetails.setBio(user.getBio()); customUserDetails.setProfileImageUrl(user.getProfileImageUrl()); + customUserDetails.setFirstLogin(user.isFirstLogin()); return customUserDetails; @@ -60,6 +61,7 @@ public static class CustomUserDetails extends User { private String nickname; private String bio; private String profileImageUrl; + private boolean isFirstLogin; public CustomUserDetails( BookPick.mvp.domain.user.entity.User user, diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index 193c5b5..fb9ec4e 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -45,9 +45,12 @@ public class User { @Size(message = "자기소개는 255자 이하여야 합니다.") private String bio; // 자기소개 문구 - @Column(name = "profileImageUrl", length = 500) + @Column(name = "profile_image_url", length = 500) private String profileImageUrl; // 프로필 사진 경로 + @Column(name ="is_first_login", nullable = false) + private boolean isFirstLogin = true; + @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; // 생성 시각 @@ -57,5 +60,9 @@ public class User { private LocalDateTime updatedAt; // 수정 시각 + public void isNotFirstLogin(){ + this.isFirstLogin=false; + } + } From 8eecdfcfa354e9710b7af392baebe83fe61fda51 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 16 Oct 2025 21:03:42 +0900 Subject: [PATCH 031/291] =?UTF-8?q?chore=20:=20=EC=98=A4=ED=83=80=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/global/api/ApiResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/api/ApiResponse.java b/src/main/java/BookPick/mvp/global/api/ApiResponse.java index 4a58784..52173b2 100644 --- a/src/main/java/BookPick/mvp/global/api/ApiResponse.java +++ b/src/main/java/BookPick/mvp/global/api/ApiResponse.java @@ -19,8 +19,8 @@ public class ApiResponse { // -- Success -- - public static ApiResponse success(SuccessCode successMessage, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 - return new ApiResponse(successMessage.getStatus().value(), successMessage.getMessage(), data); + public static ApiResponse success(SuccessCode successCode, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 + return new ApiResponse(successCode.getStatus().value(), successCode.getMessage(), data); } // -- Error -- From 8294a25647a19e411de000517abe2981e3da9b38 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 17 Oct 2025 00:01:21 +0900 Subject: [PATCH 032/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8ee3c14..51c17fc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,4 +40,6 @@ jwt: secret: refreshsecret123refreshsecret123refreshsecret123refreshsecret123 expiration: 7d - +api : + kakao : + key : 103086ac1d365cf71f026f6caac34fb3 From 3cd4342bd8aa52fd2bcbe34d27d37b74871f0275 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 17 Oct 2025 00:01:36 +0900 Subject: [PATCH 033/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/dto/AuthDtos.java | 3 +- .../book/Controller/BookSearchController.java | 29 +++++++ .../mvp/domain/book/dto/BookDtos.java | 23 +++++ .../book/service/BookSearchService.java | 84 +++++++++++++++++++ .../domain/preference/dto/PreferenceDtos.java | 32 +++---- .../BookPick/mvp/global/api/SuccessCode.java | 6 +- .../BookPick/mvp/global/dto/CursorInfo.java | 11 +++ .../BookPick/mvp/global/dto/PageInfo.java | 19 +++++ 8 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java create mode 100644 src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java create mode 100644 src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java create mode 100644 src/main/java/BookPick/mvp/global/dto/CursorInfo.java create mode 100644 src/main/java/BookPick/mvp/global/dto/PageInfo.java diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index 7247630..8fa1c2e 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -2,9 +2,9 @@ import BookPick.mvp.domain.auth.service.MyUserDetailsService; -import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.Email; public class AuthDtos { @@ -23,7 +23,6 @@ public static SignRes from(long userId) { } } - // -- Login -- public record LoginReq( @NotBlank @Email String email, diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java new file mode 100644 index 0000000..62bc216 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -0,0 +1,29 @@ +package BookPick.mvp.domain.book.Controller; + +import BookPick.mvp.domain.book.dto.BookDtos.*; +import BookPick.mvp.domain.book.service.BookSearchService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/book") +@RequiredArgsConstructor +public class BookSearchController { + + private final BookSearchService bookSearchService; + + @PostMapping("/search") + public ResponseEntity> searchBookList(@RequestBody BookSearchReq req){ + BookSearchPageRes res = bookSearchService.getBookSearchList(req); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.BOOK_LIST_READ_SUCCESS, res)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java b/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java new file mode 100644 index 0000000..fa87250 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java @@ -0,0 +1,23 @@ +package BookPick.mvp.domain.book.dto; + + +import BookPick.mvp.global.dto.PageInfo; +import java.util.List; + +public class BookDtos { + + // -- R -- + public record BookSearchReq( + String keyword + ){} + public record BookSearchRes( + String title, + String author, + String image +) {} + public record BookSearchPageRes( + List books, + PageInfo pageInfo +) {} + +} diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java new file mode 100644 index 0000000..0f4e51c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java @@ -0,0 +1,84 @@ +package BookPick.mvp.domain.book.service; + +import BookPick.mvp.domain.book.dto.BookDtos.*; +import BookPick.mvp.global.dto.PageInfo; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Service +public class BookSearchService { + + @Value("${api.kakao.key}") + private String kakaoApiKey; + + private static final String API_URL = "https://dapi.kakao.com/v3/search/book"; + + public BookSearchPageRes getBookSearchList(BookSearchReq req) { + + RestTemplate restTemplate = new RestTemplate(); + + // 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // 요청 URL 구성 + UriComponents uri = UriComponentsBuilder.fromHttpUrl(API_URL) + .queryParam("query", req.keyword()) + .queryParam("page", 1) + .queryParam("size", 10) + .build(); + + + HttpEntity requestEntity = new HttpEntity<>(headers); + + // 카카오 API 호출 + ResponseEntity response = restTemplate.exchange( + uri.toUriString(), + HttpMethod.GET, + requestEntity, + Map.class + ); + + // documents 배열 추출 + List> documents = (List>) response.getBody().get("documents"); + + // 필요한 데이터만 변환 + List books = new ArrayList<>(); + for (Map doc : documents) { + String title = (String) doc.get("title"); + List authors = (List) doc.get("authors"); + String author = authors != null && !authors.isEmpty() ? authors.get(0) : "저자 미상"; + String image = (String) doc.get("thumbnail"); + + books.add(new BookSearchRes(title, author, image)); + } + + // meta 데이터 추출 → PageInfo 변환 + Map meta = (Map) response.getBody().get("meta"); + int totalCount = ((Number) meta.get("total_count")).intValue(); + boolean isEnd = (boolean) meta.get("is_end"); + + // PageInfo 매핑 (현재 페이지는 Kakao API 요청 기준) + PageInfo pageInfo = new PageInfo( + 1, // currentPage (요청 page) + (int) Math.ceil((double) totalCount / 10), // totalPages (총 페이지 수) + totalCount, // totalElements (총 아이템 수) + !isEnd // hasNext (다음 페이지 여부) + ); + + // 최종 응답 DTO 반환 + return new BookSearchPageRes(books, pageInfo); + } +} diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java index ee02c02..e1b9151 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java @@ -22,8 +22,8 @@ public record CreateReq( @NotNull List preferredGenres, // 선호 장르 @NotNull List keywords, // 키워드 @NotNull List recommendedTrends // - ){} - + ) { + } // 2. 취향 수정 @@ -36,7 +36,8 @@ public record UpdateReq( @NotNull List preferredGenres, @NotNull List keywords, @NotNull List recommendedTrends - ) {} + ) { + } // PreferenceRes (MVP용 단순화) @@ -52,23 +53,22 @@ public record PreferenceRes( List recommendedTrends ) { public static PreferenceRes from(UserPreference p) { - return new PreferenceRes( - p.getId(), - p.getMbti(), - p.getFavoriteAuthors(), - p.getFavoriteBooks(), - p.getSelectionCriteria(), - p.getReadingHabits(), - p.getPreferredGenres(), - p.getKeywords(), - p.getRecommendedTrends() - ); -} + return new PreferenceRes( + p.getId(), + p.getMbti(), + p.getFavoriteAuthors(), + p.getFavoriteBooks(), + p.getSelectionCriteria(), + p.getReadingHabits(), + p.getPreferredGenres(), + p.getKeywords(), + p.getRecommendedTrends() + ); + } } - } diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index abda168..01f3603 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -31,7 +31,11 @@ public enum SuccessCode { COMMENT_LIST_EMPTY(HttpStatus.OK, "댓글이 없습니다."), COMMENT_READ_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 조회하였습니다."), COMMENT_UPDATE_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 수정하였습니다."), - COMMENT_DELETE_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 삭제하였습니다."); + COMMENT_DELETE_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 삭제하였습니다."), + + // -- Book -- + BOOK_LIST_READ_SUCCESS(HttpStatus.OK, "책 목록을 성공적으로 조회하였습니다."); + private final HttpStatus status; diff --git a/src/main/java/BookPick/mvp/global/dto/CursorInfo.java b/src/main/java/BookPick/mvp/global/dto/CursorInfo.java new file mode 100644 index 0000000..a9172c8 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/dto/CursorInfo.java @@ -0,0 +1,11 @@ +package BookPick.mvp.global.dto; + +public record CursorInfo( + boolean hasNext, + Long nextCursor, + int size +) { + public static CursorInfo of(boolean hasNext, Long nextCursor, int size) { + return new CursorInfo(hasNext, nextCursor, size); + } +} diff --git a/src/main/java/BookPick/mvp/global/dto/PageInfo.java b/src/main/java/BookPick/mvp/global/dto/PageInfo.java new file mode 100644 index 0000000..23eaa2f --- /dev/null +++ b/src/main/java/BookPick/mvp/global/dto/PageInfo.java @@ -0,0 +1,19 @@ +package BookPick.mvp.global.dto; + +import org.springframework.data.domain.Page; + +public record PageInfo( + int currentPage, + int totalPages, + long totalElements, + boolean hasNext +) { + public static PageInfo of(Page page) { + return new PageInfo( + page.getNumber(), + page.getTotalPages(), + page.getTotalElements(), + page.hasNext() + ); + } +} From 4bd0cd8666e0e04acdb4fc9e0c6c299765bc88d6 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 17 Oct 2025 21:20:41 +0900 Subject: [PATCH 034/291] =?UTF-8?q?chore=20:=20=EA=B8=B0=EB=8A=A5=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...r.java => ReadingPreferenceController.java} | 18 ++++++++---------- ...ferenceDtos.java => ReadingPreference.java} | 5 ++--- ...rPreference.java => ReadingPreference.java} | 10 +++++----- ...y.java => ReadingPreferenceRepository.java} | 4 ++-- ...vice.java => ReadingPreferenceService.java} | 16 ++++++++-------- 5 files changed, 25 insertions(+), 28 deletions(-) rename src/main/java/BookPick/mvp/domain/preference/controller/{PreferenceController.java => ReadingPreferenceController.java} (55%) rename src/main/java/BookPick/mvp/domain/preference/dto/{PreferenceDtos.java => ReadingPreference.java} (93%) rename src/main/java/BookPick/mvp/domain/preference/entity/{UserPreference.java => ReadingPreference.java} (90%) rename src/main/java/BookPick/mvp/domain/preference/repository/{PreferenceRepository.java => ReadingPreferenceRepository.java} (50%) rename src/main/java/BookPick/mvp/domain/preference/service/{PreferenceService.java => ReadingPreferenceService.java} (66%) diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java similarity index 55% rename from src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java rename to src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java index c0e9fc4..0f6b33e 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/PreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java @@ -1,30 +1,28 @@ package BookPick.mvp.domain.preference.controller; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; -import BookPick.mvp.domain.preference.service.PreferenceService; +import BookPick.mvp.domain.preference.dto.ReadingPreference.*; +import BookPick.mvp.domain.preference.service.ReadingPreferenceService; import BookPick.mvp.global.api.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.net.URI; - @RestController -@RequestMapping("/api/users/{id}/preferences") +@RequestMapping("/api/v1/users/{id}/preferences") @RequiredArgsConstructor -public class PreferenceController { +public class ReadingPreferenceController { - private final PreferenceService preferenceService; + private final ReadingPreferenceService readingPreferenceService; @PostMapping public ResponseEntity> create(@PathVariable("id") Long userId, @Valid @RequestBody CreateReq req) { - PreferenceRes res = preferenceService.create(userId, req); - URI location = URI.create("/api/users/" + userId + "/preferences"); + PreferenceRes res = readingPreferenceService.create(userId, req); - return ResponseEntity.created(location) + return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); } } diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java b/src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java similarity index 93% rename from src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java rename to src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java index e1b9151..8206a37 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/PreferenceDtos.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java @@ -1,13 +1,12 @@ package BookPick.mvp.domain.preference.dto; -import BookPick.mvp.domain.preference.entity.UserPreference; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.util.List; -public class PreferenceDtos { +public class ReadingPreference { // 1. 취향 설정 @@ -52,7 +51,7 @@ public record PreferenceRes( List keywords, List recommendedTrends ) { - public static PreferenceRes from(UserPreference p) { + public static PreferenceRes from(BookPick.mvp.domain.preference.entity.ReadingPreference p) { return new PreferenceRes( p.getId(), p.getMbti(), diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java similarity index 90% rename from src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java rename to src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java index f401648..b81b8fc 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/UserPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.preference.entity; -import BookPick.mvp.domain.preference.dto.PreferenceDtos.CreateReq; -import BookPick.mvp.domain.preference.dto.PreferenceDtos.UpdateReq; +import BookPick.mvp.domain.preference.dto.ReadingPreference.CreateReq; +import BookPick.mvp.domain.preference.dto.ReadingPreference.UpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.Getter; @@ -14,7 +14,7 @@ @Getter @Setter @NoArgsConstructor -public class UserPreference { +public class ReadingPreference { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -63,8 +63,8 @@ public class UserPreference { private List recommendedTrends; // ---- 팩토리 메서드 ---- - public static UserPreference from(CreateReq req, User user) { - UserPreference pref = new UserPreference(); + public static ReadingPreference from(CreateReq req, User user) { + ReadingPreference pref = new ReadingPreference(); pref.user = user; pref.mbti = req.mbti(); pref.favoriteAuthors = req.favoriteAuthors(); diff --git a/src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java b/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java similarity index 50% rename from src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java rename to src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java index db675a2..f9743d2 100644 --- a/src/main/java/BookPick/mvp/domain/preference/repository/PreferenceRepository.java +++ b/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java @@ -1,8 +1,8 @@ package BookPick.mvp.domain.preference.repository; -import BookPick.mvp.domain.preference.entity.UserPreference; +import BookPick.mvp.domain.preference.entity.ReadingPreference; import org.springframework.data.jpa.repository.JpaRepository; -public interface PreferenceRepository extends JpaRepository { +public interface ReadingPreferenceRepository extends JpaRepository { boolean existsByUserId(Long userId); } diff --git a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java similarity index 66% rename from src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java rename to src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java index aab2daf..bde1e31 100644 --- a/src/main/java/BookPick/mvp/domain/preference/service/PreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java @@ -1,8 +1,8 @@ package BookPick.mvp.domain.preference.service; -import BookPick.mvp.domain.preference.dto.PreferenceDtos.*; -import BookPick.mvp.domain.preference.entity.UserPreference; -import BookPick.mvp.domain.preference.repository.PreferenceRepository; +import BookPick.mvp.domain.preference.dto.ReadingPreference.*; +import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.preference.repository.ReadingPreferenceRepository; import BookPick.mvp.domain.user.entity.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,8 +12,8 @@ @Service @RequiredArgsConstructor -public class PreferenceService { - private final PreferenceRepository preferenceRepository; +public class ReadingPreferenceService { + private final ReadingPreferenceRepository preferenceRepository; @Transactional public PreferenceRes create(Long userId, CreateReq req) { @@ -29,12 +29,12 @@ public PreferenceRes create(Long userId, CreateReq req) { List authors = req.favoriteAuthors(); // ["1", "2", "3"] List books = req.favoriteBooks(); // ["1", "2", "3"] - // --- UserPreference 생성 --- - UserPreference pref = UserPreference.from(req, user); + // --- ReadingPreference 생성 --- + ReadingPreference pref = ReadingPreference.from(req, user); pref.setFavoriteAuthors(authors); pref.setFavoriteBooks(books); - UserPreference saved = preferenceRepository.save(pref); + ReadingPreference saved = preferenceRepository.save(pref); return PreferenceRes.from(saved); } } From 992eccb93cb74ace59690b4913e78b58b528a99a Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 02:03:50 +0900 Subject: [PATCH 035/291] =?UTF-8?q?chore=20:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java index 8fa1c2e..31ba75d 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.auth.dto; -import BookPick.mvp.domain.auth.service.MyUserDetailsService; +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Email; @@ -41,7 +41,7 @@ public record LoginRes( String accessToken ) { - public static LoginRes from(MyUserDetailsService.CustomUserDetails customUserDetails, String accessToken) { + public static LoginRes from(CustomUserDetails customUserDetails, String accessToken) { return new LoginRes( customUserDetails.getId(), customUserDetails.getUsername(), // username = email From 1f6f03eccdd5655953e2270f32ec56ea3ad982b7 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 14:52:56 +0900 Subject: [PATCH 036/291] =?UTF-8?q?test=20:=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/auth/service/MyUserDetailsService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 664b133..fec314a 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -71,6 +71,9 @@ public CustomUserDetails( this.bio = user.getBio(); this.profileImageUrl = user.getProfileImageUrl(); } + + reading + } } From 167bb2ba3f01959fddbf124a4007c61dd7020a5c Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 14:53:20 +0900 Subject: [PATCH 037/291] =?UTF-8?q?test=20:=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/auth/service/MyUserDetailsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index fec314a..2003ba5 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -72,7 +72,7 @@ public CustomUserDetails( this.profileImageUrl = user.getProfileImageUrl(); } - reading + readingㄴㄴㄴ } } From 9ea139a072f54df3efeac91fb484ac1c3e106b2c Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 15:06:16 +0900 Subject: [PATCH 038/291] =?UTF-8?q?test=20:=20=EC=BB=A4=EB=B0=8B=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/service/MyUserDetailsService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 2003ba5..902264a 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -72,7 +72,9 @@ public CustomUserDetails( this.profileImageUrl = user.getProfileImageUrl(); } - readingㄴㄴㄴ + public CustomUserDetails{ + + } } } From ba9b3a80788589632a082d20486356f4cf3dea56 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 15:55:13 +0900 Subject: [PATCH 039/291] =?UTF-8?q?etc=20:=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/global/config/JwtFilter.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 5523722..80cc9d2 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,5 +1,6 @@ package BookPick.mvp.global.config; +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.util.JwtUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; @@ -26,8 +27,6 @@ public class JwtFilter extends OncePerRequestFilter { private static final String BEARER = "Bearer"; - - @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain @@ -41,26 +40,29 @@ protected void doFilterInternal( } String token = resolveAccessToken(request); - if (token == null) { // 토큰 없으면 그냥 통과 - filterChain.doFilter(request, response); - return; - } + if (token == null) { // 토큰 없으면 그냥 통과 + filterChain.doFilter(request, response); + return; + } - try { + try { Claims claims = JwtUtil.extractToken(token); var authorities = Arrays.stream( claims.get("authorities").toString().split(",") ).map(SimpleGrantedAuthority::new).toList(); + + CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(claims.get("userId"), claims.get("email"), claims.get("authorities")); + var auth = new UsernamePasswordAuthenticationToken( - claims.get("email"), null, authorities + customUserDetails, null, customUserDetails.getAuthorities() ); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); - } catch (ExpiredJwtException e) { + } catch (ExpiredJwtException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } @@ -69,7 +71,6 @@ protected void doFilterInternal( } - private String resolveAccessToken(HttpServletRequest request) { String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (header != null && header.startsWith(BEARER)) { From 71b962092c5821e837be044abf595531a4b3d33c Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 15:55:24 +0900 Subject: [PATCH 040/291] =?UTF-8?q?etc=20:=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlreadyRegisteredReadingPreferenceException.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java new file mode 100644 index 0000000..a51b157 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.readingPreferenceRepository.Exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class AlreadyRegisteredReadingPreferenceException extends BusinessException { + public AlreadyRegisteredReadingPreferenceException(){ + super(ErrorCode.READING_PREFERENCE_ALREADY_RESiGSTER); + } +} From d1d2addf298695dc26cae25b20479f3fde7edd07 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 15:55:44 +0900 Subject: [PATCH 041/291] =?UTF-8?q?etc=20:=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreferenceController.java | 31 ++++++++ .../dto/ReadingPreferenceDtos.java | 37 +++++++++ .../entity/ReadingPreference.java | 33 +------- .../ReadingPreferenceRepository.java | 4 +- .../service/ReadingPreferenceService.java | 23 ++++++ .../auth/service/MyUserDetailsService.java | 15 +++- .../ReadingPreferenceController.java | 28 ------- .../preference/dto/ReadingPreference.java | 75 ------------------- .../service/ReadingPreferenceService.java | 40 ---------- .../mvp/domain/user/dto/UserDtos.java | 19 ----- .../BookPick/mvp/global/api/ErrorCode.java | 4 +- .../BookPick/mvp/global/api/SuccessCode.java | 5 +- 12 files changed, 118 insertions(+), 196 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/entity/ReadingPreference.java (58%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/repository/ReadingPreferenceRepository.java (60%) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java delete mode 100644 src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java delete mode 100644 src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java delete mode 100644 src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java new file mode 100644 index 0000000..8837d5a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.ReadingPreference.controller; + +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceDtos.*; +import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequestMapping("/api/v1/reading-preference") +@RequiredArgsConstructor +public class ReadingPreferenceController { + + private final ReadingPreferenceService readingPreferenceService; + + @PostMapping + public ResponseEntity> registerReadingPreference(@Valid @RequestBody ReadingPreferenceRegisterReq req, + @AuthenticationPrincipal CustomUserDetails user) { + ReadingPreferenceRegisterRes res = readingPreferenceService.create(user.getId(), req); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_REGISTER_SUCCESS, res)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java new file mode 100644 index 0000000..1652083 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java @@ -0,0 +1,37 @@ +package BookPick.mvp.domain.ReadingPreference.dto; + + +import BookPick.mvp.domain.user.entity.User; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public class ReadingPreferenceDtos { + + + // -- register -- + public record ReadingPreferenceRegisterReq( + @NotBlank String mbti, +// @NotNull List favoriteAuthors, // 좋아하는 작가 + @NotNull List favoriteBooks, // 좋아하는 책 + @NotNull List mood, // 독서 선호 분위기 + @NotNull List readingHabits, // 독서 습관 + @NotNull List preferredGenres, // 선호 장르 + @NotNull List keywords, // 키워드 + @NotNull List recommendedTrends // + ) { + } + public record ReadingPreferenceRegisterRes( + Long readingPreferenceId + ){ + public ReadingPreferenceRegisterRes from(User user){ + return new ReadingPreferenceRegisterRes(user.getId()); + } + } + + } + + + + diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java similarity index 58% rename from src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index b81b8fc..0551264 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -1,7 +1,5 @@ -package BookPick.mvp.domain.preference.entity; +package BookPick.mvp.domain.ReadingPreference.entity; -import BookPick.mvp.domain.preference.dto.ReadingPreference.CreateReq; -import BookPick.mvp.domain.preference.dto.ReadingPreference.UpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.Getter; @@ -11,6 +9,7 @@ import java.util.List; @Entity +@Table(name = "user_reading_preference") @Getter @Setter @NoArgsConstructor @@ -20,7 +19,6 @@ public class ReadingPreference { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // ✅ 유저와 1:1 관계 (유저 삭제 시 preference도 자동 삭제) @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.REMOVE) @JoinColumn(name = "user_id", unique = true, nullable = false) private User user; @@ -40,7 +38,7 @@ public class ReadingPreference { @ElementCollection @CollectionTable(name = "preference_selection_criteria", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "criteria") - private List selectionCriteria; + private List mood; @ElementCollection @CollectionTable(name = "preference_reading_habits", joinColumns = @JoinColumn(name = "preference_id")) @@ -62,29 +60,6 @@ public class ReadingPreference { @Column(name = "trend") private List recommendedTrends; - // ---- 팩토리 메서드 ---- - public static ReadingPreference from(CreateReq req, User user) { - ReadingPreference pref = new ReadingPreference(); - pref.user = user; - pref.mbti = req.mbti(); - pref.favoriteAuthors = req.favoriteAuthors(); - pref.favoriteBooks = req.favoriteBooks(); - pref.selectionCriteria = req.selectionCriteria(); - pref.readingHabits = req.readingHabits(); - pref.preferredGenres = req.preferredGenres(); - pref.keywords = req.keywords(); - pref.recommendedTrends = req.recommendedTrends(); - return pref; } - public void apply(UpdateReq req) { - this.mbti = req.mbti(); - this.favoriteAuthors = req.favoriteAuthors(); - this.favoriteBooks = req.favoriteBooks(); - this.selectionCriteria = req.selectionCriteria(); - this.readingHabits = req.readingHabits(); - this.preferredGenres = req.preferredGenres(); - this.keywords = req.keywords(); - this.recommendedTrends = req.recommendedTrends(); - } -} + diff --git a/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java similarity index 60% rename from src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java index f9743d2..66a1aac 100644 --- a/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.preference.repository; +package BookPick.mvp.domain.ReadingPreference.repository; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import org.springframework.data.jpa.repository.JpaRepository; public interface ReadingPreferenceRepository extends JpaRepository { diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java new file mode 100644 index 0000000..7237e9b --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -0,0 +1,23 @@ +package BookPick.mvp.domain.ReadingPreference.service; + +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceDtos.*; +import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReadingPreferenceService { + private final ReadingPreferenceRepository readingPreferenceRepository ; + private final UserRepository userRepository; + + @Transactional + public ReadingPreferenceRegisterRes create(Long userId, ReadingPreferenceRegisterReq req) { + + return new ReadingPreferenceRegisterRes(1L); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 902264a..7092371 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -4,6 +4,7 @@ import BookPick.mvp.domain.auth.Roles; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -72,10 +73,22 @@ public CustomUserDetails( this.profileImageUrl = user.getProfileImageUrl(); } - public CustomUserDetails{ + public CustomUserDetails( + Long userId, + String email, + Collection authorities + ){ + super(email, "", authorities); + } + static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities){ + return new CustomUserDetails(userId, email, authorities); } + + + + } } diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java deleted file mode 100644 index 0f6b33e..0000000 --- a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java +++ /dev/null @@ -1,28 +0,0 @@ -package BookPick.mvp.domain.preference.controller; - -import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.domain.preference.dto.ReadingPreference.*; -import BookPick.mvp.domain.preference.service.ReadingPreferenceService; -import BookPick.mvp.global.api.SuccessCode; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - - -@RestController -@RequestMapping("/api/v1/users/{id}/preferences") -@RequiredArgsConstructor -public class ReadingPreferenceController { - - private final ReadingPreferenceService readingPreferenceService; - - @PostMapping - public ResponseEntity> create(@PathVariable("id") Long userId, @Valid @RequestBody CreateReq req) { - PreferenceRes res = readingPreferenceService.create(userId, req); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); - } -} diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java deleted file mode 100644 index 8206a37..0000000 --- a/src/main/java/BookPick/mvp/domain/preference/dto/ReadingPreference.java +++ /dev/null @@ -1,75 +0,0 @@ -package BookPick.mvp.domain.preference.dto; - - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -import java.util.List; - -public class ReadingPreference { - - - // 1. 취향 설정 - - // Req - public record CreateReq( - @NotBlank String mbti, - @NotNull List favoriteAuthors, // 좋아하는 작가 - @NotNull List favoriteBooks, // 좋아하는 작가 - @NotNull List selectionCriteria, // 독서 선호 분위기 - @NotNull List readingHabits, // 독서 습관 - @NotNull List preferredGenres, // 선호 장르 - @NotNull List keywords, // 키워드 - @NotNull List recommendedTrends // - ) { - } - - - // 2. 취향 수정 - public record UpdateReq( - @NotBlank String mbti, - @NotNull List favoriteAuthors, - @NotNull List favoriteBooks, - @NotNull List selectionCriteria, - @NotNull List readingHabits, - @NotNull List preferredGenres, - @NotNull List keywords, - @NotNull List recommendedTrends - ) { - } - - - // PreferenceRes (MVP용 단순화) - public record PreferenceRes( - Long id, - String mbti, - List favoriteAuthors, - List favoriteBooks, - List selectionCriteria, - List readingHabits, - List preferredGenres, - List keywords, - List recommendedTrends - ) { - public static PreferenceRes from(BookPick.mvp.domain.preference.entity.ReadingPreference p) { - return new PreferenceRes( - p.getId(), - p.getMbti(), - p.getFavoriteAuthors(), - p.getFavoriteBooks(), - p.getSelectionCriteria(), - p.getReadingHabits(), - p.getPreferredGenres(), - p.getKeywords(), - p.getRecommendedTrends() - ); - } - - } - - -} - - - - diff --git a/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java deleted file mode 100644 index bde1e31..0000000 --- a/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java +++ /dev/null @@ -1,40 +0,0 @@ -package BookPick.mvp.domain.preference.service; - -import BookPick.mvp.domain.preference.dto.ReadingPreference.*; -import BookPick.mvp.domain.preference.entity.ReadingPreference; -import BookPick.mvp.domain.preference.repository.ReadingPreferenceRepository; -import BookPick.mvp.domain.user.entity.User; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class ReadingPreferenceService { - private final ReadingPreferenceRepository preferenceRepository; - - @Transactional - public PreferenceRes create(Long userId, CreateReq req) { - User user = User.builder() - .id(userId) - .build(); - - if (preferenceRepository.existsByUserId(userId)) { - throw new IllegalStateException("이미 등록된 선호정보가 있습니다."); - } - - // --- 문자열 요청 → 엔티티 변환 --- - List authors = req.favoriteAuthors(); // ["1", "2", "3"] - List books = req.favoriteBooks(); // ["1", "2", "3"] - - // --- ReadingPreference 생성 --- - ReadingPreference pref = ReadingPreference.from(req, user); - pref.setFavoriteAuthors(authors); - pref.setFavoriteBooks(books); - - ReadingPreference saved = preferenceRepository.save(pref); - return PreferenceRes.from(saved); - } -} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java b/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java deleted file mode 100644 index 39a1784..0000000 --- a/src/main/java/BookPick/mvp/domain/user/dto/UserDtos.java +++ /dev/null @@ -1,19 +0,0 @@ -package BookPick.mvp.domain.user.dto; - -import jakarta.validation.constraints.*; - - -public class UserDtos { - -// public record Res( -// Long id, -// String nickName, -// String email -// ){} -// -// // 사용처 : 회원 가입, -// public record CreateReq( -// @Email @NotBlank String email, -// @Size(min=8, max = 72 ) @NotBlank String password -// ){} -} diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java index 10057ed..1892989 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -22,8 +22,10 @@ public enum ErrorCode { // -- Comment -- - COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글 찾을 수 없습니다."); //404 + COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글 찾을 수 없습니다."), //404 + // -- Reading Preference -- + READING_PREFERENCE_ALREADY_RESiGSTER(HttpStatus.CONFLICT, "이미 독서 취향이 존재합니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index 01f3603..24c7538 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -34,7 +34,10 @@ public enum SuccessCode { COMMENT_DELETE_SUCCESS(HttpStatus.OK, "댓글을 성공적으로 삭제하였습니다."), // -- Book -- - BOOK_LIST_READ_SUCCESS(HttpStatus.OK, "책 목록을 성공적으로 조회하였습니다."); + BOOK_LIST_READ_SUCCESS(HttpStatus.OK, "책 목록을 성공적으로 조회하였습니다."), + + // -- Reading Preference -- + READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."); From 8438811f9d11bd0f44c17822cf62d52e617aae33 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 18 Oct 2025 17:53:54 +0900 Subject: [PATCH 042/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=20?= =?UTF-8?q?=EC=B7=A8=ED=96=A5=20=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...yRegisteredReadingPreferenceException.java | 2 +- .../dto/ReadingPreferenceDtos.java | 24 +++++++------- .../entity/ReadingPreference.java | 22 +++++-------- .../service/ReadingPreferenceService.java | 32 +++++++++++++++++-- .../auth/service/MyUserDetailsService.java | 1 + .../BookPick/mvp/global/config/JwtFilter.java | 8 +++-- .../exception/GlobalExceptionHandler.java | 8 ----- src/main/resources/application.yml | 2 +- 8 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java index a51b157..6cc825d 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.readingPreferenceRepository.Exception; +package BookPick.mvp.domain.ReadingPreference.Exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java index 1652083..e110eb3 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.ReadingPreference.dto; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.user.entity.User; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -12,25 +13,26 @@ public class ReadingPreferenceDtos { // -- register -- public record ReadingPreferenceRegisterReq( - @NotBlank String mbti, + String mbti, // @NotNull List favoriteAuthors, // 좋아하는 작가 - @NotNull List favoriteBooks, // 좋아하는 책 - @NotNull List mood, // 독서 선호 분위기 - @NotNull List readingHabits, // 독서 습관 - @NotNull List preferredGenres, // 선호 장르 - @NotNull List keywords, // 키워드 - @NotNull List recommendedTrends // + List favoriteBooks, // 좋아하는 책 + List moods, // 독서 선호 분위기 + List readingHabits, // 독서 습관 + List genres, // 선호 장르 + List keywords, // 키워드 + List trends // ) { } + public record ReadingPreferenceRegisterRes( Long readingPreferenceId - ){ - public ReadingPreferenceRegisterRes from(User user){ - return new ReadingPreferenceRegisterRes(user.getId()); + ) { + static public ReadingPreferenceRegisterRes from(ReadingPreference readingPreference) { + return new ReadingPreferenceRegisterRes(readingPreference.getId()); } } - } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 0551264..3e33b66 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -2,22 +2,21 @@ import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import java.util.List; @Entity @Table(name = "user_reading_preference") @Getter -@Setter +@Builder @NoArgsConstructor +@AllArgsConstructor public class ReadingPreference { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private Long Id; @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.REMOVE) @JoinColumn(name = "user_id", unique = true, nullable = false) @@ -25,20 +24,15 @@ public class ReadingPreference { private String mbti; - @ElementCollection - @CollectionTable(name = "preference_favorite_authors", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "author") - private List favoriteAuthors; - @ElementCollection @CollectionTable(name = "preference_favorite_books", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "book") private List favoriteBooks; @ElementCollection - @CollectionTable(name = "preference_selection_criteria", joinColumns = @JoinColumn(name = "preference_id")) + @CollectionTable(name = "preference_moods", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "criteria") - private List mood; + private List moods; @ElementCollection @CollectionTable(name = "preference_reading_habits", joinColumns = @JoinColumn(name = "preference_id")) @@ -48,7 +42,7 @@ public class ReadingPreference { @ElementCollection @CollectionTable(name = "preference_genres", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "genre") - private List preferredGenres; + private List genres; @ElementCollection @CollectionTable(name = "preference_keywords", joinColumns = @JoinColumn(name = "preference_id")) @@ -58,7 +52,7 @@ public class ReadingPreference { @ElementCollection @CollectionTable(name = "preference_trends", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "trend") - private List recommendedTrends; + private List trends; } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 7237e9b..a611601 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -1,7 +1,11 @@ package BookPick.mvp.domain.ReadingPreference.service; +import BookPick.mvp.domain.ReadingPreference.Exception.AlreadyRegisteredReadingPreferenceException; import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceDtos.*; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,12 +16,34 @@ @Service @RequiredArgsConstructor public class ReadingPreferenceService { - private final ReadingPreferenceRepository readingPreferenceRepository ; + private final ReadingPreferenceRepository readingPreferenceRepository; private final UserRepository userRepository; @Transactional - public ReadingPreferenceRegisterRes create(Long userId, ReadingPreferenceRegisterReq req) { + public ReadingPreferenceRegisterRes create(Long userId, ReadingPreferenceRegisterReq req) { - return new ReadingPreferenceRegisterRes(1L); + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + if(readingPreferenceRepository.existsByUserId(userId)){ + throw new AlreadyRegisteredReadingPreferenceException(); + } + + + ReadingPreference readingPreference = ReadingPreference.builder() + .user(user) + .mbti(req.mbti()) + .favoriteBooks(req.favoriteBooks()) + .moods(req.moods()) + .readingHabits(req.readingHabits()) + .genres(req.genres()) + .trends(req.trends()) + .keywords(req.keywords()) + .build(); + + ReadingPreference saved = readingPreferenceRepository.save(readingPreference); + + + return ReadingPreferenceRegisterRes.from(saved); } } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 7092371..66e506f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -79,6 +79,7 @@ public CustomUserDetails( Collection authorities ){ super(email, "", authorities); + this.id=userId; } static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities){ diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 80cc9d2..1342d90 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -48,15 +48,19 @@ protected void doFilterInternal( try { Claims claims = JwtUtil.extractToken(token); + Long userId = claims.get("userId", Number.class).longValue(); + String email = claims.get("email").toString(); + + var authorities = Arrays.stream( claims.get("authorities").toString().split(",") ).map(SimpleGrantedAuthority::new).toList(); - CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(claims.get("userId"), claims.get("email"), claims.get("authorities")); + CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(userId, email, authorities); var auth = new UsernamePasswordAuthenticationToken( - customUserDetails, null, customUserDetails.getAuthorities() + customUserDetails, null, customUserDetails.getAuthorities() ); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index 3bef20a..34546b4 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -38,13 +38,5 @@ public ResponseEntity> handleValidationException(MethodArgumen } - @ExceptionHandler(DuplicateResourceException.class) - public ResponseEntity> handleDuplicateResource(DuplicateResourceException e) { - ErrorCode errorCode = ErrorCode.DUPLICATE_EMAIL; - return ResponseEntity - .status(errorCode.getStatus()) // 409 - .body(ApiResponse.error(errorCode)); // code : DUPLICATE_EMAIL, message : 이미 존재하는 이메일입니다 - } - } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 51c17fc..9911464 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: jpa: hibernate: - ddl-auto: none + ddl-auto: update properties: hibernate: show_sql: false From 042a120efbf7ab6f60a698966cd13deaed232716 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 19 Oct 2025 20:30:08 +0900 Subject: [PATCH 043/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EB=93=B1=EB=A1=9D=ED=95=9C=20=EB=8F=85=EC=84=9C=20?= =?UTF-8?q?=EC=B7=A8=ED=96=A5=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserReadingPreferenceNotExisted.java | 10 +++ .../ReadingPreferenceController.java | 47 ++++++++++++-- .../Create/ReadingPreferenceCreateReq.java | 14 ++++ .../Create/ReadingPreferenceCreateRes.java | 19 ++++++ .../Delete/ReadingPreferenceDeleteRes.java | 16 +++++ .../dto/Get/ReadingPreferenceGetReq.java | 7 ++ .../dto/Get/ReadingPreferenceGetRes.java | 33 ++++++++++ .../dto/ReadingPreferenceDtos.java | 39 ----------- .../Update/ReadingPreferenceUpdateReq.java | 15 +++++ .../Update/ReadingPreferenceUpdateRes.java | 30 +++++++++ .../entity/ReadingPreference.java | 13 +++- .../ReadingPreferenceRepository.java | 3 + .../service/ReadingPreferenceService.java | 64 +++++++++++++++++-- .../auth/controller/AuthController.java | 2 +- .../auth/dto/{ => Create}/AuthDtos.java | 2 +- .../exception/InvalidTokenTypeException.java | 10 +++ .../exception/JwtTokenExpiredException.java | 10 +++ .../mvp/domain/auth/service/AuthService.java | 4 +- .../BookPick/mvp/global/api/ErrorCode.java | 7 +- .../BookPick/mvp/global/api/SuccessCode.java | 5 +- .../BookPick/mvp/global/config/JwtFilter.java | 6 +- .../BookPick/mvp/global/util/JwtUtil.java | 14 +++- src/main/resources/application.yml | 2 +- 23 files changed, 309 insertions(+), 63 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java rename src/main/java/BookPick/mvp/domain/auth/dto/{ => Create}/AuthDtos.java (97%) create mode 100644 src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java new file mode 100644 index 0000000..541e17e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.ReadingPreference.Exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class UserReadingPreferenceNotExisted extends BusinessException { + public UserReadingPreferenceNotExisted(){ + super(ErrorCode.READING_PREFERENCE_NOT_EXISTED); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java index 8837d5a..0fda0de 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -1,6 +1,13 @@ package BookPick.mvp.domain.ReadingPreference.controller; -import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceDtos.*; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes.*; +import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetReq; +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.api.ApiResponse; @@ -21,11 +28,43 @@ public class ReadingPreferenceController { private final ReadingPreferenceService readingPreferenceService; @PostMapping - public ResponseEntity> registerReadingPreference(@Valid @RequestBody ReadingPreferenceRegisterReq req, - @AuthenticationPrincipal CustomUserDetails user) { - ReadingPreferenceRegisterRes res = readingPreferenceService.create(user.getId(), req); + public ResponseEntity> create(@Valid @RequestBody ReadingPreferenceCreateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser) { + ReadingPreferenceCreateRes res = readingPreferenceService.addReadingPreference(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_REGISTER_SUCCESS, res)); } + + @GetMapping + public ResponseEntity> getDetails( + @AuthenticationPrincipal CustomUserDetails currentUser) { + ReadingPreferenceGetRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_READ_SUCCESS, res)); + } + + @PatchMapping + public ResponseEntity> update(@Valid @RequestBody ReadingPreferenceUpdateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser) { + ReadingPreferenceUpdateRes res = readingPreferenceService.modifyReadingPreference(currentUser.getId(), req); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res)); + + } + + @DeleteMapping + public ResponseEntity> delete( + @AuthenticationPrincipal CustomUserDetails currentUser) { + ReadingPreferenceDeleteRes res = readingPreferenceService.removeReadingPreference(currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_DELETE_SUCCESS, res)); + + } + } + + diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java new file mode 100644 index 0000000..48fe4ef --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java @@ -0,0 +1,14 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Create; + +import java.util.List; + +public record ReadingPreferenceCreateReq( + String mbti, + List favoriteBooks, // 좋아하는 책 + List moods, // 독서 선호 분위기 + List readingHabits, // 독서 습관 + List genres, // 선호 장르 + List keywords, // 키워드 + List trends // +) { +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java new file mode 100644 index 0000000..123d943 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Create; + + +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; + +import java.util.List; + + public record ReadingPreferenceCreateRes( + Long readingPreferenceId + ) { + static public ReadingPreferenceCreateRes from(ReadingPreference readingPreference) { + return new ReadingPreferenceCreateRes(readingPreference.getId()); + } + } + + + + + diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java new file mode 100644 index 0000000..bbba609 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java @@ -0,0 +1,16 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Delete; + +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; + +import java.time.LocalDateTime; +import java.util.List; + +public record ReadingPreferenceDeleteRes( + Long preferenceId, +// boolean deleted, // 추후 soft 삭제 시 사용 + LocalDateTime deletedAt +) { + static public ReadingPreferenceDeleteRes from(Long preferenceId, LocalDateTime deletedAt){ + return new ReadingPreferenceDeleteRes(preferenceId, deletedAt); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java new file mode 100644 index 0000000..d6bd7b8 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java @@ -0,0 +1,7 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Get; + +// -- 독서 취향 조회 요청 -- +public record ReadingPreferenceGetReq () +{ + +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java new file mode 100644 index 0000000..b1e1ada --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Get; + +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; + +import java.util.List; + +// -- 독서 취향 조회 응답 -- +public record ReadingPreferenceGetRes( + Long preferenceId, + String mbti, + List favoriteBooks, // 좋아하는 책 + List moods, // 독서 선호 분위기 + List readingHabits, // 독서 습관 + List genres, // 선호 장르 + List keywords, // 키워드 + List trends +){ + static public ReadingPreferenceGetRes from(ReadingPreference rp){ + return new ReadingPreferenceGetRes( + rp.getId(), + rp.getMbti(), + rp.getFavoriteBooks(), + rp.getMoods(), + rp.getReadingHabits(), + rp.getGenres(), + rp.getKeywords(), + rp.getTrends() + ); + } +} + + + diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java deleted file mode 100644 index e110eb3..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceDtos.java +++ /dev/null @@ -1,39 +0,0 @@ -package BookPick.mvp.domain.ReadingPreference.dto; - - -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; -import BookPick.mvp.domain.user.entity.User; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -import java.util.List; - -public class ReadingPreferenceDtos { - - - // -- register -- - public record ReadingPreferenceRegisterReq( - String mbti, -// @NotNull List favoriteAuthors, // 좋아하는 작가 - List favoriteBooks, // 좋아하는 책 - List moods, // 독서 선호 분위기 - List readingHabits, // 독서 습관 - List genres, // 선호 장르 - List keywords, // 키워드 - List trends // - ) { - } - - public record ReadingPreferenceRegisterRes( - Long readingPreferenceId - ) { - static public ReadingPreferenceRegisterRes from(ReadingPreference readingPreference) { - return new ReadingPreferenceRegisterRes(readingPreference.getId()); - } - } - -} - - - - diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java new file mode 100644 index 0000000..39fa2a8 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java @@ -0,0 +1,15 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Update; + +import java.util.List; + +public record ReadingPreferenceUpdateReq( + Long preferenceId, + String mbti, + List favoriteBooks, // 좋아하는 책 + List moods, // 독서 선호 분위기 + List readingHabits, // 독서 습관 + List genres, // 선호 장르 + List keywords, // 키워드 + List trends +) { +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java new file mode 100644 index 0000000..a38ef67 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java @@ -0,0 +1,30 @@ +package BookPick.mvp.domain.ReadingPreference.dto.Update; + +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; + +import java.util.List; + +public record ReadingPreferenceUpdateRes( + Long preferenceId, + String mbti, + List favoriteBooks, // 좋아하는 책 + List moods, // 독서 선호 분위기 + List readingHabits, // 독서 습관 + List genres, // 선호 장르 + List keywords, // 키워드 + List trends +) { + static public ReadingPreferenceUpdateRes from(ReadingPreference rp){ + return new ReadingPreferenceUpdateRes( + rp.getId(), + rp.getMbti(), + rp.getFavoriteBooks(), + rp.getMoods(), + rp.getReadingHabits(), + rp.getGenres(), + rp.getKeywords(), + rp.getTrends() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 3e33b66..4a5bd7c 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.ReadingPreference.entity; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; @@ -18,7 +19,7 @@ public class ReadingPreference { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long Id; - @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.REMOVE) + @OneToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_id", unique = true, nullable = false) private User user; @@ -54,6 +55,16 @@ public class ReadingPreference { @Column(name = "trend") private List trends; + public void update(ReadingPreferenceUpdateReq req) { + if (req.mbti() != null) this.mbti = req.mbti(); + if (req.favoriteBooks() != null) this.favoriteBooks = req.favoriteBooks(); + if (req.moods() != null) this.moods = req.moods(); + if (req.readingHabits() != null) this.readingHabits = req.readingHabits(); + if (req.genres() != null) this.genres = req.genres(); + if (req.keywords() != null) this.keywords = req.keywords(); + if (req.trends() != null) this.trends = req.trends(); + } + } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java index 66a1aac..92d9029 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java @@ -3,6 +3,9 @@ import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ReadingPreferenceRepository extends JpaRepository { boolean existsByUserId(Long userId); + Optional findByUserId(Long userId); } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index a611601..17d68d5 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -1,17 +1,25 @@ package BookPick.mvp.domain.ReadingPreference.service; import BookPick.mvp.domain.ReadingPreference.Exception.AlreadyRegisteredReadingPreferenceException; -import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceDtos.*; +import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; +import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetReq; +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; +import jakarta.persistence.Table; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; +import java.time.LocalDateTime; @Service @RequiredArgsConstructor @@ -19,13 +27,14 @@ public class ReadingPreferenceService { private final ReadingPreferenceRepository readingPreferenceRepository; private final UserRepository userRepository; + // -- 유저 독서 취향 등록 -- @Transactional - public ReadingPreferenceRegisterRes create(Long userId, ReadingPreferenceRegisterReq req) { + public ReadingPreferenceCreateRes addReadingPreference(Long userId, ReadingPreferenceCreateReq req) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); - if(readingPreferenceRepository.existsByUserId(userId)){ + if (readingPreferenceRepository.existsByUserId(userId)) { throw new AlreadyRegisteredReadingPreferenceException(); } @@ -44,6 +53,51 @@ public ReadingPreferenceRegisterRes create(Long userId, ReadingPreferenceRegiste ReadingPreference saved = readingPreferenceRepository.save(readingPreference); - return ReadingPreferenceRegisterRes.from(saved); + return ReadingPreferenceCreateRes.from(saved); } + + + // -- 유저 독서 취향 단건 조회 -- + @Transactional + public ReadingPreferenceGetRes findReadingPreference(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + ReadingPreference result = readingPreferenceRepository.findByUserId(userId) + .orElseThrow(UserReadingPreferenceNotExisted::new); + + return ReadingPreferenceGetRes.from(result); + } + + // -- 본인 유저 독서 수정 -- + @Transactional + public ReadingPreferenceUpdateRes modifyReadingPreference(Long userId, ReadingPreferenceUpdateReq req) { + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + ReadingPreference preference = readingPreferenceRepository.findByUserId(userId) + .orElseThrow(UserReadingPreferenceNotExisted::new); + + preference.update(req); + + return ReadingPreferenceUpdateRes.from(preference); + } + + // -- 본인 유저 독서 삭제 -- + @Transactional + public ReadingPreferenceDeleteRes removeReadingPreference(Long userId) { + + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + ReadingPreference preference = readingPreferenceRepository.findByUserId(userId) + .orElseThrow(UserReadingPreferenceNotExisted::new); + + readingPreferenceRepository.delete(preference); + + + return ReadingPreferenceDeleteRes.from(preference.getId(), LocalDateTime.now()); + + } + } diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 3bd3df4..9765e8f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.auth.controller; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.domain.auth.dto.AuthDtos.*; +import BookPick.mvp.domain.auth.dto.Create.AuthDtos.*; import BookPick.mvp.domain.auth.service.AuthService; import BookPick.mvp.global.api.SuccessCode; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java similarity index 97% rename from src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java rename to src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java index 31ba75d..464065f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.auth.dto; +package BookPick.mvp.domain.auth.dto.Create; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java new file mode 100644 index 0000000..1162378 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.auth.exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class InvalidTokenTypeException extends BusinessException { + public InvalidTokenTypeException(){ + super(ErrorCode.Invalid_Token_Type); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java b/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java new file mode 100644 index 0000000..5b3b02d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.auth.exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class JwtTokenExpiredException extends BusinessException { + public JwtTokenExpiredException(){ + super(ErrorCode.Token_Expired); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index 9a5da8b..d485b6a 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -1,14 +1,12 @@ package BookPick.mvp.domain.auth.service; import BookPick.mvp.domain.auth.Roles; -import BookPick.mvp.domain.auth.dto.AuthDtos.*; +import BookPick.mvp.domain.auth.dto.Create.AuthDtos.*; import BookPick.mvp.domain.auth.exception.DuplicateEmailException; import BookPick.mvp.domain.auth.exception.InvalidLoginException; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; -import BookPick.mvp.global.api.ErrorCode; -import BookPick.mvp.global.exception.custom.DuplicateResourceException; import BookPick.mvp.global.util.JwtUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java index 1892989..a7e0ad9 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -13,6 +13,10 @@ public enum ErrorCode { DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일 입니다."), // 409 회원 가입 AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘 못 되었습니다."), // 401 로그인 + Token_Expired(HttpStatus.UNAUTHORIZED, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), + Invalid_Token_Type(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), + + // -- User -- User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 @@ -25,7 +29,8 @@ public enum ErrorCode { COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글 찾을 수 없습니다."), //404 // -- Reading Preference -- - READING_PREFERENCE_ALREADY_RESiGSTER(HttpStatus.CONFLICT, "이미 독서 취향이 존재합니다."); + READING_PREFERENCE_ALREADY_RESiGSTER(HttpStatus.CONFLICT, "이미 독서 취향이 존재합니다."), + READING_PREFERENCE_NOT_EXISTED(HttpStatus.NOT_FOUND, "사용자의 독서 취향이 존재하지 않습니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index 24c7538..ce2f7cb 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -37,7 +37,10 @@ public enum SuccessCode { BOOK_LIST_READ_SUCCESS(HttpStatus.OK, "책 목록을 성공적으로 조회하였습니다."), // -- Reading Preference -- - READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."); + READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."), + READING_PREFERENCE_READ_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 조회하였습니다."), + READING_PREFERENCE_UPDATE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 수정하였습니다."), + READING_PREFERENCE_DELETE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 삭제하였습니다."); diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 1342d90..5e537a6 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,5 +1,6 @@ package BookPick.mvp.global.config; +import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.util.JwtUtil; import io.jsonwebtoken.Claims; @@ -45,7 +46,6 @@ protected void doFilterInternal( return; } - try { Claims claims = JwtUtil.extractToken(token); Long userId = claims.get("userId", Number.class).longValue(); @@ -66,10 +66,6 @@ protected void doFilterInternal( SecurityContextHolder.getContext().setAuthentication(auth); - } catch (ExpiredJwtException e) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } filterChain.doFilter(request, response); } diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 943710f..7dcc9cf 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -1,6 +1,10 @@ package BookPick.mvp.global.util; +import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; +import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import org.springframework.security.core.Authentication; @@ -65,9 +69,17 @@ public static String createRefreshToken(Authentication auth) { //3. JWT 오픈 public static Claims extractToken(String token) { - Claims claims = Jwts.parser().verifyWith(key).build() + + try{ + Claims claims = Jwts.parser().verifyWith(key).build() .parseSignedClaims(token).getPayload(); return claims; + } catch (ExpiredJwtException e) { + throw new JwtTokenExpiredException(); + } catch (JwtException | IllegalArgumentException e) { + throw new InvalidTokenTypeException(); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9911464..51c17fc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: none properties: hibernate: show_sql: false From 1552069f99710b5b463708de4d58c88c8800554a Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 19 Oct 2025 20:37:39 +0900 Subject: [PATCH 044/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=A4=91..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controller/CurationController.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java diff --git a/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java new file mode 100644 index 0000000..086a8b7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java @@ -0,0 +1,57 @@ +package BookPick.mvp.domain.curation.Controller; + +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode; +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 org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/curations") +public class CurationController { + + @PostMapping + public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + CurationCreateRes res = curationRepository.create(currentUser.getId(), req); + + return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); + + } + + @PostMapping + public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + CurationCreateRes res = curationRepository.create(currentUser.getId(), req); + + return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); + + } + + @PostMapping + public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + CurationCreateRes res = curationRepository.create(currentUser.getId(), req); + + return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); + + } + + @PostMapping + public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + CurationCreateRes res = curationRepository.create(currentUser.getId(), req); + + return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); + + } + + @PostMapping + public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + CurationCreateRes res = curationRepository.create(currentUser.getId(), req); + + return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); + + } +} From d0e660a32a94061beda4ba7e3862d086abb7c638 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 20 Oct 2025 10:02:58 +0900 Subject: [PATCH 045/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=A4=91..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/curation/Controller/CurationController.java | 7 +++++++ .../mvp/domain/curation/dto/create/CurationCreateReq.java | 4 ++++ .../mvp/domain/curation/dto/create/CurationCreateRes.java | 4 ++++ src/main/java/BookPick/mvp/domain/user/entity/User.java | 1 + .../java/BookPick/mvp/domain/user/service/UserService.java | 3 --- 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java diff --git a/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java index 086a8b7..8ac3cc2 100644 --- a/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java @@ -15,6 +15,8 @@ @RequestMapping("/api/v1/curations") public class CurationController { + + // -- 큐레이션 생성 -- @PostMapping public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ CurationCreateRes res = curationRepository.create(currentUser.getId(), req); @@ -23,6 +25,8 @@ public ResponseEntity> create(@Valid @RequestBody } + // -- 큐레이션 단건 조회 -- + @PostMapping public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ CurationCreateRes res = curationRepository.create(currentUser.getId(), req); @@ -31,6 +35,7 @@ public ResponseEntity> create(@Valid @RequestBody } + // -- 큐레이션 리스트 조회 -- @PostMapping public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ CurationCreateRes res = curationRepository.create(currentUser.getId(), req); @@ -39,6 +44,7 @@ public ResponseEntity> create(@Valid @RequestBody } + // -- 큐레이션 수정 -- @PostMapping public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ CurationCreateRes res = curationRepository.create(currentUser.getId(), req); @@ -47,6 +53,7 @@ public ResponseEntity> create(@Valid @RequestBody } + // -- 큐레이션 삭제 -- @PostMapping public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ CurationCreateRes res = curationRepository.create(currentUser.getId(), req); diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java new file mode 100644 index 0000000..4b644c3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.create; + +public class CurationCreateReq { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java new file mode 100644 index 0000000..97fc0af --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.create; + +public class CurationCreateRes { +} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index fb9ec4e..b9987d4 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -49,6 +49,7 @@ public class User { private String profileImageUrl; // 프로필 사진 경로 @Column(name ="is_first_login", nullable = false) + @Builder.Default private boolean isFirstLogin = true; @CreationTimestamp diff --git a/src/main/java/BookPick/mvp/domain/user/service/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/UserService.java index 2cf7469..be70d57 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/UserService.java @@ -1,15 +1,12 @@ package BookPick.mvp.domain.user.service; -import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.repository.UserRepository; -import BookPick.mvp.domain.user.dto.UserDtos.*; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.format.DateTimeFormatter; -import java.time.ZoneOffset; @Service @Transactional From af779cfc6671056ea208f60f58553e9e629d5d1b Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 20 Oct 2025 10:05:04 +0900 Subject: [PATCH 046/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=A4=91..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/user/entity/User.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index fb9ec4e..b9987d4 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -49,6 +49,7 @@ public class User { private String profileImageUrl; // 프로필 사진 경로 @Column(name ="is_first_login", nullable = false) + @Builder.Default private boolean isFirstLogin = true; @CreationTimestamp From 8e36908471b52f2eff499388a26ae4265592750c Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 22 Oct 2025 15:23:17 +0900 Subject: [PATCH 047/291] chore: meaningless commit --- .../dto/create/CurationCreateReq.java | 51 +++++++++++++++++++ .../dto/create/CurationCreateRes.java | 4 ++ .../dto/delete/CurationDeleteReq.java | 4 ++ .../dto/delete/CurationDeleteRes.java | 4 ++ .../curation/dto/read/CurationGetReq.java | 4 ++ .../curation/dto/read/CurationGetRes.java | 4 ++ .../dto/update/CurationUpdateReq.java | 4 ++ .../dto/update/CurationUpdateRes.java | 4 ++ .../BookPick/mvp/global/config/JwtConfig.java | 40 +++++++++++++++ src/main/resources/application.yml | 5 +- 10 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java create mode 100644 src/main/java/BookPick/mvp/global/config/JwtConfig.java diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java new file mode 100644 index 0000000..0b544a5 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java @@ -0,0 +1,51 @@ +package BookPick.mvp.domain.curation.dto.create; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +public record CurationCreateReq( + Thumbnail thumbnail, + CustomBook customBook, + String review, + Recommend recommend +) { +} + +@AllArgsConstructor +@Getter +@Setter +class Thumbnail{ + private String imageUrl; + private String imageColor; +} + +@AllArgsConstructor +@Getter +@Setter +class CustomBook { + String title; + String author; + String isbn; +} + +class Recommend{ +} +//book": { +// "title": "노르웨이의 숲", +// "author": "무라카미 하루키, +// "ISBN" : "123213" +// }, +// +// "review": "외로움과 청춘의 쓸쓸함이 절묘하게 녹아든 작품이에요. 비오는 날 읽으면 감정선이 더 깊어져요.", +// +// +// "recommend" : { +// "mood": ["비오는 날", "퇴근 후", "밤늦게"], +// "genres": ["소설", "에세이"], +// "keywords": ["사랑", "고독", "성장"], +// "style": ["한 번에 몰입해서 읽는 편", "독서 후 감상을 기록하는 편"], +// } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java new file mode 100644 index 0000000..97fc0af --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.create; + +public class CurationCreateRes { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java new file mode 100644 index 0000000..7bf7cb5 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.delete; + +public class CurationDeleteReq { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java new file mode 100644 index 0000000..b576901 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.delete; + +public class CurationDeleteRes { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java new file mode 100644 index 0000000..ce38198 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.read; + +public class CurationGetReq { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java new file mode 100644 index 0000000..66dc219 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.read; + +public class CurationGetRes { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java new file mode 100644 index 0000000..df8912e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.update; + +public class CurationUpdateReq { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java new file mode 100644 index 0000000..72a5a8b --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.update; + +public class CurationUpdateRes { +} diff --git a/src/main/java/BookPick/mvp/global/config/JwtConfig.java b/src/main/java/BookPick/mvp/global/config/JwtConfig.java new file mode 100644 index 0000000..8cf1959 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/JwtConfig.java @@ -0,0 +1,40 @@ +package BookPick.mvp.global.config; + +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +import javax.crypto.SecretKey; + +@Configuration +@Getter +public class JwtConfig { + + @Value("${jwt.access.secret") + private static String accessSecret; + @Value("${jwt.access.expiration") + private long accessSecretExp; + + @Value("${jwt.refresh.secret") + private String refreshSecret; + @Value("${jwt.refresh.expiration") + private long refreshSecretExp; + + + SecretKey getSecretKey(String secret) { + return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); + } + + public SecretKey getAccessSecretKey() { + return getSecretKey(this.accessSecret); + } + + public SecretKey getRefreshSecretKey() { + return getSecretKey(this.refreshSecret); + } + + + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 51c17fc..dc4f5a2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,12 +32,11 @@ logging: BookPick: DEBUG jwt: - secret: yourSuperSecretKeyHere1234567890 access: - secret: accesssecret123accesssecret123accesssecret123accesssecret123 + secret: aGVsbG93b3JsZGhlbGxvd29ybGRoZWxsb3dvcmxk expiration: 15m refresh: - secret: refreshsecret123refreshsecret123refreshsecret123refreshsecret123 + secret: cmVmcmVzaHNlY3JldGtleWZvcmp3dHRva2Vu expiration: 7d api : From 62e0e5a4e6af45da067bce5c42e40d3a62288107 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 22 Oct 2025 20:25:46 +0900 Subject: [PATCH 048/291] =?UTF-8?q?chore=20:=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/controller/AuthController.java | 2 +- .../BookPick/mvp/domain/auth/dto/Create/AuthDtos.java | 4 ++-- .../BookPick/mvp/domain/auth/service/AuthService.java | 4 ++-- .../BookPick/mvp/domain/user/service/UserService.java | 1 - src/main/java/BookPick/mvp/global/config/JwtConfig.java | 8 ++++---- .../java/BookPick/mvp/global/config/SecurityConfig.java | 2 +- src/main/resources/application.yml | 4 ++-- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 9765e8f..6a190fe 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/auth") +@RequestMapping("/api/v1") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java index 464065f..1f114ed 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java @@ -11,7 +11,7 @@ public class AuthDtos { // -- SignUp -- public record SignReq( @NotBlank @Email String email, - @Size(min = 8, max = 72) String password + @Size(min = 8, max = 72) String passWord ) { } @@ -26,7 +26,7 @@ public static SignRes from(long userId) { // -- Login -- public record LoginReq( @NotBlank @Email String email, - @Size(min = 8, max = 72) String password + @Size(min = 8, max = 72) String passWord ) { } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index d485b6a..0589b30 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -44,7 +44,7 @@ public SignRes signUp(SignReq req) { // 2. 신규 유저 생성 User user = User.builder() .email(req.email()) - .password(passwordEncoder.encode(req.password())) + .password(passwordEncoder.encode(req.passWord())) .role(Roles.ROLE_USER) .build(); @@ -59,7 +59,7 @@ public SignRes signUp(SignReq req) { // access Token O, refresh X @Transactional public LoginRes login(LoginReq req, HttpServletResponse res) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.passWord()); // 임시로 이메일과 아이디가 담김 try { Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 diff --git a/src/main/java/BookPick/mvp/domain/user/service/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/UserService.java index 2cf7469..fae7367 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/UserService.java @@ -2,7 +2,6 @@ import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.repository.UserRepository; -import BookPick.mvp.domain.user.dto.UserDtos.*; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/global/config/JwtConfig.java b/src/main/java/BookPick/mvp/global/config/JwtConfig.java index 8cf1959..12d4c25 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtConfig.java +++ b/src/main/java/BookPick/mvp/global/config/JwtConfig.java @@ -12,14 +12,14 @@ @Getter public class JwtConfig { - @Value("${jwt.access.secret") + @Value("${jwt.access.secret}") private static String accessSecret; - @Value("${jwt.access.expiration") + @Value("${jwt.access.expiration}") private long accessSecretExp; - @Value("${jwt.refresh.secret") + @Value("${jwt.refresh.secret}") private String refreshSecret; - @Value("${jwt.refresh.expiration") + @Value("${jwt.refresh.expiration}") private long refreshSecretExp; diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 580349a..396dbd3 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -37,7 +37,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/signup", "/api/v1/auth/login", "/api/v1/auth/logout").permitAll() + .requestMatchers("/api/v1/signup", "/api/v1/login", "/api/v1/logout").permitAll() .requestMatchers("/api/v1/users/*/preferences").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() .requestMatchers("/error").permitAll() diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dc4f5a2..7f0fbf6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -34,10 +34,10 @@ logging: jwt: access: secret: aGVsbG93b3JsZGhlbGxvd29ybGRoZWxsb3dvcmxk - expiration: 15m + expiration: 900000 # 15분 = 15 * 60 * 1000 refresh: secret: cmVmcmVzaHNlY3JldGtleWZvcmp3dHRva2Vu - expiration: 7d + expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 api : kakao : From 3e2f74f1f2b9d7ea1421ab90882495447c31998e Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 23 Oct 2025 19:13:21 +0900 Subject: [PATCH 049/291] chore: meaningless commit --- .../mvp/domain/auth/service/AuthService.java | 17 +++++++++-------- .../auth/service/MyUserDetailsService.java | 4 ++++ .../BookPick/mvp/global/config/JwtFilter.java | 8 ++++---- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index 0589b30..c96299f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -62,14 +62,15 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.passWord()); // 임시로 이메일과 아이디가 담김 try { + // getObject : AuthenticationManager 객체 반환 + // .authenticate : Authenticate를 상속한 구현체의 인스턴스를 검증한다. Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 - firstLoginHandle(req.email()); + firstLoginCheck(req.email()); String accessToken = JwtUtil.createAccessToken(auth); // Access O String refreshToken = JwtUtil.createRefreshToken(auth); // Refresh X - res.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); MyUserDetailsService.CustomUserDetails customUserDetails = (MyUserDetailsService.CustomUserDetails) auth.getPrincipal(); @@ -83,13 +84,13 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { } @Transactional - void firstLoginHandle(String email){ - User user = userRepository.findByEmail(email) - .orElseThrow(UserNotFoundException::new); + void firstLoginCheck(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(UserNotFoundException::new); - if(user.isFirstLogin()){ - user.isNotFirstLogin(); - } + if (user.isFirstLogin()) { + user.isNotFirstLogin(); // 더티체킹 + } } } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 66e506f..4ec8790 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -36,14 +36,18 @@ public class MyUserDetailsService implements UserDetailsService { public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { List auth = new ArrayList<>(); + + // 유저 찾고 BookPick.mvp.domain.user.entity.User user = userRepository.findByEmail(email) .orElseThrow(UserNotFoundException::new); + // 역할 부여하고 if (user.getRole().equals(Roles.ROLE_USER)) { auth.add(new SimpleGrantedAuthority(Roles.ROLE_USER.name())); } + // 프로바이더가 토큰으로 받은 비밀번호와 비교할 DB에서 조회한 User객체 반환 -> 해당 유저 객체의 비밀번호를 프로바이더가 사용한다. CustomUserDetails customUserDetails = new CustomUserDetails(user, auth); // email, passWord, authorities 등록 customUserDetails.setId(user.getId()); customUserDetails.setNickname(user.getNickname()); diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 5e537a6..150aa43 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -40,13 +40,13 @@ protected void doFilterInternal( return; } - String token = resolveAccessToken(request); + String token = resolveAccessToken(request); // 토큰있는지 문자열 체크 if (token == null) { // 토큰 없으면 그냥 통과 - filterChain.doFilter(request, response); + filterChain.doFilter(request, response); // 넘어가요 return; } - Claims claims = JwtUtil.extractToken(token); + Claims claims = JwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) Long userId = claims.get("userId", Number.class).longValue(); String email = claims.get("email").toString(); @@ -62,7 +62,7 @@ protected void doFilterInternal( var auth = new UsernamePasswordAuthenticationToken( customUserDetails, null, customUserDetails.getAuthorities() ); - auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 추가 정보 저장 (어디서 접속했는지) SecurityContextHolder.getContext().setAuthentication(auth); From 7426d29dffb62ec5bfcf8ff0ab14730970a08ea4 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 23 Oct 2025 21:49:42 +0900 Subject: [PATCH 050/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=93=B1=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/BookPickApplication.java | 3 + .../entity/ReadingPreference.java | 2 +- .../Controller/CurationController.java | 64 ------------- .../controller/CurationController.java | 37 +++++++ .../converter/StringListConverter.java | 33 +++++++ .../dto/create/CurationCreateReq.java | 20 +++- .../dto/create/CurationCreateRes.java | 9 +- .../curation/dto/create/Req/BookDto.java | 8 ++ .../curation/dto/create/Req/RecommendDto.java | 9 ++ .../curation/dto/create/Req/ThumbnailDto.java | 7 ++ .../mvp/domain/curation/entity/Curation.java | 96 +++++++++++++++++++ .../repository/CurationRepository.java | 10 ++ .../curation/service/CurationService.java | 38 ++++++++ .../BookPick/mvp/global/api/SuccessCode.java | 6 +- 14 files changed, 273 insertions(+), 69 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/entity/Curation.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/CurationService.java diff --git a/src/main/java/BookPick/mvp/BookPickApplication.java b/src/main/java/BookPick/mvp/BookPickApplication.java index a2d36ea..fd8c264 100644 --- a/src/main/java/BookPick/mvp/BookPickApplication.java +++ b/src/main/java/BookPick/mvp/BookPickApplication.java @@ -2,8 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing // ← 이거 있어? + public class BookPickApplication { public static void main(String[] args) { diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 4a5bd7c..c213a1a 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -32,7 +32,7 @@ public class ReadingPreference { @ElementCollection @CollectionTable(name = "preference_moods", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "criteria") + @Column(name = "moods") private List moods; @ElementCollection diff --git a/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java deleted file mode 100644 index 8ac3cc2..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/Controller/CurationController.java +++ /dev/null @@ -1,64 +0,0 @@ -package BookPick.mvp.domain.curation.Controller; - -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; -import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode; -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 org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/curations") -public class CurationController { - - - // -- 큐레이션 생성 -- - @PostMapping - public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ - CurationCreateRes res = curationRepository.create(currentUser.getId(), req); - - return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); - - } - - // -- 큐레이션 단건 조회 -- - - @PostMapping - public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ - CurationCreateRes res = curationRepository.create(currentUser.getId(), req); - - return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); - - } - - // -- 큐레이션 리스트 조회 -- - @PostMapping - public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ - CurationCreateRes res = curationRepository.create(currentUser.getId(), req); - - return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); - - } - - // -- 큐레이션 수정 -- - @PostMapping - public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ - CurationCreateRes res = curationRepository.create(currentUser.getId(), req); - - return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); - - } - - // -- 큐레이션 삭제 -- - @PostMapping - public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ - CurationCreateRes res = curationRepository.create(currentUser.getId(), req); - - return ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res); - - } -} diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java new file mode 100644 index 0000000..8128971 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -0,0 +1,37 @@ +package BookPick.mvp.domain.curation.controller; + +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; +import BookPick.mvp.domain.curation.service.CurationService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +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 org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/curations") +@RequiredArgsConstructor +public class CurationController { + + private final CurationService curationService; + + // -- 큐레이션 생성 -- + @PostMapping + public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { + CurationCreateRes res = curationService.create(currentUser.getId(), req); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); + + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java b/src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java new file mode 100644 index 0000000..99d2dfd --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java @@ -0,0 +1,33 @@ +// StringListConverter.java +package BookPick.mvp.domain.curation.converter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.util.List; + +@Converter +public class StringListConverter implements AttributeConverter, String> { + + private final ObjectMapper mapper = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List attribute) { + try { + return mapper.writeValueAsString(attribute); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + try { + return mapper.readValue(dbData, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java index 4b644c3..f9e97f1 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java @@ -1,4 +1,20 @@ package BookPick.mvp.domain.curation.dto.create; -public class CurationCreateReq { -} +import BookPick.mvp.domain.curation.dto.create.Req.BookDto; +import BookPick.mvp.domain.curation.dto.create.Req.RecommendDto; +import BookPick.mvp.domain.curation.dto.create.Req.ThumbnailDto; + +import java.util.List; + +// 메인 요청 DTO +public record CurationCreateReq( + ThumbnailDto thumbnail, + BookDto book, + String review, + RecommendDto recommend +) {} + + + + + diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java index 97fc0af..7e2cb47 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java @@ -1,4 +1,11 @@ package BookPick.mvp.domain.curation.dto.create; -public class CurationCreateRes { +import BookPick.mvp.domain.curation.entity.Curation; + +public record CurationCreateRes( + Long id +) { + public static CurationCreateRes from(Curation curation){ + return new CurationCreateRes(curation.getId()); + } } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java new file mode 100644 index 0000000..9307164 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java @@ -0,0 +1,8 @@ +package BookPick.mvp.domain.curation.dto.create.Req; + +// 책 정보 +public record BookDto( + String title, + String author, + String isbn +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java new file mode 100644 index 0000000..8bdbf38 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java @@ -0,0 +1,9 @@ +package BookPick.mvp.domain.curation.dto.create.Req; + +import java.util.List; +public record RecommendDto( + List moods, + List genres, + List keywords, + List styles +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java new file mode 100644 index 0000000..9303b80 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java @@ -0,0 +1,7 @@ +package BookPick.mvp.domain.curation.dto.create.Req; + +// 썸네일 정보 +public record ThumbnailDto( + String imageUrl, + String imageColor +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java new file mode 100644 index 0000000..de2bc39 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -0,0 +1,96 @@ +package BookPick.mvp.domain.curation.entity; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) +@Table(name = "curation") +public class Curation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private Long userId; + + private String thumbnailUrl; + private String thumbnailColor; + + @Column(nullable = false) + private String bookTitle; + private String bookAuthor; + private String bookIsbn; + + @Column(columnDefinition = "TEXT") + private String review; + + // 매핑 테이블로 변경! + @ElementCollection + @CollectionTable(name = "curation_moods", joinColumns = @JoinColumn(name = "curation_id")) + @Column(name = "mood") + private List moods; + + @ElementCollection + @CollectionTable(name = "curation_genres", joinColumns = @JoinColumn(name = "curation_id")) + @Column(name = "genre") + private List genres; + + @ElementCollection + @CollectionTable(name = "curation_keywords", joinColumns = @JoinColumn(name = "curation_id")) + @Column(name = "keyword") + private List keywords; + + @ElementCollection + @CollectionTable(name = "curation_styles", joinColumns = @JoinColumn(name = "curation_id")) + @Column(name = "style") + private List styles; + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + private LocalDateTime deletedAt; + + @Builder + public Curation(Long userId, String thumbnailUrl, String thumbnailColor, + String bookTitle, String bookAuthor, String bookIsbn, + String review, List moods, List genres, + List keywords, List styles) { + this.userId = userId; + this.thumbnailUrl = thumbnailUrl; + this.thumbnailColor = thumbnailColor; + this.bookTitle = bookTitle; + this.bookAuthor = bookAuthor; + this.bookIsbn = bookIsbn; + this.review = review; + this.moods = moods; + this.genres = genres; + this.keywords = keywords; + this.styles = styles; + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java new file mode 100644 index 0000000..3a06ed9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -0,0 +1,10 @@ +// CurationRepository.java +package BookPick.mvp.domain.curation.repository; + +import BookPick.mvp.domain.curation.entity.Curation; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface CurationRepository extends JpaRepository { + List findByUserId(Long userId); +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java new file mode 100644 index 0000000..fcc4c80 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -0,0 +1,38 @@ +// CurationService.java +package BookPick.mvp.domain.curation.service; + +import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CurationService { + + private final CurationRepository curationRepository; + + @Transactional + public CurationCreateRes create(Long userId, CurationCreateReq req) { + Curation curation = Curation.builder() + .userId(userId) + .thumbnailUrl(req.thumbnail().imageUrl()) + .thumbnailColor(req.thumbnail().imageColor()) + .bookTitle(req.book().title()) + .bookAuthor(req.book().author()) + .bookIsbn(req.book().isbn()) + .review(req.review()) + .moods(req.recommend().moods()) + .genres(req.recommend().genres()) + .keywords(req.recommend().keywords()) + .styles(req.recommend().styles()) + .build(); + + Curation saved = curationRepository.save(curation); + + return CurationCreateRes.from(saved); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index ce2f7cb..0abfb55 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -40,7 +40,11 @@ public enum SuccessCode { READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."), READING_PREFERENCE_READ_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 조회하였습니다."), READING_PREFERENCE_UPDATE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 수정하였습니다."), - READING_PREFERENCE_DELETE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 삭제하였습니다."); + READING_PREFERENCE_DELETE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 삭제하였습니다."), + + // -- curation -- + CURATION_REGISTER_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."); + From c316bb7e1ce229102d9fd978d984354caace33b9 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 23 Oct 2025 22:18:01 +0900 Subject: [PATCH 051/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=8B=A8=EA=B1=B4=EC=A1=B0=ED=9A=8C,=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 2 +- .../controller/CurationController.java | 46 +++++++++++++++--- .../dto/delete/CurationDeleteRes.java | 13 +++++ .../curation/dto/get/CurationGetRes.java | 36 ++++++++++++++ .../dto/update/CurationUpdateReq.java | 13 +++++ .../dto/update/CurationUpdateRes.java | 12 +++++ .../mvp/domain/curation/entity/Curation.java | 14 ++++++ .../CurationAccessDeniedException.java | 11 +++++ .../exception/CurationNotFoundException.java | 11 +++++ .../curation/service/CurationService.java | 48 +++++++++++++++++++ .../BookPick/mvp/global/api/ErrorCode.java | 9 ++-- .../BookPick/mvp/global/api/SuccessCode.java | 29 +++-------- .../mvp/global/config/SecurityConfig.java | 4 +- 13 files changed, 214 insertions(+), 34 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 9765e8f..6a190fe 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/auth") +@RequestMapping("/api/v1") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index 8128971..4716c24 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -1,8 +1,13 @@ +// CurationController.java에 추가 package BookPick.mvp.domain.curation.controller; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.get.CurationGetRes; +import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.CurationService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; @@ -11,10 +16,7 @@ import org.springframework.http.HttpStatus; 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 org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/curations") @@ -25,13 +27,45 @@ public class CurationController { // -- 큐레이션 생성 -- @PostMapping - public ResponseEntity> create(@Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { + public ResponseEntity> create( + @Valid @RequestBody CurationCreateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser) { CurationCreateRes res = curationService.create(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); + } + + // -- 큐레이션 단건 조회 -- + @GetMapping("/{curationId}") + public ResponseEntity> getCuration( + @PathVariable Long curationId) { + CurationGetRes res = curationService.findCuration(curationId); + + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); + } + + // -- 큐레이션 수정 -- + @PatchMapping("/{curationId}") + public ResponseEntity> updateCuration( + @PathVariable Long curationId, + @Valid @RequestBody CurationUpdateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser) { + CurationUpdateRes res = curationService.modifyCuration(currentUser.getId(), curationId, req); + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); } + // -- 큐레이션 삭제 -- + @DeleteMapping("/{curationId}") + public ResponseEntity> deleteCuration( + @PathVariable Long curationId, + @AuthenticationPrincipal CustomUserDetails currentUser) { + CurationDeleteRes res = curationService.removeCuration(currentUser.getId(), curationId); -} + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java new file mode 100644 index 0000000..aba93c1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java @@ -0,0 +1,13 @@ +// CurationDeleteRes.java +package BookPick.mvp.domain.curation.dto.delete; + +import java.time.LocalDateTime; + +public record CurationDeleteRes( + Long id, + LocalDateTime deletedAt +) { + public static CurationDeleteRes from(Long id, LocalDateTime deletedAt) { + return new CurationDeleteRes(id, deletedAt); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java new file mode 100644 index 0000000..1b4004f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java @@ -0,0 +1,36 @@ +// CurationGetRes.java +package BookPick.mvp.domain.curation.dto.get; + +import BookPick.mvp.domain.curation.entity.Curation; +import java.time.LocalDateTime; +import java.util.List; + +public record CurationGetRes( + Long id, + Long userId, + ThumbnailInfo thumbnail, + BookInfo book, + String review, + RecommendInfo recommend, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static CurationGetRes from(Curation curation) { + return new CurationGetRes( + curation.getId(), + curation.getUserId(), + new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), + new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), + curation.getReview(), + new RecommendInfo(curation.getMoods(), curation.getGenres(), + curation.getKeywords(), curation.getStyles()), + curation.getCreatedAt(), + curation.getUpdatedAt() + ); + } + + public record ThumbnailInfo(String imageUrl, String imageColor) {} + public record BookInfo(String title, String author, String isbn) {} + public record RecommendInfo(List moods, List genres, + List keywords, List styles) {} +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java new file mode 100644 index 0000000..4d68e18 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java @@ -0,0 +1,13 @@ +// CurationUpdateReq.java +package BookPick.mvp.domain.curation.dto.update; + +import BookPick.mvp.domain.curation.dto.create.Req.BookDto; +import BookPick.mvp.domain.curation.dto.create.Req.RecommendDto; +import BookPick.mvp.domain.curation.dto.create.Req.ThumbnailDto; + +public record CurationUpdateReq( + ThumbnailDto thumbnail, + BookDto book, + String review, + RecommendDto recommend +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java new file mode 100644 index 0000000..e0f0bc8 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java @@ -0,0 +1,12 @@ +// CurationUpdateRes.java +package BookPick.mvp.domain.curation.dto.update; + +import BookPick.mvp.domain.curation.entity.Curation; + +public record CurationUpdateRes( + Long id +) { + public static CurationUpdateRes from(Curation curation) { + return new CurationUpdateRes(curation.getId()); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index de2bc39..3bcf842 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.curation.entity; +import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; import jakarta.persistence.CollectionTable; import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; @@ -93,4 +94,17 @@ public Curation(Long userId, String thumbnailUrl, String thumbnailColor, this.keywords = keywords; this.styles = styles; } + + public void update(CurationUpdateReq req) { + this.thumbnailUrl = req.thumbnail().imageUrl(); + this.thumbnailColor = req.thumbnail().imageColor(); + this.bookTitle = req.book().title(); + this.bookAuthor = req.book().author(); + this.bookIsbn = req.book().isbn(); + this.review = req.review(); + this.moods = req.recommend().moods(); + this.genres = req.recommend().genres(); + this.keywords = req.recommend().keywords(); + this.styles = req.recommend().styles(); + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java b/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java new file mode 100644 index 0000000..f44fe16 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java @@ -0,0 +1,11 @@ +// CurationAccessDeniedException.java +package BookPick.mvp.domain.curation.exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CurationAccessDeniedException extends BusinessException { + public CurationAccessDeniedException() { + super(ErrorCode.CURATION_ACCESS_DENIED); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java b/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java new file mode 100644 index 0000000..ed95673 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java @@ -0,0 +1,11 @@ +// CurationNotFoundException.java +package BookPick.mvp.domain.curation.exception; + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CurationNotFoundException extends BusinessException { + public CurationNotFoundException() { + super(ErrorCode.CURATION_NOT_FOUND); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index fcc4c80..4cdecd9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -3,18 +3,27 @@ import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.get.CurationGetRes; +import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + @Service @RequiredArgsConstructor public class CurationService { private final CurationRepository curationRepository; + // -- 큐레이션 등록 -- @Transactional public CurationCreateRes create(Long userId, CurationCreateReq req) { Curation curation = Curation.builder() @@ -35,4 +44,43 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { return CurationCreateRes.from(saved); } + + // -- 큐레이션 단건 조회 -- + @Transactional(readOnly = true) + public CurationGetRes findCuration(Long curationId) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + return CurationGetRes.from(curation); + } + + // -- 큐레이션 수정 -- + @Transactional + public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUpdateReq req) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + if (!curation.getUserId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + + curation.update(req); + + return CurationUpdateRes.from(curation); + } + + // -- 큐레이션 삭제 -- + @Transactional + public CurationDeleteRes removeCuration(Long userId, Long curationId) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + if (!curation.getUserId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + + curationRepository.delete(curation); + + return CurationDeleteRes.from(curation.getId(), LocalDateTime.now()); + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java index a7e0ad9..442d203 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -30,9 +30,12 @@ public enum ErrorCode { // -- Reading Preference -- READING_PREFERENCE_ALREADY_RESiGSTER(HttpStatus.CONFLICT, "이미 독서 취향이 존재합니다."), - READING_PREFERENCE_NOT_EXISTED(HttpStatus.NOT_FOUND, "사용자의 독서 취향이 존재하지 않습니다."); + READING_PREFERENCE_NOT_EXISTED(HttpStatus.NOT_FOUND, "사용자의 독서 취향이 존재하지 않습니다."), + + // -- Curation -- + CURATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), + CURATION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "큐레이션 접근 권한이 없습니다."); private final HttpStatus status; private final String message; -} - +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index 0abfb55..7027a2a 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -1,11 +1,9 @@ package BookPick.mvp.global.api; - import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; - @AllArgsConstructor @Getter public enum SuccessCode { @@ -17,14 +15,6 @@ public enum SuccessCode { // -- User -- GET_USERS_SUCCESS(HttpStatus.OK, "사용자 목록 조회를 성공하였습니다."), - // -- Quration -- - CURATION_CREATE_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."), - CURATION_LIST_READ_SUCCESS(HttpStatus.OK, "큐레이션 목록을 성공적으로 조회하였습니다."), - CURATION_DETAIL_READ_SUCCESS(HttpStatus.OK, "큐레이션을 성공하였습니다."), - CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."), - CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."), - - // -- Comment -- COMMENT_CREATE_SUCCESS(HttpStatus.CREATED, "댓글을 성공적으로 등록하였습니다."), COMMENT_LIST_READ_SUCCESS(HttpStatus.OK, "댓글 목록을 성공적으로 조회하였습니다."), @@ -42,17 +32,12 @@ public enum SuccessCode { READING_PREFERENCE_UPDATE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 수정하였습니다."), READING_PREFERENCE_DELETE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 삭제하였습니다."), - // -- curation -- - CURATION_REGISTER_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."); - - - + // -- Curation -- + CURATION_REGISTER_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."), + CURATION_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 조회하였습니다."), + CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."), + CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."); private final HttpStatus status; - private final String message; // 상태 설명 (사용자 친화적) -} - - - - - + private final String message; +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 580349a..56359d9 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -37,14 +37,14 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/v1/auth/signup", "/api/v1/auth/login", "/api/v1/auth/logout").permitAll() + .requestMatchers("/api/v1/signup", "/api/v1/login", "/api/v1/logout").permitAll() .requestMatchers("/api/v1/users/*/preferences").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() .requestMatchers("/error").permitAll() .anyRequest().authenticated() ) .logout(logout -> logout - .logoutUrl("/api/v1/auth/logout") + .logoutUrl("/api/v1/logout") .clearAuthentication(true) .invalidateHttpSession(true) .logoutSuccessHandler((req, res, auth) -> { From fc865405551444f6ad457959a114a207fff9f734 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 24 Oct 2025 20:29:39 +0900 Subject: [PATCH 052/291] chore: meaningless commit --- .../BookPick/mvp/domain/curation/SortType.java | 13 +++++++++++++ .../curation/controller/CurationController.java | 14 ++++++++++++++ .../exception/CurationAccessDeniedException.java | 2 +- .../exception/CurationNotFoundException.java | 2 +- .../domain/curation/service/CurationService.java | 4 ++-- 5 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/SortType.java rename src/main/java/BookPick/mvp/domain/curation/{ => converter}/exception/CurationAccessDeniedException.java (84%) rename src/main/java/BookPick/mvp/domain/curation/{ => converter}/exception/CurationNotFoundException.java (83%) diff --git a/src/main/java/BookPick/mvp/domain/curation/SortType.java b/src/main/java/BookPick/mvp/domain/curation/SortType.java new file mode 100644 index 0000000..0ddbbca --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/SortType.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.curation; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum SortType { + + SORT_LATEST("latest"); + + private final String value; +} diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index 4716c24..a688aae 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -2,6 +2,7 @@ package BookPick.mvp.domain.curation.controller; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.curation.SortType; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.get.CurationGetRes; @@ -12,6 +13,7 @@ import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; import jakarta.validation.Valid; +import jakarta.websocket.server.PathParam; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -46,6 +48,18 @@ public ResponseEntity> getCuration( .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); } + + // -- 큐레이션 리스트 조회 최신순 -- + // /sort=latest&page=0&size=20") + @GetMapping + public ResponseEntity> getCurationList(@RequestParam String sort, @RequestParam String size) { + if(sort.equals(SortType.SORT_LATEST.getValue())){ + curationService.getCuratoinList(SortType); + + } + } + + // -- 큐레이션 수정 -- @PatchMapping("/{curationId}") public ResponseEntity> updateCuration( diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java b/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationAccessDeniedException.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java rename to src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationAccessDeniedException.java index f44fe16..218b172 100644 --- a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java +++ b/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationAccessDeniedException.java @@ -1,5 +1,5 @@ // CurationAccessDeniedException.java -package BookPick.mvp.domain.curation.exception; +package BookPick.mvp.domain.curation.converter.exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java b/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationNotFoundException.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java rename to src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationNotFoundException.java index ed95673..f90896c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationNotFoundException.java @@ -1,5 +1,5 @@ // CurationNotFoundException.java -package BookPick.mvp.domain.curation.exception; +package BookPick.mvp.domain.curation.converter.exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index 4cdecd9..65f3df8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -8,8 +8,8 @@ import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; -import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.converter.exception.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; From 58f4e3db6632338a37b5b0c0de069ffbca439c1f Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 25 Oct 2025 16:51:57 +0900 Subject: [PATCH 053/291] chore: meaningless commit --- .../controller/CurationController.java | 31 +++-- .../dto/create/CurationCreateReq.java | 1 - .../domain/curation/dto/get/list/BookRes.java | 6 + .../dto/get/list/CurationContentRes.java | 39 ++++++ .../dto/get/list/CurationListGetRes.java | 11 ++ .../curation/dto/get/list/ThumbnailRes.java | 7 ++ .../dto/get/{ => one}/CurationGetRes.java | 4 +- .../mvp/domain/curation/entity/Curation.java | 43 ++++--- .../repository/CurationRepository.java | 23 +++- .../curation/service/CurationService.java | 117 +++++++++++++++--- .../BookPick/mvp/global/api/SuccessCode.java | 3 +- .../mvp/global/config/SecurityConfig.java | 1 + 12 files changed, 240 insertions(+), 46 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java rename src/main/java/BookPick/mvp/domain/curation/dto/get/{ => one}/CurationGetRes.java (93%) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index a688aae..56c02d8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -5,15 +5,16 @@ import BookPick.mvp.domain.curation.SortType; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.get.CurationGetRes; +import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; +import BookPick.mvp.domain.curation.dto.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.CurationService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; -import jakarta.websocket.server.PathParam; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -41,25 +42,35 @@ public ResponseEntity> create( // -- 큐레이션 단건 조회 -- @GetMapping("/{curationId}") public ResponseEntity> getCuration( - @PathVariable Long curationId) { - CurationGetRes res = curationService.findCuration(curationId); + @PathVariable Long curationId, + HttpServletRequest req) { + CurationGetRes res = curationService.findCuration(curationId, req); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); } - // -- 큐레이션 리스트 조회 최신순 -- - // /sort=latest&page=0&size=20") + // CurationController.java + // GET /api/v1/curations + // GET /api/v1/curations?sort=popular + // GET /api/v1/curations?sort=latest&cursor=300&size=10 + @GetMapping - public ResponseEntity> getCurationList(@RequestParam String sort, @RequestParam String size) { - if(sort.equals(SortType.SORT_LATEST.getValue())){ - curationService.getCuratoinList(SortType); + public ResponseEntity> getCurationList( + @RequestParam(defaultValue = "latest") String sort, + @RequestParam(required = false) Long cursor, + @RequestParam(defaultValue = "10") int size) { + + CurationListGetRes curationListGetRes = curationService.getCurationList(sort, cursor, size); - } + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); } + + // -- 큐레이션 수정 -- @PatchMapping("/{curationId}") public ResponseEntity> updateCuration( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java index f9e97f1..c65ab8f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java @@ -17,4 +17,3 @@ public record CurationCreateReq( - diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java new file mode 100644 index 0000000..d4a08c1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java @@ -0,0 +1,6 @@ +package BookPick.mvp.domain.curation.dto.get.list; + +public record BookRes( + String title, + String author +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java new file mode 100644 index 0000000..f0ea824 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java @@ -0,0 +1,39 @@ +package BookPick.mvp.domain.curation.dto.get.list; + +import BookPick.mvp.domain.curation.entity.Curation; + +public record CurationContentRes( + Long curationId, + String title, + Long userId, + String nickName, + ThumbnailRes thumbnail, + String summary, + BookRes book, + int likeCount, + int commentCount, + int viewCount, + Double similarity, + String matched, + Double popularityScore, + String createdAt +) { + public static CurationContentRes from(Curation curation) { + return new CurationContentRes( + curation.getId(), + curation.getBookTitle(), + curation.getUser().getId(), + "닉네임", // TODO: User 조인 필요 + new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), + curation.getReview(), + new BookRes(curation.getBookTitle(), curation.getBookAuthor()), + 0, // TODO: 좋아요 수 + 0, // TODO: 댓글 수 + 0, // TODO: 조회수 + null, + null, + null, // TODO: popularityScore 계산 + curation.getCreatedAt().toString() + ); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java new file mode 100644 index 0000000..c2996a1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.curation.dto.get.list; + +import java.util.List; + +public record CurationListGetRes( + String sortType, + String description, + List content, + boolean hasNext, + Long nextCursor +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java new file mode 100644 index 0000000..ca24434 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java @@ -0,0 +1,7 @@ +package BookPick.mvp.domain.curation.dto.get.list; + + +public record ThumbnailRes( + String imageUrl, + String imageColor +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java similarity index 93% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java index 1b4004f..d374e73 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java @@ -1,5 +1,5 @@ // CurationGetRes.java -package BookPick.mvp.domain.curation.dto.get; +package BookPick.mvp.domain.curation.dto.get.one; import BookPick.mvp.domain.curation.entity.Curation; import java.time.LocalDateTime; @@ -18,7 +18,7 @@ public record CurationGetRes( public static CurationGetRes from(Curation curation) { return new CurationGetRes( curation.getId(), - curation.getUserId(), + curation.getUser().getId(), new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getReview(), diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 3bcf842..27c4163 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -1,16 +1,8 @@ package BookPick.mvp.domain.curation.entity; import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; -import jakarta.persistence.CollectionTable; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.Table; +import BookPick.mvp.domain.user.entity.User; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -33,8 +25,9 @@ public class Curation { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) - private Long userId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; private String thumbnailUrl; private String thumbnailColor; @@ -68,6 +61,19 @@ public class Curation { @Column(name = "style") private List styles; + @Column(name = "like_count") + private Integer likeCount = 0; + + @Column(name = "view_count") + private Integer viewCount = 0; + + @Column(name = "comment_count") + private Integer commentCount = 0; + + @Column(name = "popularity_score") + private Integer popularityScore = 0; + + @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; @@ -77,12 +83,13 @@ public class Curation { private LocalDateTime deletedAt; + @Builder - public Curation(Long userId, String thumbnailUrl, String thumbnailColor, + public Curation(User user, String thumbnailUrl, String thumbnailColor, String bookTitle, String bookAuthor, String bookIsbn, String review, List moods, List genres, List keywords, List styles) { - this.userId = userId; + this.user = user; this.thumbnailUrl = thumbnailUrl; this.thumbnailColor = thumbnailColor; this.bookTitle = bookTitle; @@ -107,4 +114,12 @@ public void update(CurationUpdateReq req) { this.keywords = req.recommend().keywords(); this.styles = req.recommend().styles(); } + + public void increaseViewCount() { + this.viewCount++; + updatePopularityScore(); // 인기도 재계산 + } + public void updatePopularityScore() { + this.popularityScore = (likeCount * 3) + (viewCount * 2) + (commentCount * 1); + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 3a06ed9..afa8d94 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -1,10 +1,31 @@ -// CurationRepository.java package BookPick.mvp.domain.curation.repository; import BookPick.mvp.domain.curation.entity.Curation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + import java.util.List; public interface CurationRepository extends JpaRepository { + List findByUserId(Long userId); + + // 첫 요청 - User JOIN (정렬 조건은 Pageable로 받음) + @Query("SELECT c FROM Curation c JOIN FETCH c.user") + Page findAllWithUser(Pageable pageable); + + // 커서 기반 조회 - 최신순 + @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id < :cursor ORDER BY c.createdAt DESC, c.id DESC") + List findByIdLessThanWithUserLatest(@Param("cursor") Long cursor, Pageable pageable); + + // 커서 기반 조회 - 인기순 + @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id < :cursor ORDER BY c.popularityScore DESC, c.id DESC") + List findByIdLessThanWithUserPopular(@Param("cursor") Long cursor, Pageable pageable); + + // 커서 기반 조회 - 조회순 + @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id < :cursor ORDER BY c.viewCount DESC, c.id DESC") + List findByIdLessThanWithUserViews(@Param("cursor") Long cursor, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index 65f3df8..3dc4408 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -3,7 +3,9 @@ import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.get.CurationGetRes; +import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; +import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; +import BookPick.mvp.domain.curation.dto.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; @@ -11,34 +13,48 @@ import BookPick.mvp.domain.curation.converter.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; @Service @RequiredArgsConstructor public class CurationService { private final CurationRepository curationRepository; + private final UserRepository userRepository; // -- 큐레이션 등록 -- @Transactional public CurationCreateRes create(Long userId, CurationCreateReq req) { + + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + Curation curation = Curation.builder() - .userId(userId) - .thumbnailUrl(req.thumbnail().imageUrl()) - .thumbnailColor(req.thumbnail().imageColor()) - .bookTitle(req.book().title()) - .bookAuthor(req.book().author()) - .bookIsbn(req.book().isbn()) - .review(req.review()) - .moods(req.recommend().moods()) - .genres(req.recommend().genres()) - .keywords(req.recommend().keywords()) - .styles(req.recommend().styles()) - .build(); + .user(user) + .thumbnailUrl(req.thumbnail().imageUrl()) + .thumbnailColor(req.thumbnail().imageColor()) + .bookTitle(req.book().title()) + .bookAuthor(req.book().author()) + .bookIsbn(req.book().isbn()) + .review(req.review()) + .moods(req.recommend().moods()) + .genres(req.recommend().genres()) + .keywords(req.recommend().keywords()) + .styles(req.recommend().styles()) + .build(); Curation saved = curationRepository.save(curation); @@ -46,21 +62,88 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { } // -- 큐레이션 단건 조회 -- - @Transactional(readOnly = true) - public CurationGetRes findCuration(Long curationId) { + @Transactional + public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); + curation.increaseViewCount(); + return CurationGetRes.from(curation); } + + // -- 큐레이션 목록 조회 -- + @Transactional(readOnly = true) + public CurationListGetRes getCurationList(String sort, Long cursor, int size) { + // 커서 기반 조회 (size + 1개 조회) + List curations; + if (cursor == null) { + // 첫 요청 - User JOIN + PageRequest pageRequest = PageRequest.of(0, size + 1, getSortCondition(sort)); + curations = curationRepository.findAllWithUser(pageRequest).getContent(); + } else { + // 커서 이후 데이터 조회 - User JOIN + PageRequest pageRequest = PageRequest.of(0, size + 1); + curations = switch (sort) { + case "latest" -> curationRepository.findByIdLessThanWithUserLatest(cursor, pageRequest); + case "popular" -> curationRepository.findByIdLessThanWithUserPopular(cursor, pageRequest); + case "views" -> curationRepository.findByIdLessThanWithUserViews(cursor, pageRequest); + default -> curationRepository.findByIdLessThanWithUserLatest(cursor, pageRequest); + }; + } + + // hasNext 판단 + boolean hasNext = curations.size() > size; + if (hasNext) { + curations = curations.subList(0, size); + } + + // Entity -> DTO 변환 + List content = curations.stream() + .map(CurationContentRes::from) + .toList(); + + // nextCursor 계산 + Long nextCursor = hasNext && !content.isEmpty() + ? content.get(content.size() - 1).curationId() + : null; + + return new CurationListGetRes( + sort, + getSortDescription(sort), + content, + hasNext, + nextCursor + ); + } + + private Sort getSortCondition(String sort) { + return switch (sort) { + case "latest" -> Sort.by(Sort.Direction.DESC, "createdAt"); + case "popular" -> Sort.by(Sort.Direction.DESC, "popularityScore", "id"); + case "views" -> Sort.by(Sort.Direction.DESC, "viewCount", "id"); + default -> Sort.by(Sort.Direction.DESC, "createdAt"); + }; + } + + private String getSortDescription(String sort) { + return switch (sort) { + case "latest" -> "최신 작성순"; + case "popular" -> "인기순"; + case "views" -> "조회순"; + default -> "최신 작성순"; + }; + } + + // -- 큐레이션 수정 -- @Transactional public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUpdateReq req) { Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); - if (!curation.getUserId().equals(userId)) { + if (!curation.getUser().getId().equals(userId)) { throw new CurationAccessDeniedException(); } @@ -75,7 +158,7 @@ public CurationDeleteRes removeCuration(Long userId, Long curationId) { Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); - if (!curation.getUserId().equals(userId)) { + if (!curation.getUser().getId().equals(userId)) { throw new CurationAccessDeniedException(); } diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index 7027a2a..9fcb7e2 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -34,7 +34,8 @@ public enum SuccessCode { // -- Curation -- CURATION_REGISTER_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."), - CURATION_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 조회하였습니다."), + CURATION_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 단건 조회하였습니다."), + CURATION_LIST_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 리스트 조회하였습니다."), CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."), CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."); diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 56359d9..947ce95 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -37,6 +37,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .requestMatchers("/**").permitAll() .requestMatchers("/api/v1/signup", "/api/v1/login", "/api/v1/logout").permitAll() .requestMatchers("/api/v1/users/*/preferences").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() From ffc1aab9e8c27fa13bb1c67bd94b7951398da0de Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 25 Oct 2025 18:44:19 +0900 Subject: [PATCH 054/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20(?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=EC=88=9C)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CurationController.java | 26 +++++--- .../repository/CurationRepository.java | 18 ++---- .../curation/service/CurationService.java | 64 ++++++------------- 3 files changed, 39 insertions(+), 69 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index 56c02d8..dda0310 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -51,26 +51,23 @@ public ResponseEntity> getCuration( } - // CurationController.java - // GET /api/v1/curations - // GET /api/v1/curations?sort=popular - // GET /api/v1/curations?sort=latest&cursor=300&size=10 + //http://localhost:8081/api/v1/curations?sort=latest&cursor=17&size=1 + + // -- 큐레이션 목록 조회 -- @GetMapping public ResponseEntity> getCurationList( @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size) { - CurationListGetRes curationListGetRes = curationService.getCurationList(sort, cursor, size); + CurationListGetRes curationListGetRes = curationService.getCurationList(cursor, size); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); } - - // -- 큐레이션 수정 -- @PatchMapping("/{curationId}") public ResponseEntity> updateCuration( @@ -83,14 +80,25 @@ public ResponseEntity> updateCuration( .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); } + // -- 큐레이션 삭제 -- @DeleteMapping("/{curationId}") public ResponseEntity> deleteCuration( @PathVariable Long curationId, @AuthenticationPrincipal CustomUserDetails currentUser) { - CurationDeleteRes res = curationService.removeCuration(currentUser.getId(), curationId); + Long userId; + + if (currentUser == null) { + userId = 2L; + } + else{ + userId=currentUser.getId(); + } + CurationDeleteRes res = curationService.removeCuration(userId, curationId); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); } -} \ No newline at end of file +} + + diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index afa8d94..2e6add1 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -7,25 +7,15 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.time.LocalDateTime; import java.util.List; public interface CurationRepository extends JpaRepository { List findByUserId(Long userId); - // 첫 요청 - User JOIN (정렬 조건은 Pageable로 받음) - @Query("SELECT c FROM Curation c JOIN FETCH c.user") - Page findAllWithUser(Pageable pageable); - // 커서 기반 조회 - 최신순 - @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id < :cursor ORDER BY c.createdAt DESC, c.id DESC") - List findByIdLessThanWithUserLatest(@Param("cursor") Long cursor, Pageable pageable); + @Query("SELECT c FROM Curation c WHERE c.id <= :cursor ORDER BY c.createdAt DESC, c.id DESC") + List findByCursorWithSize(@Param("cursor") Long cursor, Pageable pageable); +} - // 커서 기반 조회 - 인기순 - @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id < :cursor ORDER BY c.popularityScore DESC, c.id DESC") - List findByIdLessThanWithUserPopular(@Param("cursor") Long cursor, Pageable pageable); - - // 커서 기반 조회 - 조회순 - @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id < :cursor ORDER BY c.viewCount DESC, c.id DESC") - List findByIdLessThanWithUserViews(@Param("cursor") Long cursor, Pageable pageable); -} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index 3dc4408..83d9f68 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +34,8 @@ public class CurationService { private final CurationRepository curationRepository; private final UserRepository userRepository; + private static final int PAGE_SIZE = 10; + // -- 큐레이션 등록 -- @Transactional @@ -74,67 +77,36 @@ public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { // -- 큐레이션 목록 조회 -- - @Transactional(readOnly = true) - public CurationListGetRes getCurationList(String sort, Long cursor, int size) { - // 커서 기반 조회 (size + 1개 조회) - List curations; - if (cursor == null) { - // 첫 요청 - User JOIN - PageRequest pageRequest = PageRequest.of(0, size + 1, getSortCondition(sort)); - curations = curationRepository.findAllWithUser(pageRequest).getContent(); - } else { - // 커서 이후 데이터 조회 - User JOIN - PageRequest pageRequest = PageRequest.of(0, size + 1); - curations = switch (sort) { - case "latest" -> curationRepository.findByIdLessThanWithUserLatest(cursor, pageRequest); - case "popular" -> curationRepository.findByIdLessThanWithUserPopular(cursor, pageRequest); - case "views" -> curationRepository.findByIdLessThanWithUserViews(cursor, pageRequest); - default -> curationRepository.findByIdLessThanWithUserLatest(cursor, pageRequest); - }; - } + public CurationListGetRes getCurationList(Long cursor, int size) { + // size+1개 조회해서 다음 페이지 있는지 확인 + Pageable pageable = PageRequest.of(0, size + 1); - // hasNext 판단 + List curations = curationRepository.findByCursorWithSize(cursor, pageable); + + // 다음 페이지 있는지 확인 boolean hasNext = curations.size() > size; + + // nextCursor = 다음에 조회할 데이터의 ID (size+1번째) + Long nextCursor = null; if (hasNext) { - curations = curations.subList(0, size); + nextCursor = curations.get(size).getId(); // 4번째 데이터의 ID + curations = curations.subList(0, size); // 3개만 반환 } - // Entity -> DTO 변환 + // DTO 변환 List content = curations.stream() .map(CurationContentRes::from) .toList(); - // nextCursor 계산 - Long nextCursor = hasNext && !content.isEmpty() - ? content.get(content.size() - 1).curationId() - : null; - return new CurationListGetRes( - sort, - getSortDescription(sort), + "latest", + "최신순 정렬", content, hasNext, nextCursor ); } - private Sort getSortCondition(String sort) { - return switch (sort) { - case "latest" -> Sort.by(Sort.Direction.DESC, "createdAt"); - case "popular" -> Sort.by(Sort.Direction.DESC, "popularityScore", "id"); - case "views" -> Sort.by(Sort.Direction.DESC, "viewCount", "id"); - default -> Sort.by(Sort.Direction.DESC, "createdAt"); - }; - } - - private String getSortDescription(String sort) { - return switch (sort) { - case "latest" -> "최신 작성순"; - case "popular" -> "인기순"; - case "views" -> "조회순"; - default -> "최신 작성순"; - }; - } // -- 큐레이션 수정 -- @@ -166,4 +138,4 @@ public CurationDeleteRes removeCuration(Long userId, Long curationId) { return CurationDeleteRes.from(curation.getId(), LocalDateTime.now()); } -} \ No newline at end of file +} From b300cf08c83883f16ebea43aa414cfeca2de7638 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 25 Oct 2025 19:57:38 +0900 Subject: [PATCH 055/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20(?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=EC=88=9C)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/curation/SortType.java | 17 +++++++-- .../controller/CurationController.java | 15 ++++---- .../dto/get/list/CurationListGetRes.java | 15 +++++++- .../mvp/domain/curation/entity/Curation.java | 2 +- .../repository/CurationRepository.java | 12 +++++- .../curation/service/CurationService.java | 32 +++++----------- .../service/fetcher/CurationFetcher.java | 38 +++++++++++++++++++ .../mvp/global/HyperParam/Defaults.java | 27 +++++++++++++ 8 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java create mode 100644 src/main/java/BookPick/mvp/global/HyperParam/Defaults.java diff --git a/src/main/java/BookPick/mvp/domain/curation/SortType.java b/src/main/java/BookPick/mvp/domain/curation/SortType.java index 0ddbbca..f78197e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/SortType.java +++ b/src/main/java/BookPick/mvp/domain/curation/SortType.java @@ -1,3 +1,4 @@ +// SortType.java package BookPick.mvp.domain.curation; import lombok.AllArgsConstructor; @@ -6,8 +7,18 @@ @AllArgsConstructor @Getter public enum SortType { - - SORT_LATEST("latest"); + SORT_LATEST("latest", "최신순 정렬"), + SORT_POPULAR("popular", "인기순 정렬"); private final String value; -} + private final String description; + + public static SortType fromValue(String value) { + for (SortType type : values()) { + if (type.value.equals(value)) { + return type; + } + } + return SORT_LATEST; // 기본값 + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index dda0310..270f06b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -11,11 +11,14 @@ import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.CurationService; +import BookPick.mvp.global.HyperParam.Defaults; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -51,9 +54,6 @@ public ResponseEntity> getCuration( } - - //http://localhost:8081/api/v1/curations?sort=latest&cursor=17&size=1 - // -- 큐레이션 목록 조회 -- @GetMapping public ResponseEntity> getCurationList( @@ -61,7 +61,9 @@ public ResponseEntity> getCurationList( @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size) { - CurationListGetRes curationListGetRes = curationService.getCurationList(cursor, size); + SortType sortType = SortType.fromValue(sort); + + CurationListGetRes curationListGetRes = curationService.getCurationList(sortType, cursor, size); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); @@ -90,9 +92,8 @@ public ResponseEntity> deleteCuration( if (currentUser == null) { userId = 2L; - } - else{ - userId=currentUser.getId(); + } else { + userId = currentUser.getId(); } CurationDeleteRes res = curationService.removeCuration(userId, curationId); diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java index c2996a1..4c5f44c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java @@ -1,5 +1,7 @@ +// CurationListGetRes.java package BookPick.mvp.domain.curation.dto.get.list; +import BookPick.mvp.domain.curation.SortType; import java.util.List; public record CurationListGetRes( @@ -8,4 +10,15 @@ public record CurationListGetRes( List content, boolean hasNext, Long nextCursor -) {} \ No newline at end of file +) { + public static CurationListGetRes from(SortType sortType, List content, + boolean hasNext, Long nextCursor) { + return new CurationListGetRes( + sortType.getValue(), + sortType.getDescription(), + content, + hasNext, + nextCursor + ); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 27c4163..dba3930 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -120,6 +120,6 @@ public void increaseViewCount() { updatePopularityScore(); // 인기도 재계산 } public void updatePopularityScore() { - this.popularityScore = (likeCount * 3) + (viewCount * 2) + (commentCount * 1); + this.popularityScore = (likeCount * 3) + (commentCount * 2) + (viewCount * 1); } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 2e6add1..49768c9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -15,7 +15,17 @@ public interface CurationRepository extends JpaRepository { List findByUserId(Long userId); + // 사이즈만큼 최신순으로 불러오는 함수 + List findAllByOrderByCreatedAtDesc(Pageable pageable); + + + @Query("SELECT c FROM Curation c WHERE c.id <= :cursor ORDER BY c.createdAt DESC, c.id DESC") - List findByCursorWithSize(@Param("cursor") Long cursor, Pageable pageable); + List findCurations(@Param("cursor") Long cursor, Pageable pageable); + + @Query("SELECT c FROM Curation c " + + "WHERE (:cursor IS NULL OR c.id < :cursor) " + // 이 부분! + "ORDER BY c.popularityScore DESC, c.id DESC") + List findCurationsByPopularity(@Param("cursor") Long cursor, Pageable pageable); } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index 83d9f68..b7b8fa9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -1,6 +1,7 @@ // CurationService.java package BookPick.mvp.domain.curation.service; +import BookPick.mvp.domain.curation.SortType; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; @@ -13,6 +14,7 @@ import BookPick.mvp.domain.curation.converter.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; @@ -34,7 +36,7 @@ public class CurationService { private final CurationRepository curationRepository; private final UserRepository userRepository; - private static final int PAGE_SIZE = 10; + private final CurationFetcher curationFetcher; // -- 큐레이션 등록 -- @@ -64,6 +66,7 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { return CurationCreateRes.from(saved); } + // -- 큐레이션 단건 조회 -- @Transactional public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { @@ -77,38 +80,23 @@ public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { // -- 큐레이션 목록 조회 -- - public CurationListGetRes getCurationList(Long cursor, int size) { - // size+1개 조회해서 다음 페이지 있는지 확인 + public CurationListGetRes getCurationList(SortType sortType, Long cursor, int size) { Pageable pageable = PageRequest.of(0, size + 1); - List curations = curationRepository.findByCursorWithSize(cursor, pageable); + List curations = curationFetcher.fetchCurations(sortType, cursor, pageable); - // 다음 페이지 있는지 확인 boolean hasNext = curations.size() > size; + Long nextCursor = curationFetcher.calculateNextCursor(curations, size, hasNext); + List result = hasNext ? curations.subList(0, size) : curations; - // nextCursor = 다음에 조회할 데이터의 ID (size+1번째) - Long nextCursor = null; - if (hasNext) { - nextCursor = curations.get(size).getId(); // 4번째 데이터의 ID - curations = curations.subList(0, size); // 3개만 반환 - } - - // DTO 변환 - List content = curations.stream() + List content = result.stream() .map(CurationContentRes::from) .toList(); - return new CurationListGetRes( - "latest", - "최신순 정렬", - content, - hasNext, - nextCursor - ); + return CurationListGetRes.from(sortType, content, hasNext, nextCursor); } - // -- 큐레이션 수정 -- @Transactional public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUpdateReq req) { diff --git a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java new file mode 100644 index 0000000..806b9ef --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java @@ -0,0 +1,38 @@ +package BookPick.mvp.domain.curation.service.fetcher; + +import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import org.springframework.stereotype.Component; +import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; + + +@Component +@RequiredArgsConstructor +public class CurationFetcher { + + private final CurationRepository curationRepository; + + public List fetchCurations(SortType sortType, Long cursor, Pageable pageable) { + + if (cursor == null) { + if(sortType.equals(SortType.SORT_LATEST)) + return curationRepository.findAllByOrderByCreatedAtDesc(pageable); // 취향 유사도 만들기 전까진 최신순 + } + return switch (sortType) { + case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); + case SORT_LATEST -> curationRepository.findCurations(cursor, pageable); + }; + } + + public Long calculateNextCursor(List curations, int size, boolean hasNext) { + return hasNext ? curations.get(size).getId() : null; + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java b/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java new file mode 100644 index 0000000..8f0cbfd --- /dev/null +++ b/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java @@ -0,0 +1,27 @@ +package BookPick.mvp.global.HyperParam; + +import BookPick.mvp.domain.curation.SortType; +import lombok.AllArgsConstructor; +import lombok.Getter; + + +@AllArgsConstructor +@Getter +public enum Defaults { + SORT_TYPE(SortType.SORT_LATEST), + PAGE_SIZE(20); + + private Object value; + public static final String SORT_TYPE_STRING = SORT_TYPE.getValueAsString(); // 상수 정의 + + + public Object getValue() { + return value; + } + public String getValueAsString() { // 문자열로 변환해주는 메서드 + if (value instanceof Enum) { + return ((Enum) value).name(); // Enum이면 name() 사용 + } + return String.valueOf(value); // 그 외에는 그냥 toString + } +} \ No newline at end of file From 6b14438a83173374847eba93d961aa0fcacd2525 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 25 Oct 2025 20:35:38 +0900 Subject: [PATCH 056/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20(?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=EC=88=9C)=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/dto/get/list/CurationContentRes.java | 4 ++-- .../domain/curation/dto/get/list/CurationListGetRes.java | 2 ++ .../mvp/domain/curation/repository/CurationRepository.java | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java index f0ea824..5619ba6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java @@ -15,7 +15,7 @@ public record CurationContentRes( int viewCount, Double similarity, String matched, - Double popularityScore, + Integer popularityScore, String createdAt ) { public static CurationContentRes from(Curation curation) { @@ -32,7 +32,7 @@ public static CurationContentRes from(Curation curation) { 0, // TODO: 조회수 null, null, - null, // TODO: popularityScore 계산 + curation.getPopularityScore(), // TODO: popularityScore 계산 curation.getCreatedAt().toString() ); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java index 4c5f44c..83f112a 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java @@ -8,6 +8,7 @@ public record CurationListGetRes( String sortType, String description, List content, + int size, boolean hasNext, Long nextCursor ) { @@ -17,6 +18,7 @@ public static CurationListGetRes from(SortType sortType, List { List findAllByOrderByCreatedAtDesc(Pageable pageable); - @Query("SELECT c FROM Curation c WHERE c.id <= :cursor ORDER BY c.createdAt DESC, c.id DESC") List findCurations(@Param("cursor") Long cursor, Pageable pageable); @Query("SELECT c FROM Curation c " + - "WHERE (:cursor IS NULL OR c.id < :cursor) " + // 이 부분! + "WHERE (:cursor IS NULL) " + + " OR c.popularityScore <= (SELECT c2.popularityScore FROM Curation c2 WHERE c2.id = :cursor) " + + " OR (c.popularityScore = (SELECT c2.popularityScore FROM Curation c2 WHERE c2.id = :cursor) AND c.id < :cursor) " + "ORDER BY c.popularityScore DESC, c.id DESC") List findCurationsByPopularity(@Param("cursor") Long cursor, Pageable pageable); + } From 3ede92627e4b880dd6a971b5ed77997c9c649833 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 25 Oct 2025 21:02:58 +0900 Subject: [PATCH 057/291] =?UTF-8?q?refactor=20:=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?(=EC=9D=B8=EA=B8=B0=EC=88=9C,=20=EC=B5=9C=EC=8B=A0=EC=88=9C)=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/curation/SortType.java | 6 ++- .../curation/dto/get/list/CursorPage.java | 14 ++++++ .../curation/service/CurationService.java | 21 ++++----- .../service/Handler/CurationPageHandler.java | 43 +++++++++++++++++++ .../service/fetcher/CurationFetcher.java | 1 + 5 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java diff --git a/src/main/java/BookPick/mvp/domain/curation/SortType.java b/src/main/java/BookPick/mvp/domain/curation/SortType.java index f78197e..de5f44b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/SortType.java +++ b/src/main/java/BookPick/mvp/domain/curation/SortType.java @@ -8,11 +8,13 @@ @Getter public enum SortType { SORT_LATEST("latest", "최신순 정렬"), - SORT_POPULAR("popular", "인기순 정렬"); + SORT_POPULAR("popular", "인기순 정렬"), + SORT_SIMILARITY("similarity", "취향 유사도순 정렬"); private final String value; private final String description; - + + public static SortType fromValue(String value) { for (SortType type : values()) { if (type.value.equals(value)) { diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java new file mode 100644 index 0000000..980f407 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java @@ -0,0 +1,14 @@ +package BookPick.mvp.domain.curation.dto.get.list; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class CursorPage { + private final List content; + private final boolean hasNext; + private final Long nextCursor; +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index b7b8fa9..338024c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -6,6 +6,7 @@ import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; +import BookPick.mvp.domain.curation.dto.get.list.CursorPage; import BookPick.mvp.domain.curation.dto.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; @@ -14,6 +15,7 @@ import BookPick.mvp.domain.curation.converter.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.service.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; @@ -37,6 +39,8 @@ public class CurationService { private final CurationRepository curationRepository; private final UserRepository userRepository; private final CurationFetcher curationFetcher; + private final CurationPageHandler pageHandler; + // -- 큐레이션 등록 -- @@ -81,19 +85,11 @@ public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { // -- 큐레이션 목록 조회 -- public CurationListGetRes getCurationList(SortType sortType, Long cursor, int size) { - Pageable pageable = PageRequest.of(0, size + 1); - - List curations = curationFetcher.fetchCurations(sortType, cursor, pageable); - - boolean hasNext = curations.size() > size; - Long nextCursor = curationFetcher.calculateNextCursor(curations, size, hasNext); - List result = hasNext ? curations.subList(0, size) : curations; + List curations = pageHandler.fetchCurationsWithExtra(sortType, cursor, size); + CursorPage page = pageHandler.createCursorPage(curations, size); + List content = pageHandler.convertToContentRes(page.getContent()); - List content = result.stream() - .map(CurationContentRes::from) - .toList(); - - return CurationListGetRes.from(sortType, content, hasNext, nextCursor); + return CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); } @@ -112,6 +108,7 @@ public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUp return CurationUpdateRes.from(curation); } + // -- 큐레이션 삭제 -- @Transactional public CurationDeleteRes removeCuration(Long userId, Long curationId) { diff --git a/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java new file mode 100644 index 0000000..bd801f3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java @@ -0,0 +1,43 @@ +package BookPick.mvp.domain.curation.service.Handler; + + +import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; +import BookPick.mvp.domain.curation.dto.get.list.CursorPage; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CurationPageHandler { + + private final CurationFetcher curationFetcher; + + // 1. 데이터 조회 (size+1개) + public List fetchCurationsWithExtra(SortType sortType, Long cursor, int size) { + Pageable pageable = PageRequest.of(0, size + 1); + return curationFetcher.fetchCurations(sortType, cursor, pageable); + } + + // 2. 커서 페이징 처리 + public CursorPage createCursorPage(List curations, int size) { + boolean hasNext = curations.size() > size; + Long nextCursor = curationFetcher.calculateNextCursor(curations, size, hasNext); + List content = hasNext ? curations.subList(0, size) : curations; + + return new CursorPage<>(content, hasNext, nextCursor); + } + + // 3. DTO 변환 + public List convertToContentRes(List curations) { + return curations.stream() + .map(CurationContentRes::from) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java index 806b9ef..87995d6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java @@ -29,6 +29,7 @@ public List fetchCurations(SortType sortType, Long cursor, Pageable pa return switch (sortType) { case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); case SORT_LATEST -> curationRepository.findCurations(cursor, pageable); + case SORT_SIMILARITY -> curationRepository.findCurations(cursor, pageable); }; } From 16137636d83599b5110d4224c7442e287e2bbe0e Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 25 Oct 2025 21:35:47 +0900 Subject: [PATCH 058/291] chore: meaningless commit --- .../BookPick/mvp/domain/auth/controller/AuthController.java | 2 +- src/main/java/BookPick/mvp/global/config/CorsConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 6a190fe..9765e8f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1") +@RequestMapping("/api/v1/auth") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/java/BookPick/mvp/global/config/CorsConfig.java b/src/main/java/BookPick/mvp/global/config/CorsConfig.java index cb956cb..5f40a2a 100644 --- a/src/main/java/BookPick/mvp/global/config/CorsConfig.java +++ b/src/main/java/BookPick/mvp/global/config/CorsConfig.java @@ -14,7 +14,7 @@ public WebMvcConfigurer corsConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("https://nationally-sizes-carmen-press.trycloudflare.com") + .allowedOrigins("*") .allowedMethods("GET","POST","PUT","DELETE","PATCH","OPTIONS") .allowedHeaders("*") .allowCredentials(true); From 26a582bc56a7ad3d655ef0e384e0c525c8cd95d2 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 30 Oct 2025 19:33:36 +0900 Subject: [PATCH 059/291] chore: meaningless commit --- .../auth/service/MyUserDetailsService.java | 5 ---- .../fetcher/CurationSimilarityFetcher.java | 4 +++ .../similarity/SimilarityCalculator.java | 4 +++ .../service/similarity/SimilarityMatcher.java | 4 +++ ...yRegisteredReadingPreferenceException.java | 2 +- .../UserReadingPreferenceNotExisted.java | 2 +- .../ReadingPreferenceController.java | 20 +++++++------- .../Create/ReadingPreferenceCreateReq.java | 2 +- .../Create/ReadingPreferenceCreateRes.java | 8 +++--- .../Delete/ReadingPreferenceDeleteRes.java | 5 +--- .../dto/Get/ReadingPreferenceGetReq.java | 2 +- .../dto/Get/ReadingPreferenceGetRes.java | 4 +-- .../Update/ReadingPreferenceUpdateReq.java | 2 +- .../Update/ReadingPreferenceUpdateRes.java | 5 ++-- .../entity/ReadingPreference.java | 6 ++--- .../ReadingPreferenceRepository.java | 4 +-- .../service/ReadingPreferenceService.java | 26 +++++++++---------- .../mvp/global/config/CorsConfig.java | 5 +++- .../BookPick/mvp/global/config/JwtConfig.java | 1 - .../BookPick/mvp/global/config/JwtFilter.java | 24 ++++++++--------- src/main/resources/application.yml | 4 +++ 21 files changed, 71 insertions(+), 68 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/Exception/AlreadyRegisteredReadingPreferenceException.java (84%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/Exception/UserReadingPreferenceNotExisted.java (83%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/controller/ReadingPreferenceController.java (76%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Create/ReadingPreferenceCreateReq.java (88%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Create/ReadingPreferenceCreateRes.java (54%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Delete/ReadingPreferenceDeleteRes.java (71%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Get/ReadingPreferenceGetReq.java (60%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Get/ReadingPreferenceGetRes.java (87%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Update/ReadingPreferenceUpdateReq.java (88%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/dto/Update/ReadingPreferenceUpdateRes.java (80%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/entity/ReadingPreference.java (89%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/repository/ReadingPreferenceRepository.java (69%) rename src/main/java/BookPick/mvp/domain/{ReadingPreference => preference}/service/ReadingPreferenceService.java (75%) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 4ec8790..ea338b2 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -89,11 +89,6 @@ public CustomUserDetails( static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities){ return new CustomUserDetails(userId, email, authorities); } - - - - - } } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java new file mode 100644 index 0000000..62ca451 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.service.fetcher; + +public class CurationSimilarityFetcher { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java b/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java new file mode 100644 index 0000000..f96ec9a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.service.similarity; + +public class SimilarityCalculator { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java b/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java new file mode 100644 index 0000000..66c359d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.service.similarity; + +public class SimilarityMatcher { +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java b/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java rename to src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java index 6cc825d..ae18d15 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java +++ b/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.Exception; +package BookPick.mvp.domain.preference.Exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java b/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java rename to src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java index 541e17e..0dc7017 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java +++ b/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.Exception; +package BookPick.mvp.domain.preference.Exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java similarity index 76% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java rename to src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java index 0fda0de..646ebea 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java @@ -1,14 +1,12 @@ -package BookPick.mvp.domain.ReadingPreference.controller; - -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes.*; -import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetReq; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; -import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; +package BookPick.mvp.domain.preference.controller; + +import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateReq; +import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateRes; +import BookPick.mvp.domain.preference.dto.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.preference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateRes; +import BookPick.mvp.domain.preference.service.ReadingPreferenceService; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateReq.java similarity index 88% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateReq.java index 48fe4ef..9b9addc 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateReq.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Create; +package BookPick.mvp.domain.preference.dto.Create; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java b/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateRes.java similarity index 54% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateRes.java index 123d943..ea500a5 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateRes.java @@ -1,11 +1,9 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Create; +package BookPick.mvp.domain.preference.dto.Create; -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.preference.entity.ReadingPreference; -import java.util.List; - - public record ReadingPreferenceCreateRes( +public record ReadingPreferenceCreateRes( Long readingPreferenceId ) { static public ReadingPreferenceCreateRes from(ReadingPreference readingPreference) { diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java b/src/main/java/BookPick/mvp/domain/preference/dto/Delete/ReadingPreferenceDeleteRes.java similarity index 71% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Delete/ReadingPreferenceDeleteRes.java index bbba609..5afb46b 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Delete/ReadingPreferenceDeleteRes.java @@ -1,9 +1,6 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Delete; - -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +package BookPick.mvp.domain.preference.dto.Delete; import java.time.LocalDateTime; -import java.util.List; public record ReadingPreferenceDeleteRes( Long preferenceId, diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java b/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetReq.java similarity index 60% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetReq.java index d6bd7b8..4a68237 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetReq.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Get; +package BookPick.mvp.domain.preference.dto.Get; // -- 독서 취향 조회 요청 -- public record ReadingPreferenceGetReq () diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java b/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetRes.java similarity index 87% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetRes.java index b1e1ada..0175320 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetRes.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Get; +package BookPick.mvp.domain.preference.dto.Get; -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.preference.entity.ReadingPreference; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java similarity index 88% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java index 39fa2a8..004e25c 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Update; +package BookPick.mvp.domain.preference.dto.Update; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java b/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateRes.java similarity index 80% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java rename to src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateRes.java index a38ef67..e9ba89e 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateRes.java @@ -1,7 +1,6 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Update; +package BookPick.mvp.domain.preference.dto.Update; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.preference.entity.ReadingPreference; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java similarity index 89% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java rename to src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java index c213a1a..88c757e 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.ReadingPreference.entity; +package BookPick.mvp.domain.preference.entity; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; @@ -36,7 +36,7 @@ public class ReadingPreference { private List moods; @ElementCollection - @CollectionTable(name = "preference_reading_habits", joinColumns = @JoinColumn(name = "preference_id")) + @CollectionTable(name = "preference_styles", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "habit") private List readingHabits; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java b/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java similarity index 69% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java rename to src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java index 92d9029..c1a6d06 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java +++ b/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.ReadingPreference.repository; +package BookPick.mvp.domain.preference.repository; -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.preference.entity.ReadingPreference; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java similarity index 75% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java rename to src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java index 17d68d5..69d7ad8 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java @@ -1,20 +1,18 @@ -package BookPick.mvp.domain.ReadingPreference.service; - -import BookPick.mvp.domain.ReadingPreference.Exception.AlreadyRegisteredReadingPreferenceException; -import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; -import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetReq; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; -import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +package BookPick.mvp.domain.preference.service; + +import BookPick.mvp.domain.preference.Exception.AlreadyRegisteredReadingPreferenceException; +import BookPick.mvp.domain.preference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateReq; +import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateRes; +import BookPick.mvp.domain.preference.dto.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.preference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateRes; +import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.preference.repository.ReadingPreferenceRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; -import jakarta.persistence.Table; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/BookPick/mvp/global/config/CorsConfig.java b/src/main/java/BookPick/mvp/global/config/CorsConfig.java index 5f40a2a..1a62f22 100644 --- a/src/main/java/BookPick/mvp/global/config/CorsConfig.java +++ b/src/main/java/BookPick/mvp/global/config/CorsConfig.java @@ -14,7 +14,7 @@ public WebMvcConfigurer corsConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("*") + .allowedOrigins("https://bookpick-front.vercel.app") .allowedMethods("GET","POST","PUT","DELETE","PATCH","OPTIONS") .allowedHeaders("*") .allowCredentials(true); @@ -22,3 +22,6 @@ public void addCorsMappings(CorsRegistry registry) { }; } } + +// 오리진 모두 허용 +// evil에서 요 \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/config/JwtConfig.java b/src/main/java/BookPick/mvp/global/config/JwtConfig.java index 12d4c25..966037f 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtConfig.java +++ b/src/main/java/BookPick/mvp/global/config/JwtConfig.java @@ -36,5 +36,4 @@ public SecretKey getRefreshSecretKey() { } - } diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 150aa43..88e2663 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -46,25 +46,25 @@ protected void doFilterInternal( return; } - Claims claims = JwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) + Claims claims = JwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) - Long userId = claims.get("userId", Number.class).longValue(); - String email = claims.get("email").toString(); + Long userId = claims.get("userId", Number.class).longValue(); + String email = claims.get("email").toString(); - var authorities = Arrays.stream( - claims.get("authorities").toString().split(",") - ).map(SimpleGrantedAuthority::new).toList(); + var authorities = Arrays.stream( + claims.get("authorities").toString().split(",") + ).map(SimpleGrantedAuthority::new).toList(); - CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(userId, email, authorities); + CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(userId, email, authorities); - var auth = new UsernamePasswordAuthenticationToken( - customUserDetails, null, customUserDetails.getAuthorities() - ); - auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 추가 정보 저장 (어디서 접속했는지) + var auth = new UsernamePasswordAuthenticationToken( + customUserDetails, null, customUserDetails.getAuthorities() + ); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 추가 정보 저장 (어디서 접속했는지) - SecurityContextHolder.getContext().setAuthentication(auth); + SecurityContextHolder.getContext().setAuthentication(auth); filterChain.doFilter(request, response); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7f0fbf6..8017895 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,3 +42,7 @@ jwt: api : kakao : key : 103086ac1d365cf71f026f6caac34fb3 + google-gemini: + key : AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw + + From 030e86978e07eda48ca557169c4aebfad18cf4d1 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 30 Oct 2025 19:40:41 +0900 Subject: [PATCH 060/291] =?UTF-8?q?feat=20:=20=EA=B0=81=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=ED=99=94=20(with=20=ED=94=84=EB=A1=9C=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=EB=8D=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../provider/CurationLatestProvider.java | 24 +++++++++++++++++++ .../provider/CurationPopularProvider.java | 22 +++++++++++++++++ .../provider/CurationSimilarityProvider.java | 22 +++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java new file mode 100644 index 0000000..fbed1d6 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java @@ -0,0 +1,24 @@ +package BookPick.mvp.domain.curation.service.provider; + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CurationLatestProvider { + + private final CurationRepository curationRepository; + + public List fetch(Long cursor, Pageable pageable) { + if (cursor == null) { + return curationRepository.findAllByOrderByCreatedAtDesc(pageable); + } + return curationRepository.findCurations(cursor, pageable); + } +} + diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java new file mode 100644 index 0000000..dc0253f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java @@ -0,0 +1,22 @@ +package BookPick.mvp.domain.curation.service.provider; + + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CurationPopularProvider { + + private final CurationRepository curationRepository; + + public List fetch(Long cursor, Pageable pageable) { + return curationRepository.findCurationsByPopularity(cursor, pageable); + } +} + diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java new file mode 100644 index 0000000..915fb90 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java @@ -0,0 +1,22 @@ +package BookPick.mvp.domain.curation.service.provider; + + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CurationSimilarityProvider { + + private final CurationRepository curationRepository; + + public List fetch(Long cursor, Pageable pageable) { + // TODO: 유사도 알고리즘 구현 후 변경 예정 + return curationRepository.findCurations(cursor, pageable); + } +} From 70238ef638f55de682f0fe16eb46cf57da17b62c Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 30 Oct 2025 19:52:46 +0900 Subject: [PATCH 061/291] =?UTF-8?q?feat=20:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20API=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/integration/gemini/client/GeminiClient.java | 4 ++++ .../BookPick/mvp/integration/gemini/client/GeminiConfig.java | 4 ++++ .../mvp/integration/gemini/prompt/GeminiPromptStore.java | 4 ++++ .../mvp/integration/gemini/prompt/PromptTemplate.java | 4 ++++ .../integration/gemini/prompt/prompt_files/base_prompt.txt | 0 .../integration/gemini/prompt/prompt_files/keyword_prompt.txt | 0 .../integration/gemini/prompt/prompt_files/mood_prompt.txt | 0 .../java/BookPick/mvp/integration/gemini/service/dfas.java | 4 ++++ 8 files changed, 20 insertions(+) create mode 100644 src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java create mode 100644 src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/base_prompt.txt create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/keyword_prompt.txt create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/mood_prompt.txt create mode 100644 src/main/java/BookPick/mvp/integration/gemini/service/dfas.java diff --git a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java new file mode 100644 index 0000000..b95711e --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java @@ -0,0 +1,4 @@ +package BookPick.mvp.integration.gemini.client; + +public class GeminiClient { +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java new file mode 100644 index 0000000..94e7098 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java @@ -0,0 +1,4 @@ +package BookPick.mvp.integration.gemini.client; + +public class GeminiConfig { +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java b/src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java new file mode 100644 index 0000000..7f963d1 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java @@ -0,0 +1,4 @@ +package BookPick.mvp.integration.gemini.prompt; + +public class GeminiPromptStore { +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java b/src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java new file mode 100644 index 0000000..cf95fa2 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java @@ -0,0 +1,4 @@ +package BookPick.mvp.integration.gemini.prompt; + +public class PromptTemplate { +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/base_prompt.txt b/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/base_prompt.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/keyword_prompt.txt b/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/keyword_prompt.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/mood_prompt.txt b/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/mood_prompt.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/BookPick/mvp/integration/gemini/service/dfas.java b/src/main/java/BookPick/mvp/integration/gemini/service/dfas.java new file mode 100644 index 0000000..89792a5 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/service/dfas.java @@ -0,0 +1,4 @@ +package BookPick.mvp.integration.gemini.service; + +public class dfas { +} From 9db1bda724f76e0f9bbda9cf5ee55481f9fe0bb7 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 30 Oct 2025 23:44:50 +0900 Subject: [PATCH 062/291] =?UTF-8?q?feat=20:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20API=20=EC=82=AC=EC=9A=A9,=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20Mood,=20Genre,=20Keyword=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B3=A0=20Reading=20Style=20=EC=B6=94=EC=B2=9C?= =?UTF-8?q?=ED=95=B4=EC=A3=BC=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 ++- .../gemini/client/GeminiClient.java | 65 ++++++++++++++++++- .../gemini/client/GeminiConfig.java | 15 ++++- .../gemini/prompt/ContentPromptTemplate.java | 35 ++++++++++ .../gemini/prompt/GeminiPromptStore.java | 4 -- .../gemini/prompt/PromptTemplate.java | 4 -- .../SystemInstructionPromptTemplate.java | 22 +++++++ .../prompt/prompt_files/base_prompt.txt | 0 .../prompt/prompt_files/keyword_prompt.txt | 0 .../prompt/prompt_files/mood_prompt.txt | 0 .../gemini/service/GeminiService.java | 28 ++++++++ .../mvp/integration/gemini/service/dfas.java | 4 -- src/main/resources/application.yml | 6 +- .../mvp/BookPickApplicationTests.java | 13 ---- .../java/BookPick/mvp/GeminiClientMain.java | 47 ++++++++++++++ 15 files changed, 222 insertions(+), 31 deletions(-) create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java delete mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java delete mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java create mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java delete mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/base_prompt.txt delete mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/keyword_prompt.txt delete mode 100644 src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/mood_prompt.txt create mode 100644 src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java delete mode 100644 src/main/java/BookPick/mvp/integration/gemini/service/dfas.java delete mode 100644 src/test/java/BookPick/mvp/BookPickApplicationTests.java create mode 100644 src/test/java/BookPick/mvp/GeminiClientMain.java diff --git a/build.gradle b/build.gradle index 7d32541..448de7f 100644 --- a/build.gradle +++ b/build.gradle @@ -48,8 +48,8 @@ dependencies { // JWT (JJWT 0.12.x) implementation 'io.jsonwebtoken:jjwt-api:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5' // Gson 사용 중. Jackson 쓰려면 jjwt-jackson로 교체 + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5' // Gson 사용 중. Jackson 쓰려면 jjwt-jackson로 교체 // DB runtimeOnly 'com.mysql:mysql-connector-j' @@ -62,6 +62,12 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + +} + +test { + useJUnitPlatform() } tasks.named('test') { diff --git a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java index b95711e..c14f5a1 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java +++ b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java @@ -1,4 +1,67 @@ package BookPick.mvp.integration.gemini.client; + + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +@RequiredArgsConstructor public class GeminiClient { -} + + private final GeminiConfig config; + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + public String callGemini(String systemInstruction, String userContent) { + try { + String url = config.getApiUrl() + "?key=" + config.getApiKey(); + + // 요청 바디 생성 (올바른 구조) + String requestBody = String.format(""" + { + "system_instruction": { + "parts": [ + {"text": "%s"} + ] + }, + "contents": [ + { + "parts": [ + {"text": "%s"} + ] + } + ] + } + """, + systemInstruction.replace("\"", "\\\"").replace("\n", "\\n"), + userContent.replace("\"", "\\\"").replace("\n", "\\n") + ); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity entity = new HttpEntity<>(requestBody, headers); + + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.POST, + entity, + String.class + ); + + // 응답에서 텍스트 추출 + JsonNode root = objectMapper.readTree(response.getBody()); + return root.path("candidates").get(0) + .path("content").path("parts").get(0) + .path("text").asText(); + + } catch (Exception e) { + throw new RuntimeException("Gemini API 호출 실패", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java index 94e7098..207fe2a 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java +++ b/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java @@ -1,4 +1,17 @@ package BookPick.mvp.integration.gemini.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import lombok.Getter; + +@Configuration +@Getter public class GeminiConfig { -} + + @Value("${api.gemini.api.key}") + private String apiKey; + + @Value("${api.gemini.api.url}") + private String apiUrl; +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java b/src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java new file mode 100644 index 0000000..f250ac0 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java @@ -0,0 +1,35 @@ +package BookPick.mvp.integration.gemini.prompt; +import lombok.Builder; +import lombok.Getter; + + +@Getter +@Builder +public class ContentPromptTemplate { + + private String mbti; + private String mood; + private String readingMethod; + private String genre; + private String keyword; + private String readingStyle; + + public String toContentPrompt() { + return String.format(""" + **User Input** + MBTI: %s + Mood: %s + Reading Method: %s + Genre: %s + Keyword: %s + Reading Style: %s + """, + mbti, + mood, + readingMethod, + genre, + keyword, + readingStyle + ); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java b/src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java deleted file mode 100644 index 7f963d1..0000000 --- a/src/main/java/BookPick/mvp/integration/gemini/prompt/GeminiPromptStore.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.integration.gemini.prompt; - -public class GeminiPromptStore { -} diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java b/src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java deleted file mode 100644 index cf95fa2..0000000 --- a/src/main/java/BookPick/mvp/integration/gemini/prompt/PromptTemplate.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.integration.gemini.prompt; - -public class PromptTemplate { -} diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java b/src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java new file mode 100644 index 0000000..1f8bf5f --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java @@ -0,0 +1,22 @@ +package BookPick.mvp.integration.gemini.prompt; + +import org.springframework.stereotype.Component; + +@Component +public class SystemInstructionPromptTemplate { + + private static final String SYSTEM_PROMPT = """ + Based on the user's reading preferences, select exactly one item from each of Mood, Genre, Keyword, and ReadingStyle. + You must only choose from the provided lists. + Output only the selected values, one per line, without any labels or formatting. + + Mood list: 퇴근 후, 따뜻한 차 한잔, 비 오는 날, 눈 오는 날, 지하철·버스, 카페, 침대에서, 공원, 도서관, 서점에서, 새벽 시간, 주말 오후, 점심시간, 늦은 밤, 잠들기 전, 혼자만의 시간, 창가에서, 음악과 함께, 여행 중, 휴가 중 + Genre list: 소설, 에세이, 역사, 예술, 자기개발, 경제, 심리학, 사회, 교육, 과학, 철학, 종교 + Keyword list: 위로, 성장, 사랑, 공감, 지식, 유머, 추리, 모험, 판타지, 현실, 미래, 과거 + Reading Style list: 속독형, 몰입형, 정독형, 취향 탐색형, 스토리 중심, 지식 위주, 감성적, 논리적, 창의적, 실용적, 비평적, 상상력 중시, 느긋한 독서, 깊이 있는 사색, 가볍게 즐기기 + """; + + public String getSystemInstructionPrompt() { + return SYSTEM_PROMPT; + } +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/base_prompt.txt b/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/base_prompt.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/keyword_prompt.txt b/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/keyword_prompt.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/mood_prompt.txt b/src/main/java/BookPick/mvp/integration/gemini/prompt/prompt_files/mood_prompt.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java new file mode 100644 index 0000000..59e2e11 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java @@ -0,0 +1,28 @@ +package BookPick.mvp.integration.gemini.service; + + +import BookPick.mvp.integration.gemini.client.GeminiClient; +import BookPick.mvp.integration.gemini.prompt.SystemInstructionPromptTemplate; +import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GeminiService { + + private final GeminiClient geminiClient; + private final SystemInstructionPromptTemplate systemPromptTemplate; + + public String generateRecommendation(ContentPromptTemplate contentTemplate) { + String systemPrompt = systemPromptTemplate.getSystemInstructionPrompt(); + String userPrompt = contentTemplate.toContentPrompt(); + + return geminiClient.callGemini(systemPrompt, userPrompt); + } + + // 결과를 4줄로 파싱 + public String[] parseResult(String result) { + return result.trim().split("\n"); + } +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/service/dfas.java b/src/main/java/BookPick/mvp/integration/gemini/service/dfas.java deleted file mode 100644 index 89792a5..0000000 --- a/src/main/java/BookPick/mvp/integration/gemini/service/dfas.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.integration.gemini.service; - -public class dfas { -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8017895..192d3b5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,7 +42,9 @@ jwt: api : kakao : key : 103086ac1d365cf71f026f6caac34fb3 - google-gemini: - key : AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw + gemini: + api: + key: AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw + url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent diff --git a/src/test/java/BookPick/mvp/BookPickApplicationTests.java b/src/test/java/BookPick/mvp/BookPickApplicationTests.java deleted file mode 100644 index 87d7744..0000000 --- a/src/test/java/BookPick/mvp/BookPickApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package BookPick.mvp; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class BookPickApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/BookPick/mvp/GeminiClientMain.java b/src/test/java/BookPick/mvp/GeminiClientMain.java new file mode 100644 index 0000000..42d5e9c --- /dev/null +++ b/src/test/java/BookPick/mvp/GeminiClientMain.java @@ -0,0 +1,47 @@ +package BookPick.mvp; + + + +import BookPick.mvp.integration.gemini.client.GeminiClient; +import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; +import BookPick.mvp.integration.gemini.prompt.SystemInstructionPromptTemplate; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + + + + + +@SpringBootApplication +public class GeminiClientMain { + + public static void main(String[] args) { + SpringApplication.run(GeminiClientMain.class, args); + } + + @Bean + CommandLineRunner test(GeminiClient client, SystemInstructionPromptTemplate systemTemplate) { + return args -> { + + // ContentPromptTemplate 생성 + ContentPromptTemplate contentTemplate = ContentPromptTemplate.builder() + .mbti("INFP") + .mood("새벽 시간, 카페, 혼자만의 시간") + .readingMethod("한 번에 완독하는 편, 조용한 곳에서만 읽는 편") + .genre("에세이, 철학, 소설") + .keyword("성장, 공감, 현실") + .readingStyle("몰입형, 감성적, 깊이 있는 사색") + .build(); + + // 시스템 프롬프트와 콘텐츠 프롬프트 + String systemPrompt = systemTemplate.getSystemInstructionPrompt(); + String userPrompt = contentTemplate.toContentPrompt(); + + System.out.println("=== 추천 결과 ==="); + String result = client.callGemini(systemPrompt, userPrompt); + System.out.println(result); + }; + } +} \ No newline at end of file From 7b2fe3f0dd799b238b42e188d122e20ed38bad73 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 2 Nov 2025 14:54:11 +0900 Subject: [PATCH 063/291] =?UTF-8?q?feat=20:=20=EC=A0=9C=EB=AF=B8=EB=82=98?= =?UTF-8?q?=EC=9D=B4=20sql=20=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/service/AuthService.java | 2 - .../repository/CurationRepository.java | 17 ++++ .../gemini/dto/CurationMatchResult.java | 82 +++++++++++++++++++ .../gemini/service/GeminiService.java | 44 +++++++++- .../java/BookPick/mvp/GeminiClientMain.java | 53 ++++++------ 5 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index c96299f..f9ac360 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -62,8 +62,6 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.passWord()); // 임시로 이메일과 아이디가 담김 try { - // getObject : AuthenticationManager 객체 반환 - // .authenticate : Authenticate를 상속한 구현체의 인스턴스를 검증한다. Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 firstLoginCheck(req.email()); diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 41472b8..277bcd7 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -29,5 +29,22 @@ public interface CurationRepository extends JpaRepository { "ORDER BY c.popularityScore DESC, c.id DESC") List findCurationsByPopularity(@Param("cursor") Long cursor, Pageable pageable); + // Gemini 추천 결과로 큐레이션 찾기 + @Query(""" + SELECT DISTINCT c FROM Curation c + LEFT JOIN c.moods m + LEFT JOIN c.genres g + LEFT JOIN c.keywords k + LEFT JOIN c.styles s + WHERE c.deletedAt IS NULL + AND (m IN :moods OR g IN :genres OR k IN :keywords OR s IN :styles) + ORDER BY c.popularityScore DESC + """) + List findByRecommendation( + @Param("moods") List moods, + @Param("genres") List genres, + @Param("keywords") List keywords, + @Param("styles") List styles + ); } diff --git a/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java b/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java new file mode 100644 index 0000000..c4de097 --- /dev/null +++ b/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java @@ -0,0 +1,82 @@ +package BookPick.mvp.integration.gemini.dto; + +import BookPick.mvp.domain.curation.entity.Curation; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CurationMatchResult { + private Curation curation; + private String matchedMood; + private String matchedGenre; + private String matchedKeyword; + private String matchedStyle; + private int totalMatchCount; + + public static CurationMatchResult of(Curation curation, + String recommendedMood, + String recommendedGenre, + String recommendedKeyword, + String recommendedStyle) { + + String matchedMood = null; + String matchedGenre = null; + String matchedKeyword = null; + String matchedStyle = null; + int matchCount = 0; + + // Mood 매칭 + if (curation.getMoods() != null && curation.getMoods().contains(recommendedMood)) { + matchedMood = recommendedMood; + matchCount++; + } + + // Genre 매칭 + if (curation.getGenres() != null && curation.getGenres().contains(recommendedGenre)) { + matchedGenre = recommendedGenre; + matchCount++; + } + + // Keyword 매칭 + if (curation.getKeywords() != null && curation.getKeywords().contains(recommendedKeyword)) { + matchedKeyword = recommendedKeyword; + matchCount++; + } + + // Style 매칭 + if (curation.getStyles() != null && curation.getStyles().contains(recommendedStyle)) { + matchedStyle = recommendedStyle; + matchCount++; + } + + return CurationMatchResult.builder() + .curation(curation) + .matchedMood(matchedMood) + .matchedGenre(matchedGenre) + .matchedKeyword(matchedKeyword) + .matchedStyle(matchedStyle) + .totalMatchCount(matchCount) + .build(); + } + + // 일치한 것만 문자열로 반환 + public String getMatchedString() { + StringBuilder sb = new StringBuilder(); + + if (matchedMood != null) { + sb.append("Mood: ").append(matchedMood).append("\n"); + } + if (matchedGenre != null) { + sb.append("Genre: ").append(matchedGenre).append("\n"); + } + if (matchedKeyword != null) { + sb.append("Keyword: ").append(matchedKeyword).append("\n"); + } + if (matchedStyle != null) { + sb.append("Style: ").append(matchedStyle).append("\n"); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java index 59e2e11..4b849ff 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java @@ -1,11 +1,18 @@ package BookPick.mvp.integration.gemini.service; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.integration.gemini.client.GeminiClient; +import BookPick.mvp.integration.gemini.dto.CurationMatchResult; import BookPick.mvp.integration.gemini.prompt.SystemInstructionPromptTemplate; import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -13,6 +20,7 @@ public class GeminiService { private final GeminiClient geminiClient; private final SystemInstructionPromptTemplate systemPromptTemplate; + private final CurationRepository curationRepository; public String generateRecommendation(ContentPromptTemplate contentTemplate) { String systemPrompt = systemPromptTemplate.getSystemInstructionPrompt(); @@ -21,8 +29,40 @@ public String generateRecommendation(ContentPromptTemplate contentTemplate) { return geminiClient.callGemini(systemPrompt, userPrompt); } - // 결과를 4줄로 파싱 public String[] parseResult(String result) { return result.trim().split("\n"); } -} + + @Transactional(readOnly = true) + public List recommendCurationsWithMatch(ContentPromptTemplate contentTemplate) { + // 1. Gemini에게 추천 받기 + String result = generateRecommendation(contentTemplate); + String[] parsed = parseResult(result); + + // 2. 파싱된 결과 (각 1개씩) + String recommendedMood = parsed[0].trim(); + String recommendedGenre = parsed[1].trim(); + String recommendedKeyword = parsed[2].trim(); + String recommendedStyle = parsed[3].trim(); + + // 3. DB에서 큐레이션 찾기 + List curations = curationRepository.findByRecommendation( + List.of(recommendedMood), + List.of(recommendedGenre), + List.of(recommendedKeyword), + List.of(recommendedStyle) + ); + + // 4. 일치 정보와 함께 반환 (일치 개수 많은 순) + return curations.stream() + .map(curation -> CurationMatchResult.of( + curation, + recommendedMood, + recommendedGenre, + recommendedKeyword, + recommendedStyle + )) + .sorted((a, b) -> Integer.compare(b.getTotalMatchCount(), a.getTotalMatchCount())) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/test/java/BookPick/mvp/GeminiClientMain.java b/src/test/java/BookPick/mvp/GeminiClientMain.java index 42d5e9c..a9736a6 100644 --- a/src/test/java/BookPick/mvp/GeminiClientMain.java +++ b/src/test/java/BookPick/mvp/GeminiClientMain.java @@ -1,18 +1,16 @@ package BookPick.mvp; - -import BookPick.mvp.integration.gemini.client.GeminiClient; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.integration.gemini.dto.CurationMatchResult; import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; -import BookPick.mvp.integration.gemini.prompt.SystemInstructionPromptTemplate; +import BookPick.mvp.integration.gemini.service.GeminiService; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; - - - +import java.util.List; @SpringBootApplication public class GeminiClientMain { @@ -22,26 +20,33 @@ public static void main(String[] args) { } @Bean - CommandLineRunner test(GeminiClient client, SystemInstructionPromptTemplate systemTemplate) { + CommandLineRunner test(GeminiService geminiService) { return args -> { - // ContentPromptTemplate 생성 - ContentPromptTemplate contentTemplate = ContentPromptTemplate.builder() - .mbti("INFP") - .mood("새벽 시간, 카페, 혼자만의 시간") - .readingMethod("한 번에 완독하는 편, 조용한 곳에서만 읽는 편") - .genre("에세이, 철학, 소설") - .keyword("성장, 공감, 현실") - .readingStyle("몰입형, 감성적, 깊이 있는 사색") - .build(); - - // 시스템 프롬프트와 콘텐츠 프롬프트 - String systemPrompt = systemTemplate.getSystemInstructionPrompt(); - String userPrompt = contentTemplate.toContentPrompt(); - - System.out.println("=== 추천 결과 ==="); - String result = client.callGemini(systemPrompt, userPrompt); - System.out.println(result); + ContentPromptTemplate template = ContentPromptTemplate.builder() + .mbti("INFP") + .mood("새벽 시간, 카페, 혼자만의 시간") + .readingMethod("한 번에 완독하는 편, 조용한 곳에서만 읽는 편") + .genre("에세이, 철학, 소설") + .keyword("성장, 공감, 현실") + .readingStyle("몰입형, 감성적, 깊이 있는 사색") + .build(); + + System.out.println("=== 추천된 큐레이션 ==="); + List results = geminiService.recommendCurationsWithMatch(template); + + System.out.println("총 " + results.size() + "개의 큐레이션 발견\n"); + + results.forEach(result -> { + Curation c = result.getCuration(); + System.out.println("📚 책 제목: " + c.getBookTitle()); + System.out.println(" 저자: " + c.getBookAuthor()); + System.out.println(" 총 일치: " + result.getTotalMatchCount() + "개"); + System.out.println("\n=== 일치한 태그 ==="); + System.out.print(result.getMatchedString()); // ← 일치하는 것만 출력! + System.out.println(" 인기도: " + c.getPopularityScore()); + System.out.println("-----------------------------------"); + }); }; } } \ No newline at end of file From cc0261ffb123e0803a734759ff173d1edf863b08 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 2 Nov 2025 15:26:30 +0900 Subject: [PATCH 064/291] =?UTF-8?q?feat=20:=20LoginDto,=20SignDto=20passWo?= =?UTF-8?q?rd=20->=20password=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java | 4 ++-- .../java/BookPick/mvp/domain/auth/service/AuthService.java | 4 ++-- .../mvp/domain/auth/service/MyUserDetailsService.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java index 1f114ed..464065f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java @@ -11,7 +11,7 @@ public class AuthDtos { // -- SignUp -- public record SignReq( @NotBlank @Email String email, - @Size(min = 8, max = 72) String passWord + @Size(min = 8, max = 72) String password ) { } @@ -26,7 +26,7 @@ public static SignRes from(long userId) { // -- Login -- public record LoginReq( @NotBlank @Email String email, - @Size(min = 8, max = 72) String passWord + @Size(min = 8, max = 72) String password ) { } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index c96299f..c7e97b9 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -44,7 +44,7 @@ public SignRes signUp(SignReq req) { // 2. 신규 유저 생성 User user = User.builder() .email(req.email()) - .password(passwordEncoder.encode(req.passWord())) + .password(passwordEncoder.encode(req.password())) .role(Roles.ROLE_USER) .build(); @@ -59,7 +59,7 @@ public SignRes signUp(SignReq req) { // access Token O, refresh X @Transactional public LoginRes login(LoginReq req, HttpServletResponse res) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.passWord()); // 임시로 이메일과 아이디가 담김 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 try { // getObject : AuthenticationManager 객체 반환 diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index ea338b2..3fa0ea0 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -48,7 +48,7 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep } // 프로바이더가 토큰으로 받은 비밀번호와 비교할 DB에서 조회한 User객체 반환 -> 해당 유저 객체의 비밀번호를 프로바이더가 사용한다. - CustomUserDetails customUserDetails = new CustomUserDetails(user, auth); // email, passWord, authorities 등록 + CustomUserDetails customUserDetails = new CustomUserDetails(user, auth); // email, password, authorities 등록 customUserDetails.setId(user.getId()); customUserDetails.setNickname(user.getNickname()); customUserDetails.setBio(user.getBio()); From 08238cba2018e6445b0632cf6b65b1ae5fb0be0a Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 2 Nov 2025 16:54:50 +0900 Subject: [PATCH 065/291] =?UTF-8?q?fix=20:=20jpa=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EB=B3=80=EA=B2=BD=20moods=20->=20mood?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/auth/service/AuthService.java | 2 -- .../mvp/domain/preference/entity/ReadingPreference.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index c7e97b9..cd81542 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -62,8 +62,6 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 try { - // getObject : AuthenticationManager 객체 반환 - // .authenticate : Authenticate를 상속한 구현체의 인스턴스를 검증한다. Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 firstLoginCheck(req.email()); diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java index 88c757e..349eedd 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java @@ -32,7 +32,7 @@ public class ReadingPreference { @ElementCollection @CollectionTable(name = "preference_moods", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "moods") + @Column(name = "mood") private List moods; @ElementCollection From c9c0e5409340050aa694f178e4f2b5897cb92dab Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 3 Nov 2025 22:24:49 +0900 Subject: [PATCH 066/291] =?UTF-8?q?feat=20:=20feat:=20CommentController=20?= =?UTF-8?q?=EC=B4=88=EC=95=88=20=EC=9E=91=EC=84=B1=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 31 +++++++++++++++++++ .../domain/comment/dto/CreateCommentReq.java | 30 ++++++++++++++++++ .../domain/comment/dto/CreateCommentRes.java | 3 ++ .../BookPick/mvp/global/api/SuccessCode.java | 2 +- 4 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java new file mode 100644 index 0000000..ba35bf2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.comment.controller; + +import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.comment.dto.CreateCommentReq; +import BookPick.mvp.domain.comment.dto.CreateCommentRes; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/comment") +public class CommentController { + + // 1. create post + + public ResponseEntity> createComment(@Valid @RequestBody CreateCommentReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + + CreateCommentRes res =null; + + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); + + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java b/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java new file mode 100644 index 0000000..43d02c3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java @@ -0,0 +1,30 @@ +package BookPick.mvp.domain.comment.dto; + +/* +"{ + ""status"": 201, + ""message"": ""댓글이 성공적으로 등록되었습니다."", + ""data"": { + ""commentId"": 71, + ""postId"": 8, + ""parentId"": null, + ""nickname"": ""책읽는밤"", + ""profileImageUrl"": ""https://example.com/profile/reader.png"", + ""content"": ""이번 큐레이션 덕분에 좋은 책을 발견했어요!"", + ""createdAt"": ""2025-10-10T20:15:00"" + } +} +" +*/ + +public record CreateCommentReq( + Long commentId, + Long postId, + Long parentId, + String nickName, + String profileImageUrl, + String content, + +) { + +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java new file mode 100644 index 0000000..9f1daf7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java @@ -0,0 +1,3 @@ +package BookPick.mvp.domain.comment.dto; + +public class CreateCommentRes { diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode.java index 9fcb7e2..45b168c 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode.java @@ -25,9 +25,9 @@ public enum SuccessCode { // -- Book -- BOOK_LIST_READ_SUCCESS(HttpStatus.OK, "책 목록을 성공적으로 조회하였습니다."), + READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."), // -- Reading Preference -- - READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."), READING_PREFERENCE_READ_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 조회하였습니다."), READING_PREFERENCE_UPDATE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 수정하였습니다."), READING_PREFERENCE_DELETE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 삭제하였습니다."), From 1b96c2919e878d0a9631b4e7ff57e32aa00175b3 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 3 Nov 2025 22:26:38 +0900 Subject: [PATCH 067/291] =?UTF-8?q?chore=20:=20Exception=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/CurationAccessDeniedException.java | 2 +- .../exception/CurationNotFoundException.java | 2 +- .../mvp/domain/curation/service/CurationService.java | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/{converter => }/exception/CurationAccessDeniedException.java (84%) rename src/main/java/BookPick/mvp/domain/curation/{converter => }/exception/CurationNotFoundException.java (83%) diff --git a/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationAccessDeniedException.java b/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationAccessDeniedException.java rename to src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java index 218b172..f44fe16 100644 --- a/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationAccessDeniedException.java +++ b/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java @@ -1,5 +1,5 @@ // CurationAccessDeniedException.java -package BookPick.mvp.domain.curation.converter.exception; +package BookPick.mvp.domain.curation.exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationNotFoundException.java b/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationNotFoundException.java rename to src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java index f90896c..ed95673 100644 --- a/src/main/java/BookPick/mvp/domain/curation/converter/exception/CurationNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java @@ -1,5 +1,5 @@ // CurationNotFoundException.java -package BookPick.mvp.domain.curation.converter.exception; +package BookPick.mvp.domain.curation.exception; import BookPick.mvp.global.api.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index 338024c..79a1765 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -12,8 +12,8 @@ import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.converter.exception.CurationAccessDeniedException; -import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.service.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; @@ -21,11 +21,7 @@ import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; From 8c1974ddde696704b6eb8a63a32ca3d657def2aa Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 4 Nov 2025 21:50:46 +0900 Subject: [PATCH 068/291] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/dto/Create/AuthDtos.java | 2 +- .../mvp/domain/auth/service/AuthService.java | 3 +- .../auth/service/CustomUserDetails.java | 40 ++++++ .../auth/service/MyUserDetailsService.java | 40 ------ .../comment/controller/CommentController.java | 79 +++++++++-- .../domain/comment/dto/CreateCommentReq.java | 30 ---- .../domain/comment/dto/CreateCommentRes.java | 3 - .../comment/dto/create/CommentCreateReq.java | 15 ++ .../comment/dto/create/CommentCreateRes.java | 13 ++ .../comment/dto/delete/CommentDeleteRes.java | 13 ++ .../comment/dto/read/CommentDetailRes.java | 31 +++++ .../comment/dto/read/CommentListRes.java | 39 ++++++ .../comment/dto/update/CommentUpdateReq.java | 7 + .../comment/dto/update/CommentUpdateRes.java | 26 ++++ .../mvp/domain/comment/entity/Comment.java | 52 +++++++ .../exception/CommentNotFoundException.java | 12 ++ .../comment/repository/CommentRepository.java | 13 ++ .../comment/service/CommentService.java | 129 ++++++++++++++++++ .../controller/CurationController.java | 5 +- .../ReadingPreferenceController.java | 2 +- .../BookPick/mvp/domain/user/entity/User.java | 2 +- .../BookPick/mvp/global/api/ErrorCode.java | 2 +- .../BookPick/mvp/global/config/JwtConfig.java | 2 +- .../BookPick/mvp/global/config/JwtFilter.java | 4 +- .../mvp/global/config/SecurityConfig.java | 2 + .../BookPick/mvp/global/util/JwtUtil.java | 28 ++-- 26 files changed, 477 insertions(+), 117 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java delete mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/delete/CommentDeleteRes.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/read/CommentDetailRes.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/entity/Comment.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/service/CommentService.java diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java index 464065f..95244ea 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.auth.dto.Create; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Email; diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index cd81542..d2e8f44 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -11,7 +11,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -70,7 +69,7 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { String refreshToken = JwtUtil.createRefreshToken(auth); // Refresh X - MyUserDetailsService.CustomUserDetails customUserDetails = (MyUserDetailsService.CustomUserDetails) auth.getPrincipal(); + CustomUserDetails customUserDetails = (CustomUserDetails) auth.getPrincipal(); return LoginRes.from(customUserDetails, "Bearer " + accessToken); diff --git a/src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java b/src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java new file mode 100644 index 0000000..2618024 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java @@ -0,0 +1,40 @@ +package BookPick.mvp.domain.auth.service; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.Collection; + +@Getter +@Setter +public class CustomUserDetails extends User { + private Long id; + private String nickname; + private String bio; + private String profileImageUrl; + private boolean isFirstLogin; + + public CustomUserDetails( + BookPick.mvp.domain.user.entity.User user, + Collection authorities + ) { + super(user.getEmail(), user.getPassword(), authorities); + this.bio = user.getBio(); + this.profileImageUrl = user.getProfileImageUrl(); + } + + public CustomUserDetails( + Long userId, + String email, + Collection authorities + ) { + super(email, "", authorities); + this.id = userId; + } + + static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities) { + return new CustomUserDetails(userId, email, authorities); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 3fa0ea0..6fc57a9 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -4,26 +4,17 @@ import BookPick.mvp.domain.auth.Roles; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; -import lombok.Builder; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import javax.management.relation.Role; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import static org.apache.coyote.http11.Constants.a; - // 스프링 시큐리티에서 자동으로 호출 @@ -59,36 +50,5 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep } - @Getter - @Setter - public static class CustomUserDetails extends User { - private Long id; - private String nickname; - private String bio; - private String profileImageUrl; - private boolean isFirstLogin; - - public CustomUserDetails( - BookPick.mvp.domain.user.entity.User user, - Collection authorities - ) { - super(user.getEmail(), user.getPassword(), authorities); - this.bio = user.getBio(); - this.profileImageUrl = user.getProfileImageUrl(); - } - - public CustomUserDetails( - Long userId, - String email, - Collection authorities - ){ - super(email, "", authorities); - this.id=userId; - } - - static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities){ - return new CustomUserDetails(userId, email, authorities); - } - } } diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index ba35bf2..0df2b5d 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -1,31 +1,82 @@ package BookPick.mvp.domain.comment.controller; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; -import BookPick.mvp.domain.comment.dto.CreateCommentReq; -import BookPick.mvp.domain.comment.dto.CreateCommentRes; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; +import BookPick.mvp.domain.comment.dto.create.CommentCreateRes; +import BookPick.mvp.domain.comment.dto.delete.CommentDeleteRes; +import BookPick.mvp.domain.comment.dto.read.CommentDetailRes; +import BookPick.mvp.domain.comment.dto.read.CommentListRes; +import BookPick.mvp.domain.comment.dto.update.CommentUpdateReq; +import BookPick.mvp.domain.comment.dto.update.CommentUpdateRes; +import BookPick.mvp.domain.comment.service.CommentService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; -import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/comment") +@RequestMapping("/api/v1/curations") +@RequiredArgsConstructor public class CommentController { + private final CommentService commentService; - // 1. create post + // -- 1. 댓글 생성 -- + @PostMapping("/{curationId}/comments") + public ResponseEntity> create(@PathVariable Long curationId, + @RequestBody CommentCreateReq commentCreateReq, @AuthenticationPrincipal CustomUserDetails currentUser) { + CommentCreateRes res = commentService.createComment(currentUser.getId(), curationId, commentCreateReq); - public ResponseEntity> createComment(@Valid @RequestBody CreateCommentReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); + } - CreateCommentRes res =null; + // -- 2. 댓글 조회 -- + @GetMapping("/{curationId}/comments") + public ResponseEntity> getCommentList(@PathVariable Long curationId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + CommentListRes res = commentService.getCommentList(curationId, page, size); + if (res.comments().isEmpty()) { + return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_EMPTY, res)); + } - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); - + return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_READ_SUCCESS, res)); } + @GetMapping("/{curationId}/comments/{commentId}") + public ResponseEntity> getCommentDetail(@PathVariable Long curationId ,@PathVariable Long commentId) { + CommentDetailRes res = commentService.getCommentDetail(commentId); + return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_READ_SUCCESS, res)); + } + + + // -- 3. 댓글 수정 -- + @PatchMapping("/{curationId}/comments/{commentId}") + public ResponseEntity> updateComment( + @PathVariable Long curationId, + @PathVariable Long commentId, + @RequestBody CommentUpdateReq req + ) { + CommentUpdateRes res = commentService.updateComment(commentId, req); + return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_UPDATE_SUCCESS, res)); + } + + + // -- 4. 댓글 삭제 -- + @DeleteMapping("/{curationId}/comments/{commentId}") + public ResponseEntity> deleteComment( + @PathVariable Long curationId, + @PathVariable Long commentId + ) { + CommentDeleteRes res = commentService.deleteComment(commentId); + return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_DELETE_SUCCESS, res)); + } + + } + + + + + diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java b/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java deleted file mode 100644 index 43d02c3..0000000 --- a/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentReq.java +++ /dev/null @@ -1,30 +0,0 @@ -package BookPick.mvp.domain.comment.dto; - -/* -"{ - ""status"": 201, - ""message"": ""댓글이 성공적으로 등록되었습니다."", - ""data"": { - ""commentId"": 71, - ""postId"": 8, - ""parentId"": null, - ""nickname"": ""책읽는밤"", - ""profileImageUrl"": ""https://example.com/profile/reader.png"", - ""content"": ""이번 큐레이션 덕분에 좋은 책을 발견했어요!"", - ""createdAt"": ""2025-10-10T20:15:00"" - } -} -" -*/ - -public record CreateCommentReq( - Long commentId, - Long postId, - Long parentId, - String nickName, - String profileImageUrl, - String content, - -) { - -} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java deleted file mode 100644 index 9f1daf7..0000000 --- a/src/main/java/BookPick/mvp/domain/comment/dto/CreateCommentRes.java +++ /dev/null @@ -1,3 +0,0 @@ -package BookPick.mvp.domain.comment.dto; - -public class CreateCommentRes { diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateReq.java b/src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateReq.java new file mode 100644 index 0000000..24e171a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateReq.java @@ -0,0 +1,15 @@ +package BookPick.mvp.domain.comment.dto.create; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +// Todo. 각 리코드 분리 필요 +// -- C -- +public record CommentCreateReq( + Long parentId, + + @NotBlank(message = "댓글 내용은 비워둘 수 없습니다.") + @Size(max = 1000, message = "댓글은 최대 1000자까지 입력 가능합니다.") + String content +) { +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateRes.java new file mode 100644 index 0000000..87551d8 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/create/CommentCreateRes.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.comment.dto.create; + +import BookPick.mvp.domain.comment.entity.Comment; + +public record CommentCreateRes( + Long commentId +) { + public static CommentCreateRes from(Comment comment) { + return new CommentCreateRes( + comment.getId() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/delete/CommentDeleteRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/delete/CommentDeleteRes.java new file mode 100644 index 0000000..4fa0ad9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/delete/CommentDeleteRes.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.comment.dto.delete; + +import java.time.LocalDateTime; + +// -- D -- +public record CommentDeleteRes( + Long commentId, + LocalDateTime deletedAt +) { + public static CommentDeleteRes of(Long commentId, LocalDateTime deletedAt) { + return new CommentDeleteRes(commentId, deletedAt); + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentDetailRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentDetailRes.java new file mode 100644 index 0000000..c12bec3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentDetailRes.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.comment.dto.read; + +import BookPick.mvp.domain.comment.entity.Comment; + +import java.time.LocalDateTime; + +public record CommentDetailRes( + Long commentId, + Long parentId, + Long curationId, + Long userId, + String nickname, + String profileImageUrl, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt +) { + public static CommentDetailRes of(Comment comment) { + return new CommentDetailRes( + comment.getId(), + comment.getParent() != null ? comment.getParent().getId() : null, + comment.getCuration().getId(), + comment.getUser().getId(), + comment.getUser().getNickname(), + comment.getUser().getProfileImageUrl(), + comment.getContent(), + comment.getCreatedAt(), + comment.getUpdatedAt() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java new file mode 100644 index 0000000..7fee5cd --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java @@ -0,0 +1,39 @@ +package BookPick.mvp.domain.comment.dto.read; + +import BookPick.mvp.global.dto.PageInfo; + +import java.time.LocalDateTime; +import java.util.List; + +// -- R -- +public record CommentListRes( + List comments, + PageInfo pageInfo +) { + public static CommentListRes of(List comments, PageInfo pageInfo) { + return new CommentListRes(comments, pageInfo); + } + + // 📝 댓글 요약 DTO + public record CommentSummary( + Long commentId, + Long parentId, + String nickname, + String profileImageUrl, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + public static CommentSummary of( + Long commentId, + Long parentId, + String nickname, + String profileImageUrl, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + return new CommentSummary(commentId, parentId, nickname, profileImageUrl, content, createdAt, updatedAt); + } + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateReq.java b/src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateReq.java new file mode 100644 index 0000000..cfc76c4 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateReq.java @@ -0,0 +1,7 @@ +package BookPick.mvp.domain.comment.dto.update; + +// -- U -- +public record CommentUpdateReq( + String content +) { +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateRes.java new file mode 100644 index 0000000..5a1b6ed --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateRes.java @@ -0,0 +1,26 @@ +package BookPick.mvp.domain.comment.dto.update; + + +import BookPick.mvp.domain.comment.entity.Comment; + +import java.time.LocalDateTime; + +public record CommentUpdateRes( + Long commentId, + Long parentId, + String content, + String nickname, + String profileImage, + LocalDateTime updatedAt +) { + public static CommentUpdateRes of(Comment comment) { + return new CommentUpdateRes( + comment.getId(), + comment.getParent() != null ? comment.getParent().getId() : null, + comment.getContent(), + comment.getUser().getNickname(), + comment.getUser().getProfileImageUrl(), + comment.getUpdatedAt() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java new file mode 100644 index 0000000..d1cd19a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java @@ -0,0 +1,52 @@ +package BookPick.mvp.domain.comment.entity; + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.user.entity.User; +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Table(name = "comment") +@Builder +@AllArgsConstructor +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "comment_id") + private Long id; + + @ManyToOne + @JoinColumn(name = "parent_comment_id") + private Comment parent; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "curation_id") + private Curation curation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Size(max = 1000) + private String content; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + + public Comment() { + } +} + diff --git a/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java b/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java new file mode 100644 index 0000000..439c9ec --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.comment.exception; + + +import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CommentNotFoundException extends BusinessException { + public CommentNotFoundException(){ + super(ErrorCode.COMMENT_NOT_FOUND); + } + +} diff --git a/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java b/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java new file mode 100644 index 0000000..d0c2693 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.comment.repository; + +import BookPick.mvp.domain.comment.entity.Comment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CommentRepository extends JpaRepository { + + Page findByCurationId(Long curationId, Pageable pageable); +} diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java new file mode 100644 index 0000000..4f74122 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -0,0 +1,129 @@ +package BookPick.mvp.domain.comment.service; + +import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; +import BookPick.mvp.domain.comment.dto.create.CommentCreateRes; +import BookPick.mvp.domain.comment.dto.delete.CommentDeleteRes; +import BookPick.mvp.domain.comment.dto.read.CommentDetailRes; +import BookPick.mvp.domain.comment.dto.read.CommentListRes; +import BookPick.mvp.domain.comment.dto.update.CommentUpdateReq; +import BookPick.mvp.domain.comment.dto.update.CommentUpdateRes; +import BookPick.mvp.domain.comment.entity.Comment; +import BookPick.mvp.domain.comment.exception.CommentNotFoundException; +import BookPick.mvp.domain.comment.repository.CommentRepository; +import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.global.dto.PageInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CommentService { + private final UserRepository userRepository; + private final CurationRepository curationRepository; + private final CommentRepository commentRepository; + + + // -- C -- + public CommentCreateRes createComment(Long userId, Long curationId, CommentCreateReq req) { + + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + Comment parent = null; + + if (req.parentId() != null) { + parent = commentRepository.findById(req.parentId()) + .orElseThrow(CommentNotFoundException::new); + } + + Comment comment = Comment.builder() + .user(user) + .parent(parent) + .curation(curation) + .content(req.content()) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + Comment saved = commentRepository.save(comment); + + return CommentCreateRes.from(saved); + } + + + // -- R -- + @Transactional(readOnly = true) + public CommentListRes getCommentList(Long curationId, int page, int size) { + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page commentPage = commentRepository.findByCurationId(curationId, pageable); + + List commentList = commentPage.getContent().stream() + .map(comment -> CommentListRes.CommentSummary.of( + comment.getId(), + comment.getParent() != null ? comment.getParent().getId() : null, + comment.getUser().getNickname(), + comment.getUser().getProfileImageUrl(), + comment.getContent(), + comment.getCreatedAt(), + comment.getUpdatedAt() + )) + .toList(); + + PageInfo pageInfo = PageInfo.of(commentPage); + + return CommentListRes.of(commentList, pageInfo); + } + + @Transactional(readOnly = true) + public CommentDetailRes getCommentDetail(Long commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + return CommentDetailRes.of(comment); + } + + + // -- U -- + @Transactional + public CommentUpdateRes updateComment(Long commentId, CommentUpdateReq req) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + if (req.content() != null && !req.content().isBlank()) { + comment.setContent(req.content()); + comment.setUpdatedAt(LocalDateTime.now()); + } + + return CommentUpdateRes.of(comment); + } + + + // -- D -- + @Transactional + public CommentDeleteRes deleteComment(Long commentId) { + Comment comment = commentRepository.findById(commentId) + .orElseThrow(CommentNotFoundException::new); + + commentRepository.delete(comment); + + return CommentDeleteRes.of(commentId, LocalDateTime.now()); + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index 270f06b..e7df5ac 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -1,7 +1,7 @@ // CurationController.java에 추가 package BookPick.mvp.domain.curation.controller; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.SortType; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; @@ -11,14 +11,11 @@ import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.CurationService; -import BookPick.mvp.global.HyperParam.Defaults; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java index 646ebea..da05cb6 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.preference.controller; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateReq; import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateRes; import BookPick.mvp.domain.preference.dto.Delete.ReadingPreferenceDeleteRes; @@ -7,7 +8,6 @@ import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.preference.service.ReadingPreferenceService; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; import jakarta.validation.Valid; diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index b9987d4..d8a30a9 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -22,7 +22,7 @@ public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "userId") + @Column(name = "user_Id") private Long id; // 내부 식별자 (PK) @Column(name = "login_email", nullable = false, unique = true, length = 255) diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode.java index 442d203..9049f44 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode.java @@ -21,7 +21,7 @@ public enum ErrorCode { User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 - // -- Post -- + // -- Curation -- POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), //404 diff --git a/src/main/java/BookPick/mvp/global/config/JwtConfig.java b/src/main/java/BookPick/mvp/global/config/JwtConfig.java index 966037f..fb9f5d1 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtConfig.java +++ b/src/main/java/BookPick/mvp/global/config/JwtConfig.java @@ -13,7 +13,7 @@ public class JwtConfig { @Value("${jwt.access.secret}") - private static String accessSecret; + private String accessSecret; @Value("${jwt.access.expiration}") private long accessSecretExp; diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 88e2663..82a8ba1 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,10 +1,8 @@ package BookPick.mvp.global.config; -import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.global.util.JwtUtil; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 947ce95..05e89f6 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -3,6 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -40,6 +41,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/**").permitAll() .requestMatchers("/api/v1/signup", "/api/v1/login", "/api/v1/logout").permitAll() .requestMatchers("/api/v1/users/*/preferences").permitAll() + .requestMatchers(HttpMethod.GET,"/api/v1/curations/*/comments").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() .requestMatchers("/error").permitAll() .anyRequest().authenticated() diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 7dcc9cf..0e6e2fe 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.io.Decoders; @@ -28,7 +28,7 @@ public class JwtUtil { )); // (추가) 토큰 수명 상수 - private static final long ACCESS_TTL_MS = 1000L * 60 * 60; // 1시간 + private static final long ACCESS_TTL_MS = 1000L * 60 * 60; // 1시간 private static final long REFRESH_TTL_MS = 1000L * 60 * 60 * 24 * 14; // 14일 // 2. JWT 생성 @@ -36,17 +36,16 @@ public static String createAccessToken(Authentication auth) { CustomUserDetails usr = (CustomUserDetails) auth.getPrincipal(); String authorities = auth.getAuthorities().stream() //getAuthorities -> List return - .map(a->a.getAuthority()) // getAuthority() -> String return + .map(a -> a.getAuthority()) // getAuthority() -> String return .collect(Collectors.joining(",")); - String jwt = Jwts.builder() .claim("userId", usr.getId()) .claim("email", usr.getUsername()) .claim("authorities", authorities) .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + 1000*60*60)) // expiration : 만료 + .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // expiration : 만료 .signWith(key) .compact(); return jwt; @@ -70,20 +69,17 @@ public static String createRefreshToken(Authentication auth) { //3. JWT 오픈 public static Claims extractToken(String token) { - try{ + try { Claims claims = Jwts.parser().verifyWith(key).build() - .parseSignedClaims(token).getPayload(); - return claims; + .parseSignedClaims(token).getPayload(); + return claims; } catch (ExpiredJwtException e) { - throw new JwtTokenExpiredException(); - } catch (JwtException | IllegalArgumentException e) { - throw new InvalidTokenTypeException(); - } - - } - - + throw new JwtTokenExpiredException(); + } catch (JwtException | IllegalArgumentException e) { + throw new InvalidTokenTypeException(); + } } +} From f787cda457c5669678f9807981643e5861469304 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 5 Nov 2025 22:42:13 +0900 Subject: [PATCH 069/291] =?UTF-8?q?feat=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=ED=8E=B8=EC=A7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 9 ++++-- .../book/Controller/BookSearchController.java | 2 ++ .../controller/CurationController.java | 31 ++++++------------- .../ReadingPreferenceController.java | 24 +++++++------- .../mvp/global/config/SwaggerConfig.java | 16 +++++++--- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 9765e8f..997f3e7 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -4,6 +4,7 @@ import BookPick.mvp.domain.auth.dto.Create.AuthDtos.*; import BookPick.mvp.domain.auth.service.AuthService; import BookPick.mvp.global.api.SuccessCode; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -11,6 +12,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import io.swagger.v3.oas.annotations.Operation; + @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor @@ -18,20 +21,20 @@ public class AuthController { private final AuthService authService; + @Operation(summary = "회원가입", description = "사용자 회원가입", tags = {"Auth"}) @PostMapping("/signup") public ResponseEntity> signUp(@Valid @RequestBody SignReq req) { SignRes signRes = authService.signUp(req); - return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS, signRes)); } - + @Operation(summary = "로그인", description = "사용자 로그인", tags = {"Auth"}) @PostMapping("/login") public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res) { LoginRes loginRes = authService.login(req, res); - return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, loginRes)); } } + diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index 62bc216..1b9aa07 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -4,6 +4,7 @@ import BookPick.mvp.domain.book.service.BookSearchService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,6 +20,7 @@ public class BookSearchController { private final BookSearchService bookSearchService; + @Operation(summary = "책 검색", description = "검색어로 책 목록 조회", tags = {"Book Search"}) @PostMapping("/search") public ResponseEntity> searchBookList(@RequestBody BookSearchReq req){ BookSearchPageRes res = bookSearchService.getBookSearchList(req); diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index 270f06b..5c8193e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -24,6 +24,8 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import io.swagger.v3.oas.annotations.Operation; + @RestController @RequestMapping("/api/v1/curations") @RequiredArgsConstructor @@ -31,75 +33,60 @@ public class CurationController { private final CurationService curationService; - // -- 큐레이션 생성 -- + @Operation(summary = "큐레이션 생성", description = "새 큐레이션을 생성합니다", tags = {"Curation"}) @PostMapping public ResponseEntity> create( @Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { CurationCreateRes res = curationService.create(currentUser.getId(), req); - return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); } - // -- 큐레이션 단건 조회 -- + @Operation(summary = "큐레이션 단건 조회", description = "큐레이션 ID로 단건 조회", tags = {"Curation"}) @GetMapping("/{curationId}") public ResponseEntity> getCuration( @PathVariable Long curationId, HttpServletRequest req) { CurationGetRes res = curationService.findCuration(curationId, req); - return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); } - - // -- 큐레이션 목록 조회 -- + @Operation(summary = "큐레이션 목록 조회", description = "큐레이션 목록을 페이징 조회", tags = {"Curation"}) @GetMapping public ResponseEntity> getCurationList( @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size) { - SortType sortType = SortType.fromValue(sort); - CurationListGetRes curationListGetRes = curationService.getCurationList(sortType, cursor, size); - return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); } - - // -- 큐레이션 수정 -- + @Operation(summary = "큐레이션 수정", description = "큐레이션 정보를 수정", tags = {"Curation"}) @PatchMapping("/{curationId}") public ResponseEntity> updateCuration( @PathVariable Long curationId, @Valid @RequestBody CurationUpdateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { CurationUpdateRes res = curationService.modifyCuration(currentUser.getId(), curationId, req); - return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); } - - // -- 큐레이션 삭제 -- + @Operation(summary = "큐레이션 삭제", description = "큐레이션을 삭제합니다", tags = {"Curation"}) @DeleteMapping("/{curationId}") public ResponseEntity> deleteCuration( @PathVariable Long curationId, @AuthenticationPrincipal CustomUserDetails currentUser) { - Long userId; - - if (currentUser == null) { - userId = 2L; - } else { - userId = currentUser.getId(); - } + Long userId = (currentUser == null) ? 2L : currentUser.getId(); CurationDeleteRes res = curationService.removeCuration(userId, curationId); - return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); } } + diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java index 646ebea..e84c4c4 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java @@ -17,6 +17,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import io.swagger.v3.oas.annotations.Operation; @RestController @RequestMapping("/api/v1/reading-preference") @@ -25,44 +26,43 @@ public class ReadingPreferenceController { private final ReadingPreferenceService readingPreferenceService; + @Operation(summary = "독서 취향 생성", description = "사용자의 독서 취향을 등록합니다", tags = {"Reading Preference"}) @PostMapping - public ResponseEntity> create(@Valid @RequestBody ReadingPreferenceCreateReq req, - @AuthenticationPrincipal CustomUserDetails currentUser) { + public ResponseEntity> create( + @Valid @RequestBody ReadingPreferenceCreateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceCreateRes res = readingPreferenceService.addReadingPreference(currentUser.getId(), req); - return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_REGISTER_SUCCESS, res)); } + @Operation(summary = "독서 취향 조회", description = "사용자의 독서 취향 상세 조회", tags = {"Reading Preference"}) @GetMapping public ResponseEntity> getDetails( - @AuthenticationPrincipal CustomUserDetails currentUser) { + @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceGetRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); - return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_READ_SUCCESS, res)); } + @Operation(summary = "독서 취향 수정", description = "사용자의 독서 취향을 수정합니다", tags = {"Reading Preference"}) @PatchMapping - public ResponseEntity> update(@Valid @RequestBody ReadingPreferenceUpdateReq req, - @AuthenticationPrincipal CustomUserDetails currentUser) { + public ResponseEntity> update( + @Valid @RequestBody ReadingPreferenceUpdateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceUpdateRes res = readingPreferenceService.modifyReadingPreference(currentUser.getId(), req); - return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res)); - } + @Operation(summary = "독서 취향 삭제", description = "사용자의 독서 취향을 삭제합니다", tags = {"Reading Preference"}) @DeleteMapping public ResponseEntity> delete( @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceDeleteRes res = readingPreferenceService.removeReadingPreference(currentUser.getId()); - return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_DELETE_SUCCESS, res)); - } - } diff --git a/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java b/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java index 5dfb400..d2d5c47 100644 --- a/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java @@ -3,23 +3,29 @@ import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.tags.Tag; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -@Configuration // 스프링 실행시 설정파일 읽어드리기 위한 어노테이션 +@Configuration public class SwaggerConfig { @Bean public OpenAPI openAPI() { return new OpenAPI() .components(new Components()) - .info(apiInfo()); + .info(apiInfo()) + // 태그별로 그룹화 + .addTagsItem(new Tag().name("Reading Preference").description("유저 독서 취향 관련 API")) + .addTagsItem(new Tag().name("Curation").description("큐레이션 관련 API")) + .addTagsItem(new Tag().name("Book Search").description("책 검색 관련 API")) + .addTagsItem(new Tag().name("Auth").description("유저 인증 관련 API")); } private Info apiInfo() { return new Info() - .title("CodeArena Swagger") - .description("CodeArena 유저 및 인증 , ps, 알림에 관한 REST API") + .title("Swagger Book Pick ") + .description("블라인드 큐레이션 스토어 API 및 스키마") .version("1.0.0"); } -} \ No newline at end of file +} From 1db8a723bfbd1ff5eb2d95cd71d66d7e99904781 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 5 Nov 2025 22:46:10 +0900 Subject: [PATCH 070/291] =?UTF-8?q?feat=20:=20util=EC=95=84=EB=9E=98?= =?UTF-8?q?=EC=97=90=20=EC=BB=A8=EB=B2=84=ED=84=B0,=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=EB=8D=94=20=EB=93=B1=20=EB=84=A3=EC=9D=84=20?= =?UTF-8?q?=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/controller/CurationController.java | 5 +---- .../mvp/domain/curation/dto/create/CurationCreateRes.java | 2 +- .../domain/curation/dto/get/list/CurationContentRes.java | 2 +- .../domain/curation/dto/get/list/CurationListGetRes.java | 2 +- .../mvp/domain/curation/dto/get/one/CurationGetRes.java | 2 +- .../mvp/domain/curation/dto/update/CurationUpdateRes.java | 2 +- .../mvp/domain/curation/{ => enums}/SortType.java | 2 +- .../mvp/domain/curation/{entity => model}/Curation.java | 2 +- .../domain/curation/repository/CurationRepository.java | 4 +--- .../mvp/domain/curation/service/CurationService.java | 4 ++-- .../curation/service/Handler/CurationPageHandler.java | 4 ++-- .../domain/curation/service/fetcher/CurationFetcher.java | 8 ++------ .../curation/service/provider/CurationLatestProvider.java | 2 +- .../service/provider/CurationPopularProvider.java | 2 +- .../service/provider/CurationSimilarityProvider.java | 2 +- .../{ => util}/converter/StringListConverter.java | 2 +- .../java/BookPick/mvp/global/HyperParam/Defaults.java | 2 +- .../mvp/integration/gemini/dto/CurationMatchResult.java | 2 +- .../mvp/integration/gemini/service/GeminiService.java | 2 +- src/test/java/BookPick/mvp/GeminiClientMain.java | 2 +- 20 files changed, 23 insertions(+), 32 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/{ => enums}/SortType.java (93%) rename src/main/java/BookPick/mvp/domain/curation/{entity => model}/Curation.java (98%) rename src/main/java/BookPick/mvp/domain/curation/{ => util}/converter/StringListConverter.java (94%) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java index 5c8193e..47156a2 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java @@ -2,7 +2,7 @@ package BookPick.mvp.domain.curation.controller; import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; -import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.enums.SortType; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; @@ -11,14 +11,11 @@ import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.CurationService; -import BookPick.mvp.global.HyperParam.Defaults; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java index 7e2cb47..fff42f8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.dto.create; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; public record CurationCreateRes( Long id diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java index 5619ba6..4edabf5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.dto.get.list; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; public record CurationContentRes( Long curationId, diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java index 83f112a..e8df3d7 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java @@ -1,7 +1,7 @@ // CurationListGetRes.java package BookPick.mvp.domain.curation.dto.get.list; -import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.enums.SortType; import java.util.List; public record CurationListGetRes( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java index d374e73..f2ed07d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java @@ -1,7 +1,7 @@ // CurationGetRes.java package BookPick.mvp.domain.curation.dto.get.one; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import java.time.LocalDateTime; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java index e0f0bc8..f443909 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java @@ -1,7 +1,7 @@ // CurationUpdateRes.java package BookPick.mvp.domain.curation.dto.update; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; public record CurationUpdateRes( Long id diff --git a/src/main/java/BookPick/mvp/domain/curation/SortType.java b/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java similarity index 93% rename from src/main/java/BookPick/mvp/domain/curation/SortType.java rename to src/main/java/BookPick/mvp/domain/curation/enums/SortType.java index de5f44b..9fb5136 100644 --- a/src/main/java/BookPick/mvp/domain/curation/SortType.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java @@ -1,5 +1,5 @@ // SortType.java -package BookPick.mvp.domain.curation; +package BookPick.mvp.domain.curation.enums; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/model/Curation.java similarity index 98% rename from src/main/java/BookPick/mvp/domain/curation/entity/Curation.java rename to src/main/java/BookPick/mvp/domain/curation/model/Curation.java index dba3930..5e2faec 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/model/Curation.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.entity; +package BookPick.mvp.domain.curation.model; import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; import BookPick.mvp.domain.user.entity.User; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 277bcd7..953de54 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -1,13 +1,11 @@ package BookPick.mvp.domain.curation.repository; -import BookPick.mvp.domain.curation.entity.Curation; -import org.springframework.data.domain.Page; +import BookPick.mvp.domain.curation.model.Curation; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.time.LocalDateTime; import java.util.List; public interface CurationRepository extends JpaRepository { diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java index 79a1765..7152ec7 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java @@ -1,7 +1,7 @@ // CurationService.java package BookPick.mvp.domain.curation.service; -import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.enums.SortType; import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; @@ -11,7 +11,7 @@ import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java index bd801f3..99f4f60 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java @@ -1,10 +1,10 @@ package BookPick.mvp.domain.curation.service.Handler; -import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.enums.SortType; import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; import BookPick.mvp.domain.curation.dto.get.list.CursorPage; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java index 87995d6..c6ad712 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java @@ -1,15 +1,11 @@ package BookPick.mvp.domain.curation.service.fetcher; -import BookPick.mvp.domain.curation.SortType; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import org.springframework.stereotype.Component; -import BookPick.mvp.domain.curation.SortType; -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.repository.CurationRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java index fbed1d6..ed24e8c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.service.provider; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java index dc0253f..7d5818c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.curation.service.provider; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java index 915fb90..2899259 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.curation.service.provider; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java b/src/main/java/BookPick/mvp/domain/curation/util/converter/StringListConverter.java similarity index 94% rename from src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java rename to src/main/java/BookPick/mvp/domain/curation/util/converter/StringListConverter.java index 99d2dfd..4161800 100644 --- a/src/main/java/BookPick/mvp/domain/curation/converter/StringListConverter.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/converter/StringListConverter.java @@ -1,5 +1,5 @@ // StringListConverter.java -package BookPick.mvp.domain.curation.converter; +package BookPick.mvp.domain.curation.util.converter; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; diff --git a/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java b/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java index 8f0cbfd..bae1528 100644 --- a/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java +++ b/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java @@ -1,6 +1,6 @@ package BookPick.mvp.global.HyperParam; -import BookPick.mvp.domain.curation.SortType; +import BookPick.mvp.domain.curation.enums.SortType; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java b/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java index c4de097..dd7fa02 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java +++ b/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java @@ -1,6 +1,6 @@ package BookPick.mvp.integration.gemini.dto; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java index 4b849ff..0daee9a 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java @@ -1,7 +1,7 @@ package BookPick.mvp.integration.gemini.service; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.integration.gemini.client.GeminiClient; import BookPick.mvp.integration.gemini.dto.CurationMatchResult; diff --git a/src/test/java/BookPick/mvp/GeminiClientMain.java b/src/test/java/BookPick/mvp/GeminiClientMain.java index a9736a6..f9335f4 100644 --- a/src/test/java/BookPick/mvp/GeminiClientMain.java +++ b/src/test/java/BookPick/mvp/GeminiClientMain.java @@ -1,7 +1,7 @@ package BookPick.mvp; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.integration.gemini.dto.CurationMatchResult; import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; import BookPick.mvp.integration.gemini.service.GeminiService; From 1b1193d4ad9441851a1ae3de0e550dcf75da9889 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 5 Nov 2025 23:31:47 +0900 Subject: [PATCH 071/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20page=20=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java | 3 ++- .../BookPick/mvp/domain/book/service/BookSearchService.java | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java b/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java index fa87250..16ab25f 100644 --- a/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java +++ b/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java @@ -8,7 +8,8 @@ public class BookDtos { // -- R -- public record BookSearchReq( - String keyword + String keyword, + Integer page ){} public record BookSearchRes( String title, diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java index 0f4e51c..12e0ce9 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java @@ -36,7 +36,7 @@ public BookSearchPageRes getBookSearchList(BookSearchReq req) { // 요청 URL 구성 UriComponents uri = UriComponentsBuilder.fromHttpUrl(API_URL) .queryParam("query", req.keyword()) - .queryParam("page", 1) + .queryParam("page", req.page()) .queryParam("size", 10) .build(); @@ -72,7 +72,7 @@ public BookSearchPageRes getBookSearchList(BookSearchReq req) { // PageInfo 매핑 (현재 페이지는 Kakao API 요청 기준) PageInfo pageInfo = new PageInfo( - 1, // currentPage (요청 page) + req.page(), // currentPage (요청 page) (int) Math.ceil((double) totalCount / 10), // totalPages (총 페이지 수) totalCount, // totalElements (총 아이템 수) !isEnd // hasNext (다음 페이지 여부) From feeaec155adfbd5c6ff1583f213e895faa2c10fd Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 00:01:52 +0900 Subject: [PATCH 072/291] =?UTF-8?q?chore=20:=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 3 +- .../mvp/domain/auth/dto/Create/AuthDtos.java | 6 ++-- .../mvp/domain/auth/service/AuthService.java | 7 ++-- .../book/Controller/BookSearchController.java | 2 +- .../{ => base}/CurationController.java | 36 +++++++------------ .../dto/base/create/CurationCreateReq.java | 17 +++++++++ .../{ => base}/create/CurationCreateRes.java | 2 +- .../dto/{ => base}/create/Req/BookDto.java | 2 +- .../dto/base/delete/CurationDeleteReq.java | 4 +++ .../{ => base}/delete/CurationDeleteRes.java | 2 +- .../dto/{ => base}/get/list/BookRes.java | 2 +- .../get/list/CurationContentRes.java | 25 +++++++++++-- .../dto/create/CurationCreateReq.java | 19 ---------- .../dto/delete/CurationDeleteReq.java | 4 --- .../CurationAccessDeniedException.java | 2 +- .../mvp/domain/curation/model/Curation.java | 2 +- .../gemini/prompt/ContentPromptTemplate.java | 2 +- ...yRegisteredReadingPreferenceException.java | 2 +- .../BookPick/mvp/global/api/ApiResponse.java | 6 ++-- .../global/exception/BusinessException.java | 8 +++-- 20 files changed, 80 insertions(+), 73 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/controller/{ => base}/CurationController.java (66%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/create/CurationCreateRes.java (81%) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/create/Req/BookDto.java (62%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/delete/CurationDeleteRes.java (83%) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/get/list/BookRes.java (53%) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/get/list/CurationContentRes.java (50%) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java rename src/main/java/BookPick/mvp/{integration => domain/curation/util}/gemini/prompt/ContentPromptTemplate.java (92%) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java index 997f3e7..cc4b4f9 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java @@ -3,8 +3,7 @@ import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.domain.auth.dto.Create.AuthDtos.*; import BookPick.mvp.domain.auth.service.AuthService; -import BookPick.mvp.global.api.SuccessCode; -import io.swagger.v3.oas.annotations.media.Schema; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java index 1f114ed..95244ea 100644 --- a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java +++ b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.auth.dto.Create; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Email; @@ -11,7 +11,7 @@ public class AuthDtos { // -- SignUp -- public record SignReq( @NotBlank @Email String email, - @Size(min = 8, max = 72) String passWord + @Size(min = 8, max = 72) String password ) { } @@ -26,7 +26,7 @@ public static SignRes from(long userId) { // -- Login -- public record LoginReq( @NotBlank @Email String email, - @Size(min = 8, max = 72) String passWord + @Size(min = 8, max = 72) String password ) { } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index f9ac360..d2e8f44 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -11,7 +11,6 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; @@ -44,7 +43,7 @@ public SignRes signUp(SignReq req) { // 2. 신규 유저 생성 User user = User.builder() .email(req.email()) - .password(passwordEncoder.encode(req.passWord())) + .password(passwordEncoder.encode(req.password())) .role(Roles.ROLE_USER) .build(); @@ -59,7 +58,7 @@ public SignRes signUp(SignReq req) { // access Token O, refresh X @Transactional public LoginRes login(LoginReq req, HttpServletResponse res) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.passWord()); // 임시로 이메일과 아이디가 담김 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 try { Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 @@ -70,7 +69,7 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { String refreshToken = JwtUtil.createRefreshToken(auth); // Refresh X - MyUserDetailsService.CustomUserDetails customUserDetails = (MyUserDetailsService.CustomUserDetails) auth.getPrincipal(); + CustomUserDetails customUserDetails = (CustomUserDetails) auth.getPrincipal(); return LoginRes.from(customUserDetails, "Bearer " + accessToken); diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index 1b9aa07..a372510 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -3,7 +3,7 @@ import BookPick.mvp.domain.book.dto.BookDtos.*; import BookPick.mvp.domain.book.service.BookSearchService; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java similarity index 66% rename from src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java rename to src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 47156a2..521fc36 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -1,18 +1,16 @@ -// CurationController.java에 추가 -package BookPick.mvp.domain.curation.controller; +// CurationListController.java에 추가 +package BookPick.mvp.domain.curation.controller.base; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; -import BookPick.mvp.domain.curation.enums.SortType; -import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; -import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; -import BookPick.mvp.domain.curation.dto.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; -import BookPick.mvp.domain.curation.service.CurationService; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; +import BookPick.mvp.domain.curation.service.base.CurationService; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -50,17 +48,7 @@ public ResponseEntity> getCuration( .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); } - @Operation(summary = "큐레이션 목록 조회", description = "큐레이션 목록을 페이징 조회", tags = {"Curation"}) - @GetMapping - public ResponseEntity> getCurationList( - @RequestParam(defaultValue = "latest") String sort, - @RequestParam(required = false) Long cursor, - @RequestParam(defaultValue = "10") int size) { - SortType sortType = SortType.fromValue(sort); - CurationListGetRes curationListGetRes = curationService.getCurationList(sortType, cursor, size); - return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); - } + @Operation(summary = "큐레이션 수정", description = "큐레이션 정보를 수정", tags = {"Curation"}) @PatchMapping("/{curationId}") diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java new file mode 100644 index 0000000..9ec262e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java @@ -0,0 +1,17 @@ +package BookPick.mvp.domain.curation.dto.base.create; + +import BookPick.mvp.domain.curation.dto.base.create.Req.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.Req.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.Req.ThumbnailDto; + +// 메인 요청 DTO +public record CurationCreateReq( + ThumbnailDto thumbnail, + BookDto book, + String review, + RecommendDto recommend +) {} + + + + diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java similarity index 81% rename from src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java index fff42f8..a4c2f6d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.create; +package BookPick.mvp.domain.curation.dto.base.create; import BookPick.mvp.domain.curation.model.Curation; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/BookDto.java similarity index 62% rename from src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/BookDto.java index 9307164..e6d795c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/BookDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/BookDto.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.create.Req; +package BookPick.mvp.domain.curation.dto.base.create.Req; // 책 정보 public record BookDto( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java new file mode 100644 index 0000000..d4e5156 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.base.delete; + +public class CurationDeleteReq { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java index aba93c1..235fd99 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java @@ -1,5 +1,5 @@ // CurationDeleteRes.java -package BookPick.mvp.domain.curation.dto.delete; +package BookPick.mvp.domain.curation.dto.base.delete; import java.time.LocalDateTime; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java similarity index 53% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java index d4a08c1..12cfa9d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/BookRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.get.list; +package BookPick.mvp.domain.curation.dto.base.get.list; public record BookRes( String title, diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java similarity index 50% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 4edabf5..199535a 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -1,6 +1,7 @@ -package BookPick.mvp.domain.curation.dto.get.list; +package BookPick.mvp.domain.curation.dto.base.get.list; import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; public record CurationContentRes( Long curationId, @@ -25,8 +26,7 @@ public static CurationContentRes from(Curation curation) { curation.getUser().getId(), "닉네임", // TODO: User 조인 필요 new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), - curation.getReview(), - new BookRes(curation.getBookTitle(), curation.getBookAuthor()), + curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), 0, // TODO: 좋아요 수 0, // TODO: 댓글 수 0, // TODO: 조회수 @@ -36,4 +36,23 @@ public static CurationContentRes from(Curation curation) { curation.getCreatedAt().toString() ); } + + public static CurationContentRes from(CurationMatchResult matchResult) { + Curation curation = matchResult.getCuration(); + return new CurationContentRes( + curation.getId(), + curation.getBookTitle(), + curation.getUser().getId(), + "닉네임", // TODO: User 조인 필요 + new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), + curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), + 0, // TODO: 좋아요 수 + 0, // TODO: 댓글 수 + 0, // TODO: 조회수 + null, // TODO: similarity + matchResult.getMatched(), + curation.getPopularityScore(), + curation.getCreatedAt().toString() + ); + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java deleted file mode 100644 index c65ab8f..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/CurationCreateReq.java +++ /dev/null @@ -1,19 +0,0 @@ -package BookPick.mvp.domain.curation.dto.create; - -import BookPick.mvp.domain.curation.dto.create.Req.BookDto; -import BookPick.mvp.domain.curation.dto.create.Req.RecommendDto; -import BookPick.mvp.domain.curation.dto.create.Req.ThumbnailDto; - -import java.util.List; - -// 메인 요청 DTO -public record CurationCreateReq( - ThumbnailDto thumbnail, - BookDto book, - String review, - RecommendDto recommend -) {} - - - - diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java deleted file mode 100644 index 7bf7cb5..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/delete/CurationDeleteReq.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.dto.delete; - -public class CurationDeleteReq { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java b/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java index f44fe16..307efb1 100644 --- a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java +++ b/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java @@ -1,7 +1,7 @@ // CurationAccessDeniedException.java package BookPick.mvp.domain.curation.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class CurationAccessDeniedException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/curation/model/Curation.java b/src/main/java/BookPick/mvp/domain/curation/model/Curation.java index 5e2faec..03a1431 100644 --- a/src/main/java/BookPick/mvp/domain/curation/model/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/model/Curation.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.model; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.AccessLevel; diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java similarity index 92% rename from src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java rename to src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java index f250ac0..788c390 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/prompt/ContentPromptTemplate.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java @@ -1,4 +1,4 @@ -package BookPick.mvp.integration.gemini.prompt; +package BookPick.mvp.domain.curation.util.gemini.prompt; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java b/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java index ae18d15..0593f0e 100644 --- a/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java +++ b/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.preference.Exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class AlreadyRegisteredReadingPreferenceException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/global/api/ApiResponse.java b/src/main/java/BookPick/mvp/global/api/ApiResponse.java index 52173b2..6ce56e1 100644 --- a/src/main/java/BookPick/mvp/global/api/ApiResponse.java +++ b/src/main/java/BookPick/mvp/global/api/ApiResponse.java @@ -1,6 +1,8 @@ package BookPick.mvp.global.api; -import lombok.*; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import org.springframework.http.HttpStatus; @@ -24,7 +26,7 @@ public static ApiResponse success(SuccessCode successCode, T data) { } // -- Error -- - public static ApiResponse error(ErrorCode errorCode) { + public static ApiResponse error(ErrorCodeInterface errorCode) { return new ApiResponse( errorCode.getStatus().value(), errorCode.getMessage(), // @Valid 같은 데서 넘어온 메시지 diff --git a/src/main/java/BookPick/mvp/global/exception/BusinessException.java b/src/main/java/BookPick/mvp/global/exception/BusinessException.java index 56e9ecb..6c6b779 100644 --- a/src/main/java/BookPick/mvp/global/exception/BusinessException.java +++ b/src/main/java/BookPick/mvp/global/exception/BusinessException.java @@ -1,13 +1,15 @@ package BookPick.mvp.global.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; import lombok.Getter; @Getter public class BusinessException extends RuntimeException{ - ErrorCode errorCode; + private final ErrorCodeInterface errorCode; - public BusinessException(ErrorCode errorCode){ + + public BusinessException(ErrorCodeInterface errorCode){ super(errorCode.getMessage()); // 나중에 로그 확인을 위해 런타임 예외 디테일 message 에 저장 this.errorCode=errorCode; } From 3e1f4c709e9bf894180bf0d10b37562fe166921d Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 00:04:25 +0900 Subject: [PATCH 073/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EB=B3=84=20=EC=97=90=EB=9F=AC=20=EA=B4=80=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EC=97=90=EB=9F=AC=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/enums/CurationErrorCode.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java new file mode 100644 index 0000000..24dd1eb --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.curation.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum CurationErrorCode { + + CURATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), + CURATION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "큐레이션 접근 권한이 없습니다."); + + + + private final HttpStatus status; + private final String message; +} \ No newline at end of file From 75fce630b6b0b901a720f9fa33d5a75701a41da7 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 00:05:00 +0900 Subject: [PATCH 074/291] =?UTF-8?q?chore:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EB=94=94=EB=B2=A8=EB=A1=AD=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => base}/get/one/CurationGetRes.java | 2 +- .../curation/dto/read/CurationGetReq.java | 4 -- .../curation/dto/read/CurationGetRes.java | 4 -- .../service/fetcher/CurationFetcher.java | 35 ------------ .../provider/CurationLatestProvider.java | 24 -------- .../util/list/fetcher/CurationFetcher.java | 55 +++++++++++++++++++ 6 files changed, 56 insertions(+), 68 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/get/one/CurationGetRes.java (96%) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java similarity index 96% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index f2ed07d..3a51bde 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -1,5 +1,5 @@ // CurationGetRes.java -package BookPick.mvp.domain.curation.dto.get.one; +package BookPick.mvp.domain.curation.dto.base.get.one; import BookPick.mvp.domain.curation.model.Curation; import java.time.LocalDateTime; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java deleted file mode 100644 index ce38198..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetReq.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.dto.read; - -public class CurationGetReq { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java deleted file mode 100644 index 66dc219..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/read/CurationGetRes.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.dto.read; - -public class CurationGetRes { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java deleted file mode 100644 index c6ad712..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationFetcher.java +++ /dev/null @@ -1,35 +0,0 @@ -package BookPick.mvp.domain.curation.service.fetcher; - -import BookPick.mvp.domain.curation.enums.SortType; -import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import org.springframework.stereotype.Component; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; - -import java.util.List; - - -@Component -@RequiredArgsConstructor -public class CurationFetcher { - - private final CurationRepository curationRepository; - - public List fetchCurations(SortType sortType, Long cursor, Pageable pageable) { - - if (cursor == null) { - if(sortType.equals(SortType.SORT_LATEST)) - return curationRepository.findAllByOrderByCreatedAtDesc(pageable); // 취향 유사도 만들기 전까진 최신순 - } - return switch (sortType) { - case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); - case SORT_LATEST -> curationRepository.findCurations(cursor, pageable); - case SORT_SIMILARITY -> curationRepository.findCurations(cursor, pageable); - }; - } - - public Long calculateNextCursor(List curations, int size, boolean hasNext) { - return hasNext ? curations.get(size).getId() : null; - } -} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java deleted file mode 100644 index ed24e8c..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationLatestProvider.java +++ /dev/null @@ -1,24 +0,0 @@ -package BookPick.mvp.domain.curation.service.provider; - -import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -@RequiredArgsConstructor -public class CurationLatestProvider { - - private final CurationRepository curationRepository; - - public List fetch(Long cursor, Pageable pageable) { - if (cursor == null) { - return curationRepository.findAllByOrderByCreatedAtDesc(pageable); - } - return curationRepository.findCurations(cursor, pageable); - } -} - diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java new file mode 100644 index 0000000..a65af1f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -0,0 +1,55 @@ +package BookPick.mvp.domain.curation.util.list.fetcher; + +import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; +import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.service.list.CurationRecommendationService; +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; +import org.springframework.stereotype.Component; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.stream.Collectors; + + +@Component +@RequiredArgsConstructor +public class CurationFetcher { + + private final CurationRepository curationRepository; + private final CurationRecommendationService curationRecommendationService; + + + // 1. sort Type별로 큐레이션 리스트 가져오기 + public List fetchCurations(SortType sortType, Long cursor, Pageable pageable, ReadingPreferenceInfo readingPreferenceInfo) { + + + // 1) 맨 처음 페이지 로딩 + if (cursor == null) { + if (sortType.equals(SortType.SORT_LATEST)) + return curationRepository.findAllByOrderByCreatedAtDesc(pageable); // 취향 유사도 만들기 전까진 최신순 + } + + // 2) 분류 기준에 맞게 데이터 가져오기 + return switch (sortType) { + case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); // 인기순 + case SORT_LATEST -> curationRepository.findLatestCurations(cursor, pageable); // 최신순 + + // 1. 큐레이션 리스트 뽑기 + // 2. 입력값 : + case SORT_SIMILARITY -> { + List recommended = curationRecommendationService.recommend(readingPreferenceInfo); + List paginated = CurationMatchResultPagination.paginate(recommended, cursor, pageable); + yield paginated.stream().map(CurationMatchResult::getCuration).collect(Collectors.toList()); + } + + }; + } + + public Long calculateNextCursor(List curations, int size, boolean hasNext) { + return hasNext ? curations.get(size).getId() : null; + } +} \ No newline at end of file From 5ba66767a49ccdff00577a5de1d518c46a86c92a Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 00:05:25 +0900 Subject: [PATCH 075/291] =?UTF-8?q?chore:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EB=94=94=EB=B2=A8=EB=A1=AD=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list/CurationListController.java | 53 ++++++++++++++++ .../get/list/CurationListGetRes.java | 2 +- .../service/list/CurationListService.java | 62 +++++++++++++++++++ .../util}/gemini/dto/CurationMatchResult.java | 16 ++++- 4 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/get/list/CurationListGetRes.java (92%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java rename src/main/java/BookPick/mvp/{integration => domain/curation/util}/gemini/dto/CurationMatchResult.java (84%) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java new file mode 100644 index 0000000..bd4f633 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -0,0 +1,53 @@ +// CurationListController.java에 추가 +package BookPick.mvp.domain.curation.controller.list; + +import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; +import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.service.list.CurationListService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; +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 +@RequestMapping("/api/v1/curations") +@RequiredArgsConstructor +public class CurationListController { + + private final CurationListService curationListService; + + + @Operation(summary = "큐레이션 최신순 목록 조회", description = "큐레이션 목록을 페이징 조회", tags = {"Curation"}) + @GetMapping + public ResponseEntity> CurationsGet( + @RequestParam(defaultValue = "latest") String sort, + @RequestParam(required = false) Long cursor, + @RequestParam(defaultValue = "10") int size, + @AuthenticationPrincipal @Valid CustomUserDetails currentUser + ) { + + if(currentUser==null){ + throw new InvalidTokenTypeException(); + } + + // 1. SortType 변환 + SortType sortType = SortType.fromValue(sort); + + // 2. 큐레이션 리스트 반환 + CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); + + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); + } + + +} + + + diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java similarity index 92% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java index e8df3d7..4c197c1 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CurationListGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java @@ -1,5 +1,5 @@ // CurationListGetRes.java -package BookPick.mvp.domain.curation.dto.get.list; +package BookPick.mvp.domain.curation.dto.base.get.list; import BookPick.mvp.domain.curation.enums.SortType; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java new file mode 100644 index 0000000..41a059f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -0,0 +1,62 @@ +package BookPick.mvp.domain.curation.service.list; + +import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; +import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; +import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; +import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; +import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; +import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; +import BookPick.mvp.domain.preference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.preference.repository.ReadingPreferenceRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class CurationListService { + + private final CurationPageHandler pageHandler; + private final ReadingPreferenceRepository readingPreferenceRepository; + private final CurationRecommendationService curationRecommendationService; + + // 1. 큐레이션 리스트 조회 + public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long userId) { + + if (sortType == SortType.SORT_SIMILARITY) { + ReadingPreferenceInfo preferenceInfo = readingPreferenceRepository.findByUserId(userId) + .map(ReadingPreferenceInfo::from) + .orElseThrow(UserReadingPreferenceNotExisted::new); + + List recommended = curationRecommendationService.recommend(preferenceInfo); + List paginated = CurationMatchResultPagination.paginate(recommended, cursor, PageRequest.of(0, size + 1)); + + boolean hasNext = paginated.size() > size; + List contentResults = hasNext ? paginated.subList(0, size) : paginated; + Long nextCursor = hasNext ? paginated.get(size).getCuration().getId() : null; + + List content = contentResults.stream() + .map(CurationContentRes::from) + .collect(Collectors.toList()); + + return CurationListGetRes.from(sortType, content, hasNext, nextCursor); + } + + // 1) 큐레이션 페이징해서 구분 + List curations = pageHandler.getCurationsPage(sortType, cursor, size, null); + CursorPage page = pageHandler.createCursorPage(curations, size); + List content = pageHandler.convertToContentRes(page.getContent()); + + return CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); + } + + // Issue 1) DTO 만들어서 독서취향 정보 레이어간 소통 vs 사용자 독서취향 실시간 수정 반영 고려 + // 1. 사용자는 독서취향을 한번 설정하면 자주 바꾸지 않는다. + // 2. 따라서 DTO 생성 후 넣기로 결정 +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java similarity index 84% rename from src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java rename to src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java index dd7fa02..19dd923 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/dto/CurationMatchResult.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java @@ -1,9 +1,12 @@ -package BookPick.mvp.integration.gemini.dto; +package BookPick.mvp.domain.curation.util.gemini.dto; import BookPick.mvp.domain.curation.model.Curation; import lombok.Builder; import lombok.Getter; +import java.util.ArrayList; +import java.util.List; + @Getter @Builder public class CurationMatchResult { @@ -13,6 +16,7 @@ public class CurationMatchResult { private String matchedKeyword; private String matchedStyle; private int totalMatchCount; + private String matched; public static CurationMatchResult of(Curation curation, String recommendedMood, @@ -25,31 +29,38 @@ public static CurationMatchResult of(Curation curation, String matchedKeyword = null; String matchedStyle = null; int matchCount = 0; + List matchedItems = new ArrayList<>(); // Mood 매칭 if (curation.getMoods() != null && curation.getMoods().contains(recommendedMood)) { matchedMood = recommendedMood; + matchedItems.add(recommendedMood); matchCount++; } // Genre 매칭 if (curation.getGenres() != null && curation.getGenres().contains(recommendedGenre)) { matchedGenre = recommendedGenre; + matchedItems.add(recommendedGenre); matchCount++; } // Keyword 매칭 if (curation.getKeywords() != null && curation.getKeywords().contains(recommendedKeyword)) { matchedKeyword = recommendedKeyword; + matchedItems.add(recommendedKeyword); matchCount++; } // Style 매칭 if (curation.getStyles() != null && curation.getStyles().contains(recommendedStyle)) { matchedStyle = recommendedStyle; + matchedItems.add(recommendedStyle); matchCount++; } + String matchedString = String.join(", ", matchedItems); + return CurationMatchResult.builder() .curation(curation) .matchedMood(matchedMood) @@ -57,6 +68,7 @@ public static CurationMatchResult of(Curation curation, .matchedKeyword(matchedKeyword) .matchedStyle(matchedStyle) .totalMatchCount(matchCount) + .matched(matchedString) .build(); } @@ -79,4 +91,4 @@ public String getMatchedString() { return sb.toString(); } -} \ No newline at end of file +} From c80674428a8a013c519ec2fdaa88db37aed1a051 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 00:05:53 +0900 Subject: [PATCH 076/291] =?UTF-8?q?chore:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A7=A4=EC=B9=AD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/DuplicateEmailException.java | 2 +- .../auth/exception/InvalidLoginException.java | 2 +- .../exception/InvalidTokenTypeException.java | 2 +- .../exception/JwtTokenExpiredException.java | 2 +- .../auth/service/CustomUserDetails.java | 40 ++++++++++++++++++ .../auth/service/MyUserDetailsService.java | 40 ------------------ .../{ => base}/create/Req/RecommendDto.java | 2 +- .../{ => base}/create/Req/ThumbnailDto.java | 2 +- .../dto/{ => base}/get/list/CursorPage.java | 2 +- .../dto/{ => base}/get/list/ThumbnailRes.java | 2 +- .../dto/base/update/CurationUpdateReq.java | 13 ++++++ .../{ => base}/update/CurationUpdateRes.java | 2 +- .../dto/prefer/ReadingPreferenceInfo.java | 31 ++++++++++++++ .../dto/update/CurationUpdateReq.java | 13 ------ .../exception/CurationNotFoundException.java | 2 +- .../repository/CurationRepository.java | 2 +- .../service/{ => base}/CurationService.java | 36 +++++----------- .../fetcher/CurationSimilarityFetcher.java | 4 -- .../list/CurationRecommendationService.java | 35 ++++++++++++++++ .../provider/CurationPopularProvider.java | 22 ---------- .../provider/CurationSimilarityProvider.java | 22 ---------- .../similarity/SimilarityCalculator.java | 4 -- .../service/similarity/SimilarityMatcher.java | 4 -- .../util/gemini/GeminiErrorException.java | 12 ++++++ .../util}/gemini/client/GeminiClient.java | 5 ++- .../util}/gemini/client/GeminiConfig.java | 2 +- .../util/gemini/enums/GeminiErrorCode.java | 18 ++++++++ .../SystemInstructionPromptTemplate.java | 2 +- .../util}/gemini/service/GeminiService.java | 12 +++--- .../list}/Handler/CurationPageHandler.java | 24 +++++++---- .../fetcher/CurationSimilarityFetcher.java | 4 ++ .../CurationMatchResultPagination.java | 28 +++++++++++++ .../list/similarity/SimilarityCalculator.java | 4 ++ .../list/similarity/SimilarityMatcher.java | 4 ++ .../UserReadingPreferenceNotExisted.java | 2 +- .../domain/preference/PrferenceErrorCode.java | 18 ++++++++ .../ReadingPreferenceController.java | 4 +- .../preference/entity/ReadingPreference.java | 2 +- .../user/exception/UserNotFoundException.java | 2 +- .../api/DuplicateResourceException.java | 2 +- .../global/api/{ => ErrorCode}/ErrorCode.java | 4 +- .../api/ErrorCode/ErrorCodeInterface.java | 8 ++++ .../api/{ => SuccessCode}/SuccessCode.java | 2 +- .../BookPick/mvp/global/config/JwtFilter.java | 4 +- .../exception/DuplicateResourceException.java | 2 +- .../exception/GlobalExceptionHandler.java | 6 +-- .../custom/DuplicateResourceException.java | 2 +- .../BookPick/mvp/global/util/JwtUtil.java | 2 +- .../java/BookPick/mvp/GeminiClientMain.java | 41 +++++++++++++------ 49 files changed, 310 insertions(+), 192 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/create/Req/RecommendDto.java (72%) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/create/Req/ThumbnailDto.java (62%) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/get/list/CursorPage.java (81%) rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/get/list/ThumbnailRes.java (57%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java rename src/main/java/BookPick/mvp/domain/curation/dto/{ => base}/update/CurationUpdateRes.java (82%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java rename src/main/java/BookPick/mvp/domain/curation/service/{ => base}/CurationService.java (70%) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/gemini/GeminiErrorException.java rename src/main/java/BookPick/mvp/{integration => domain/curation/util}/gemini/client/GeminiClient.java (93%) rename src/main/java/BookPick/mvp/{integration => domain/curation/util}/gemini/client/GeminiConfig.java (85%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java rename src/main/java/BookPick/mvp/{integration => domain/curation/util}/gemini/prompt/SystemInstructionPromptTemplate.java (96%) rename src/main/java/BookPick/mvp/{integration => domain/curation/util}/gemini/service/GeminiService.java (86%) rename src/main/java/BookPick/mvp/domain/curation/{service => util/list}/Handler/CurationPageHandler.java (52%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationSimilarityFetcher.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityCalculator.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityMatcher.java create mode 100644 src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java rename src/main/java/BookPick/mvp/global/api/{ => ErrorCode}/ErrorCode.java (94%) create mode 100644 src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java rename src/main/java/BookPick/mvp/global/api/{ => SuccessCode}/SuccessCode.java (98%) diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java b/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java index 70ec89c..9e16355 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/DuplicateEmailException.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.auth.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class DuplicateEmailException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java index 8ab2eed..c5fa443 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidLoginException.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.auth.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java index 1162378..6cb5f78 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/InvalidTokenTypeException.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.auth.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class InvalidTokenTypeException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java b/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java index 5b3b02d..77e1084 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/JwtTokenExpiredException.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.auth.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class JwtTokenExpiredException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java b/src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java new file mode 100644 index 0000000..2618024 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/CustomUserDetails.java @@ -0,0 +1,40 @@ +package BookPick.mvp.domain.auth.service; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.util.Collection; + +@Getter +@Setter +public class CustomUserDetails extends User { + private Long id; + private String nickname; + private String bio; + private String profileImageUrl; + private boolean isFirstLogin; + + public CustomUserDetails( + BookPick.mvp.domain.user.entity.User user, + Collection authorities + ) { + super(user.getEmail(), user.getPassword(), authorities); + this.bio = user.getBio(); + this.profileImageUrl = user.getProfileImageUrl(); + } + + public CustomUserDetails( + Long userId, + String email, + Collection authorities + ) { + super(email, "", authorities); + this.id = userId; + } + + static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities) { + return new CustomUserDetails(userId, email, authorities); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index ea338b2..2e238ee 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -4,26 +4,17 @@ import BookPick.mvp.domain.auth.Roles; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; -import lombok.Builder; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import javax.management.relation.Role; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import static org.apache.coyote.http11.Constants.a; - // 스프링 시큐리티에서 자동으로 호출 @@ -59,36 +50,5 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep } - @Getter - @Setter - public static class CustomUserDetails extends User { - private Long id; - private String nickname; - private String bio; - private String profileImageUrl; - private boolean isFirstLogin; - - public CustomUserDetails( - BookPick.mvp.domain.user.entity.User user, - Collection authorities - ) { - super(user.getEmail(), user.getPassword(), authorities); - this.bio = user.getBio(); - this.profileImageUrl = user.getProfileImageUrl(); - } - - public CustomUserDetails( - Long userId, - String email, - Collection authorities - ){ - super(email, "", authorities); - this.id=userId; - } - - static public CustomUserDetails fromJwt(Long userId, String email, Collection authorities){ - return new CustomUserDetails(userId, email, authorities); - } - } } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/RecommendDto.java similarity index 72% rename from src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/RecommendDto.java index 8bdbf38..f7b04c9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/RecommendDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/RecommendDto.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.create.Req; +package BookPick.mvp.domain.curation.dto.base.create.Req; import java.util.List; public record RecommendDto( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/ThumbnailDto.java similarity index 62% rename from src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/ThumbnailDto.java index 9303b80..3ab3635 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/create/Req/ThumbnailDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/ThumbnailDto.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.create.Req; +package BookPick.mvp.domain.curation.dto.base.create.Req; // 썸네일 정보 public record ThumbnailDto( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CursorPage.java similarity index 81% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CursorPage.java index 980f407..803d41f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/CursorPage.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CursorPage.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.get.list; +package BookPick.mvp.domain.curation.dto.base.get.list; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/ThumbnailRes.java similarity index 57% rename from src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/ThumbnailRes.java index ca24434..f644679 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/get/list/ThumbnailRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/ThumbnailRes.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.get.list; +package BookPick.mvp.domain.curation.dto.base.get.list; public record ThumbnailRes( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java new file mode 100644 index 0000000..557e5fb --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java @@ -0,0 +1,13 @@ +// CurationUpdateReq.java +package BookPick.mvp.domain.curation.dto.base.update; + +import BookPick.mvp.domain.curation.dto.base.create.Req.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.Req.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.Req.ThumbnailDto; + +public record CurationUpdateReq( + ThumbnailDto thumbnail, + BookDto book, + String review, + RecommendDto recommend +) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java similarity index 82% rename from src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java index f443909..a5ffb5b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java @@ -1,5 +1,5 @@ // CurationUpdateRes.java -package BookPick.mvp.domain.curation.dto.update; +package BookPick.mvp.domain.curation.dto.base.update; import BookPick.mvp.domain.curation.model.Curation; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java new file mode 100644 index 0000000..fcba621 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.curation.dto.prefer; + +import BookPick.mvp.domain.preference.entity.ReadingPreference; + +import java.util.List; + +public record ReadingPreferenceInfo( + Long userId, + String mbti, + List favoriteBooks, + List moods, + List readingHabits, + List genres, + List keywords, + List trends +) { + + // 엔티티 → DTO 변환 + public static ReadingPreferenceInfo from(ReadingPreference preference) { + return new ReadingPreferenceInfo( + preference.getUser().getId(), + preference.getMbti(), + preference.getFavoriteBooks(), + preference.getMoods(), + preference.getReadingHabits(), + preference.getGenres(), + preference.getKeywords(), + preference.getTrends() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java deleted file mode 100644 index 4d68e18..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/update/CurationUpdateReq.java +++ /dev/null @@ -1,13 +0,0 @@ -// CurationUpdateReq.java -package BookPick.mvp.domain.curation.dto.update; - -import BookPick.mvp.domain.curation.dto.create.Req.BookDto; -import BookPick.mvp.domain.curation.dto.create.Req.RecommendDto; -import BookPick.mvp.domain.curation.dto.create.Req.ThumbnailDto; - -public record CurationUpdateReq( - ThumbnailDto thumbnail, - BookDto book, - String review, - RecommendDto recommend -) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java b/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java index ed95673..ead82ed 100644 --- a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java @@ -1,7 +1,7 @@ // CurationNotFoundException.java package BookPick.mvp.domain.curation.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class CurationNotFoundException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 953de54..d786dbe 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -18,7 +18,7 @@ public interface CurationRepository extends JpaRepository { @Query("SELECT c FROM Curation c WHERE c.id <= :cursor ORDER BY c.createdAt DESC, c.id DESC") - List findCurations(@Param("cursor") Long cursor, Pageable pageable); + List findLatestCurations(@Param("cursor") Long cursor, Pageable pageable); @Query("SELECT c FROM Curation c " + "WHERE (:cursor IS NULL) " + diff --git a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java similarity index 70% rename from src/main/java/BookPick/mvp/domain/curation/service/CurationService.java rename to src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 7152ec7..cfa884d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -1,22 +1,18 @@ -// CurationService.java -package BookPick.mvp.domain.curation.service; - -import BookPick.mvp.domain.curation.enums.SortType; -import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; -import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; -import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; -import BookPick.mvp.domain.curation.dto.get.list.CursorPage; -import BookPick.mvp.domain.curation.dto.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; +// CurationListService.java +package BookPick.mvp.domain.curation.service.base; + +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.domain.curation.service.Handler.CurationPageHandler; -import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; +import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; +import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; @@ -26,7 +22,6 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.List; @Service @RequiredArgsConstructor @@ -79,15 +74,6 @@ public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { } - // -- 큐레이션 목록 조회 -- - public CurationListGetRes getCurationList(SortType sortType, Long cursor, int size) { - List curations = pageHandler.fetchCurationsWithExtra(sortType, cursor, size); - CursorPage page = pageHandler.createCursorPage(curations, size); - List content = pageHandler.convertToContentRes(page.getContent()); - - return CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); - } - // -- 큐레이션 수정 -- @Transactional diff --git a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java b/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java deleted file mode 100644 index 62ca451..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/fetcher/CurationSimilarityFetcher.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.service.fetcher; - -public class CurationSimilarityFetcher { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java new file mode 100644 index 0000000..1153a39 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java @@ -0,0 +1,35 @@ +package BookPick.mvp.domain.curation.service.list; + +import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.curation.util.gemini.prompt.ContentPromptTemplate; +import BookPick.mvp.domain.curation.util.gemini.service.GeminiService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CurationRecommendationService { + + private final GeminiService geminiService; + + public List recommend(ReadingPreferenceInfo preferenceInfo) { + + return geminiService.recommendCurationsWithMatch( + + + // 1. 제미나이 프롬프트 생성 + ContentPromptTemplate.builder() + .mbti(preferenceInfo.mbti()) + .mood(String.join(", ", preferenceInfo.moods())) //, 으로 하나의 문자열로 변경 -> 제미나이 프롬프트에 넣기 위해서 + .readingMethod(String.join(", ", preferenceInfo.readingHabits())) + .genre(String.join(", ", preferenceInfo.genres())) + .keyword(String.join(", ", preferenceInfo.keywords())) + .readingStyle(String.join(", ", preferenceInfo.trends())) + .build() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java deleted file mode 100644 index 7d5818c..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationPopularProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -package BookPick.mvp.domain.curation.service.provider; - - -import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -@RequiredArgsConstructor -public class CurationPopularProvider { - - private final CurationRepository curationRepository; - - public List fetch(Long cursor, Pageable pageable) { - return curationRepository.findCurationsByPopularity(cursor, pageable); - } -} - diff --git a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java b/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java deleted file mode 100644 index 2899259..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/provider/CurationSimilarityProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -package BookPick.mvp.domain.curation.service.provider; - - -import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Component -@RequiredArgsConstructor -public class CurationSimilarityProvider { - - private final CurationRepository curationRepository; - - public List fetch(Long cursor, Pageable pageable) { - // TODO: 유사도 알고리즘 구현 후 변경 예정 - return curationRepository.findCurations(cursor, pageable); - } -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java b/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java deleted file mode 100644 index f96ec9a..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityCalculator.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.service.similarity; - -public class SimilarityCalculator { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java b/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java deleted file mode 100644 index 66c359d..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/similarity/SimilarityMatcher.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.service.similarity; - -public class SimilarityMatcher { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/GeminiErrorException.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/GeminiErrorException.java new file mode 100644 index 0000000..fc85bc7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/GeminiErrorException.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.curation.util.gemini; + +import BookPick.mvp.domain.curation.util.gemini.enums.GeminiErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class GeminiErrorException extends BusinessException { + + public GeminiErrorException(){ + super(GeminiErrorCode.GEMINI_API_CALL_FAILED); + + } +} diff --git a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/client/GeminiClient.java similarity index 93% rename from src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java rename to src/main/java/BookPick/mvp/domain/curation/util/gemini/client/GeminiClient.java index c14f5a1..828c3f9 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiClient.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/client/GeminiClient.java @@ -1,7 +1,8 @@ -package BookPick.mvp.integration.gemini.client; +package BookPick.mvp.domain.curation.util.gemini.client; +import BookPick.mvp.domain.curation.util.gemini.GeminiErrorException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -61,7 +62,7 @@ public String callGemini(String systemInstruction, String userContent) { .path("text").asText(); } catch (Exception e) { - throw new RuntimeException("Gemini API 호출 실패", e); + throw new GeminiErrorException(); } } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/client/GeminiConfig.java similarity index 85% rename from src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java rename to src/main/java/BookPick/mvp/domain/curation/util/gemini/client/GeminiConfig.java index 207fe2a..d9f3922 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/client/GeminiConfig.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/client/GeminiConfig.java @@ -1,4 +1,4 @@ -package BookPick.mvp.integration.gemini.client; +package BookPick.mvp.domain.curation.util.gemini.client; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java new file mode 100644 index 0000000..ede3a04 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.curation.util.gemini.enums; + +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum GeminiErrorCode implements ErrorCodeInterface { + + GEMINI_API_CALL_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "Gemini API 호출에 실패했습니다."); + + + + private final HttpStatus status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java similarity index 96% rename from src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java rename to src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java index 1f8bf5f..e35d7c7 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/prompt/SystemInstructionPromptTemplate.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java @@ -1,4 +1,4 @@ -package BookPick.mvp.integration.gemini.prompt; +package BookPick.mvp.domain.curation.util.gemini.prompt; import org.springframework.stereotype.Component; diff --git a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java similarity index 86% rename from src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java rename to src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index 0daee9a..1b4d3b3 100644 --- a/src/main/java/BookPick/mvp/integration/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -1,12 +1,12 @@ -package BookPick.mvp.integration.gemini.service; +package BookPick.mvp.domain.curation.util.gemini.service; import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.integration.gemini.client.GeminiClient; -import BookPick.mvp.integration.gemini.dto.CurationMatchResult; -import BookPick.mvp.integration.gemini.prompt.SystemInstructionPromptTemplate; -import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; +import BookPick.mvp.domain.curation.util.gemini.client.GeminiClient; +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.curation.util.gemini.prompt.SystemInstructionPromptTemplate; +import BookPick.mvp.domain.curation.util.gemini.prompt.ContentPromptTemplate; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,6 +35,8 @@ public String[] parseResult(String result) { @Transactional(readOnly = true) public List recommendCurationsWithMatch(ContentPromptTemplate contentTemplate) { + + // 1. Gemini에게 추천 받기 String result = generateRecommendation(contentTemplate); String[] parsed = parseResult(result); diff --git a/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java similarity index 52% rename from src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java rename to src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index 99f4f60..83fb4d0 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -1,11 +1,14 @@ -package BookPick.mvp.domain.curation.service.Handler; +package BookPick.mvp.domain.curation.util.list.Handler; +import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.enums.SortType; -import BookPick.mvp.domain.curation.dto.get.list.CurationContentRes; -import BookPick.mvp.domain.curation.dto.get.list.CursorPage; +import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; +import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.domain.curation.service.fetcher.CurationFetcher; +import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; +import BookPick.mvp.domain.preference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.preference.entity.ReadingPreference; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -19,10 +22,17 @@ public class CurationPageHandler { private final CurationFetcher curationFetcher; - // 1. 데이터 조회 (size+1개) - public List fetchCurationsWithExtra(SortType sortType, Long cursor, int size) { + // 1. 데이터 조회 (size+1개 : +1을 하는 이유는 다음 것을 항상 확인하기 위해서 + public List getCurationsPage(SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { Pageable pageable = PageRequest.of(0, size + 1); - return curationFetcher.fetchCurations(sortType, cursor, pageable); + // SORT_SIMILARITY일 경우 preferenceInfo를 사용, 나머지는 user 관련 정보 필요 없음 + if (sortType == SortType.SORT_SIMILARITY && readingPreferenceInfo == null) { + throw new UserReadingPreferenceNotExisted(); + } + + + // 1) DB에서 실제로 가져오는 로직 (fetch : DB에서 가져오는 행위) + return curationFetcher.fetchCurations(sortType, cursor, pageable, readingPreferenceInfo); } // 2. 커서 페이징 처리 diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationSimilarityFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationSimilarityFetcher.java new file mode 100644 index 0000000..458f74c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationSimilarityFetcher.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.util.list.fetcher; + +public class CurationSimilarityFetcher { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java new file mode 100644 index 0000000..058d2bd --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java @@ -0,0 +1,28 @@ +package BookPick.mvp.domain.curation.util.list.similarity; + +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public class CurationMatchResultPagination { + + /** + * cursor 기준으로 큐레이션 리스트 페이징 처리 + */ + public static List paginate(List curationMatchResults, Long cursor, Pageable pageable) { + int start = 0; + + if (cursor != null) { + for (int i = 0; i < curationMatchResults.size(); i++) { + if (curationMatchResults.get(i).getCuration().getId().equals(cursor)) { + start = i + 1; + break; + } + } + } + + int end = Math.min(start + pageable.getPageSize(), curationMatchResults.size()); + return curationMatchResults.subList(start, end); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityCalculator.java b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityCalculator.java new file mode 100644 index 0000000..6c3a436 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityCalculator.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.util.list.similarity; + +public class SimilarityCalculator { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityMatcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityMatcher.java new file mode 100644 index 0000000..eb8ed60 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/SimilarityMatcher.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.util.list.similarity; + +public class SimilarityMatcher { +} diff --git a/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java b/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java index 0dc7017..5a7e588 100644 --- a/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java +++ b/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.preference.Exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class UserReadingPreferenceNotExisted extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java new file mode 100644 index 0000000..75d038a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.preference; + +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum PrferenceErrorCode implements ErrorCodeInterface { + + PREFERENCE_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 독서취향이 설정되지 않았습니다."); + + + + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java index e84c4c4..3952361 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.preference.controller; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateReq; import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateRes; import BookPick.mvp.domain.preference.dto.Delete.ReadingPreferenceDeleteRes; @@ -7,9 +8,8 @@ import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.preference.service.ReadingPreferenceService; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java index 88c757e..349eedd 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java @@ -32,7 +32,7 @@ public class ReadingPreference { @ElementCollection @CollectionTable(name = "preference_moods", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "moods") + @Column(name = "mood") private List moods; @ElementCollection diff --git a/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java b/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java index 3f0d2c4..a9b6469 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.user.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class UserNotFoundException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java index 69a6e78..9fcfe65 100644 --- a/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java +++ b/src/main/java/BookPick/mvp/global/api/DuplicateResourceException.java @@ -1,10 +1,10 @@ package BookPick.mvp.global.api; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; import lombok.Getter; import lombok.Setter; -import BookPick.mvp.global.api.ErrorCode.*; @Getter diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java similarity index 94% rename from src/main/java/BookPick/mvp/global/api/ErrorCode.java rename to src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 442d203..beac9ea 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.global.api; +package BookPick.mvp.global.api.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; @@ -6,7 +6,7 @@ @AllArgsConstructor @Getter -public enum ErrorCode { +public enum ErrorCode implements ErrorCodeInterface { // -- Auth INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), // 400 회원 가입 diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java new file mode 100644 index 0000000..ce07e83 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java @@ -0,0 +1,8 @@ +package BookPick.mvp.global.api.ErrorCode; + +import org.springframework.http.HttpStatus; + +public interface ErrorCodeInterface { + HttpStatus getStatus(); + String getMessage(); +} diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java similarity index 98% rename from src/main/java/BookPick/mvp/global/api/SuccessCode.java rename to src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java index 9fcb7e2..2520219 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.global.api; +package BookPick.mvp.global.api.SuccessCode; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 88e2663..82a8ba1 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,10 +1,8 @@ package BookPick.mvp.global.config; -import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.global.util.JwtUtil; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java index 6a1cb57..05b5a02 100644 --- a/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java +++ b/src/main/java/BookPick/mvp/global/exception/DuplicateResourceException.java @@ -1,7 +1,7 @@ package BookPick.mvp.global.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index 34546b4..b70caf1 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -2,8 +2,8 @@ import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.ErrorCode; -import org.springframework.http.HttpStatus; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -14,7 +14,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity> handleBusinessException(BusinessException e) { - ErrorCode errorCode = e.getErrorCode(); + ErrorCodeInterface errorCode = e.getErrorCode(); return ResponseEntity .status(errorCode.getStatus()) diff --git a/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java b/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java index 1210dae..5d3901a 100644 --- a/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java +++ b/src/main/java/BookPick/mvp/global/exception/custom/DuplicateResourceException.java @@ -1,7 +1,7 @@ package BookPick.mvp.global.exception.custom; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 7dcc9cf..aa11449 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; -import BookPick.mvp.domain.auth.service.MyUserDetailsService.*; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.io.Decoders; diff --git a/src/test/java/BookPick/mvp/GeminiClientMain.java b/src/test/java/BookPick/mvp/GeminiClientMain.java index f9335f4..900a07c 100644 --- a/src/test/java/BookPick/mvp/GeminiClientMain.java +++ b/src/test/java/BookPick/mvp/GeminiClientMain.java @@ -1,14 +1,16 @@ package BookPick.mvp; - import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.integration.gemini.dto.CurationMatchResult; -import BookPick.mvp.integration.gemini.prompt.ContentPromptTemplate; -import BookPick.mvp.integration.gemini.service.GeminiService; +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.curation.util.gemini.prompt.ContentPromptTemplate; +import BookPick.mvp.domain.curation.util.gemini.service.GeminiService; +import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.user.entity.User; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.beans.factory.annotation.Autowired; import java.util.List; @@ -19,17 +21,30 @@ public static void main(String[] args) { SpringApplication.run(GeminiClientMain.class, args); } + @Autowired + private GeminiService geminiService; + @Bean - CommandLineRunner test(GeminiService geminiService) { + CommandLineRunner runTest() { return args -> { + User user = new User(); // 테스트용 유저 생성 + ReadingPreference preference = ReadingPreference.builder() + .user(user) + .mbti("INFP") + .moods(List.of("새벽 시간, 카페, 혼자만의 시간")) + .readingHabits(List.of("한 번에 완독하는 편, 조용한 곳에서만 읽는 편")) + .genres(List.of("에세이, 철학, 소설")) + .keywords(List.of("성장, 공감, 현실")) + .trends(List.of("몰입형, 감성적, 깊이 있는 사색")) + .build(); ContentPromptTemplate template = ContentPromptTemplate.builder() - .mbti("INFP") - .mood("새벽 시간, 카페, 혼자만의 시간") - .readingMethod("한 번에 완독하는 편, 조용한 곳에서만 읽는 편") - .genre("에세이, 철학, 소설") - .keyword("성장, 공감, 현실") - .readingStyle("몰입형, 감성적, 깊이 있는 사색") + .mbti(preference.getMbti()) + .mood(String.join(", ", preference.getMoods())) + .readingMethod(String.join(", ", preference.getReadingHabits())) + .genre(String.join(", ", preference.getGenres())) + .keyword(String.join(", ", preference.getKeywords())) + .readingStyle(String.join(", ", preference.getTrends())) .build(); System.out.println("=== 추천된 큐레이션 ==="); @@ -43,10 +58,10 @@ CommandLineRunner test(GeminiService geminiService) { System.out.println(" 저자: " + c.getBookAuthor()); System.out.println(" 총 일치: " + result.getTotalMatchCount() + "개"); System.out.println("\n=== 일치한 태그 ==="); - System.out.print(result.getMatchedString()); // ← 일치하는 것만 출력! + System.out.print(result.getMatchedString()); System.out.println(" 인기도: " + c.getPopularityScore()); System.out.println("-----------------------------------"); }); }; } -} \ No newline at end of file +} From 1738e7c3fc524ad4cc9a418f6c7c7c1b56e6b6a4 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 01:05:24 +0900 Subject: [PATCH 077/291] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=EB=8F=85?= =?UTF-8?q?=EC=84=9C=20=EC=B7=A8=ED=96=A5=EA=B3=BC=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=EB=90=9C=20=ED=82=A4=EC=9B=8C=EB=93=9C=EB=93=A4=20=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=98=EB=90=9C=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=8B=B4=EB=8A=94=20DTO=20matched=20?= =?UTF-8?q?=ED=82=A4=EC=9B=8C=EB=93=9C=EC=97=90=20=EB=84=A3=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list/CurationListController.java | 2 +- .../dto/base/get/list/CurationContentRes.java | 39 +++++++++++++------ .../service/list/CurationListService.java | 3 +- .../util/gemini/dto/CurationMatchResult.java | 4 ++ .../util/gemini/service/GeminiService.java | 1 + 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index bd4f633..6b966e0 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -23,7 +23,7 @@ public class CurationListController { private final CurationListService curationListService; - @Operation(summary = "큐레이션 최신순 목록 조회", description = "큐레이션 목록을 페이징 조회", tags = {"Curation"}) + @Operation(summary = "큐레이션 목록 조회", description = "최신순 / 인기순 / 사용자 취향 유사도 순", tags = {"Curation"}) @GetMapping public ResponseEntity> CurationsGet( @RequestParam(defaultValue = "latest") String sort, diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 199535a..6ea90b2 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -1,7 +1,9 @@ package BookPick.mvp.domain.curation.dto.base.get.list; +import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.user.entity.User; public record CurationContentRes( Long curationId, @@ -14,7 +16,7 @@ public record CurationContentRes( int likeCount, int commentCount, int viewCount, - Double similarity, + Integer similarity, String matched, Integer popularityScore, String createdAt @@ -24,35 +26,48 @@ public static CurationContentRes from(Curation curation) { curation.getId(), curation.getBookTitle(), curation.getUser().getId(), - "닉네임", // TODO: User 조인 필요 + curation.getUser().getNickname(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), - 0, // TODO: 좋아요 수 - 0, // TODO: 댓글 수 - 0, // TODO: 조회수 + curation.getLikeCount(), + curation.getCommentCount(), + curation.getViewCount(), null, null, - curation.getPopularityScore(), // TODO: popularityScore 계산 + curation.getPopularityScore(), curation.getCreatedAt().toString() ); } - public static CurationContentRes from(CurationMatchResult matchResult) { + public static CurationContentRes from(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo) { Curation curation = matchResult.getCuration(); return new CurationContentRes( curation.getId(), curation.getBookTitle(), curation.getUser().getId(), - "닉네임", // TODO: User 조인 필요 + matchResult.getUser().getNickname(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), - 0, // TODO: 좋아요 수 - 0, // TODO: 댓글 수 - 0, // TODO: 조회수 - null, // TODO: similarity + curation.getLikeCount(), + curation.getCommentCount(), + curation.getViewCount(), + getSimilarity(matchResult, preferenceInfo), matchResult.getMatched(), curation.getPopularityScore(), curation.getCreatedAt().toString() ); } + + // 1. 유사도 계산법 100% + // 1) 작가 19% + // 2) Matched -> 이거 , 로 분리해서 개수 Count 하나당 20% + + static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo){ + Integer similarity = 0; + User user = matchResult.getUser(); + String userPrferAuthor = preferenceInfo. + String userPrferAuthor = matchResult.getCuration().getBookAuthor(); + if(matchResult.getCuration().getBookAuthor().equals(user) + return null; + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 41a059f..b12c381 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -42,9 +42,10 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long nextCursor = hasNext ? paginated.get(size).getCuration().getId() : null; List content = contentResults.stream() - .map(CurationContentRes::from) + .map(result -> CurationContentRes.from(result, preferenceInfo)) .collect(Collectors.toList()); + return CurationListGetRes.from(sortType, content, hasNext, nextCursor); } diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java index 19dd923..35d5c5e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.curation.util.gemini.dto; import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.user.entity.User; import lombok.Builder; import lombok.Getter; @@ -11,6 +12,7 @@ @Builder public class CurationMatchResult { private Curation curation; + private User user; private String matchedMood; private String matchedGenre; private String matchedKeyword; @@ -19,6 +21,7 @@ public class CurationMatchResult { private String matched; public static CurationMatchResult of(Curation curation, + User user, String recommendedMood, String recommendedGenre, String recommendedKeyword, @@ -63,6 +66,7 @@ public static CurationMatchResult of(Curation curation, return CurationMatchResult.builder() .curation(curation) + .user(user) .matchedMood(matchedMood) .matchedGenre(matchedGenre) .matchedKeyword(matchedKeyword) diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index 1b4d3b3..3f621f8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -59,6 +59,7 @@ public List recommendCurationsWithMatch(ContentPromptTempla return curations.stream() .map(curation -> CurationMatchResult.of( curation, + curation.getUser(), recommendedMood, recommendedGenre, recommendedKeyword, From 2f0c41486d70729c4c6129eef6b67b6fb804a091 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 20:05:03 +0900 Subject: [PATCH 078/291] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=8F=85=EC=84=9C=20=EC=B7=A8=ED=96=A5=20=EC=9C=A0=EC=82=AC?= =?UTF-8?q?=EB=8F=84=20=EA=B3=84=EC=82=B0=20=EB=B0=8F=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=ED=8F=AC=EB=A9=A7=20=EC=95=88,=20similarity=EC=97=90=20?= =?UTF-8?q?=EC=82=BD=EC=9E=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/base/get/list/CurationContentRes.java | 31 +++++++++++++++---- .../dto/prefer/ReadingPreferenceInfo.java | 7 +++-- .../service/list/CurationListService.java | 15 ++++++++- .../SystemInstructionPromptTemplate.java | 4 +++ .../Update/ReadingPreferenceUpdateReq.java | 1 + .../preference/entity/ReadingPreference.java | 6 ++++ 6 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 6ea90b2..70efb74 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -5,6 +5,8 @@ import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.user.entity.User; +import java.util.List; + public record CurationContentRes( Long curationId, String title, @@ -62,12 +64,29 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr // 1) 작가 19% // 2) Matched -> 이거 , 로 분리해서 개수 Count 하나당 20% - static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo){ - Integer similarity = 0; + // 1. 유저의 독서취향을 가지고 + // 2. + + static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo) { + Integer similarity = 50; User user = matchResult.getUser(); - String userPrferAuthor = preferenceInfo. - String userPrferAuthor = matchResult.getCuration().getBookAuthor(); - if(matchResult.getCuration().getBookAuthor().equals(user) - return null; + + + + // 1. 매칭된 큐레이션의 작가중 + String author = matchResult.getCuration().getBookAuthor(); + + + //2. 유저 독서취향의 작가들 안에 존재하면 +20 + List favoriteAuthors = preferenceInfo.favoriteAuthors(); + + if(favoriteAuthors.contains(author)){ + similarity+=10; + } + + // 3. 각 키워드들마다 매칭되는거 있으면 +10 + similarity += matchResult.getTotalMatchCount()*10; + + return similarity; } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java index fcba621..66788da 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -7,19 +7,21 @@ public record ReadingPreferenceInfo( Long userId, String mbti, + List favoriteAuthors, List favoriteBooks, - List moods, List readingHabits, + List moods, List genres, List keywords, List trends -) { + ) { // 엔티티 → DTO 변환 public static ReadingPreferenceInfo from(ReadingPreference preference) { return new ReadingPreferenceInfo( preference.getUser().getId(), preference.getMbti(), + preference.getFavoriteAuthors(), preference.getFavoriteBooks(), preference.getMoods(), preference.getReadingHabits(), @@ -28,4 +30,5 @@ public static ReadingPreferenceInfo from(ReadingPreference preference) { preference.getTrends() ); } + } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index b12c381..bd5290c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -30,22 +30,35 @@ public class CurationListService { public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long userId) { if (sortType == SortType.SORT_SIMILARITY) { + + // 1. 유저 독서 취향 반환 ReadingPreferenceInfo preferenceInfo = readingPreferenceRepository.findByUserId(userId) .map(ReadingPreferenceInfo::from) .orElseThrow(UserReadingPreferenceNotExisted::new); + // 2. 매칭된 큐레이션 리스트트 조회 List recommended = curationRecommendationService.recommend(preferenceInfo); + + //3. 매칭된 큐레이션 페이지네이션 List paginated = CurationMatchResultPagination.paginate(recommended, cursor, PageRequest.of(0, size + 1)); + //4. 유저가 스크롤시, 다음 조회할 값있는지 boolean hasNext = paginated.size() > size; + + // 5. 콘텐츠가 더있으면 자르고 다음페이지에서 보여줌 + // 더 없으면 그냥 보여줌 List contentResults = hasNext ? paginated.subList(0, size) : paginated; + + //6. 다음 커서 반환 Long nextCursor = hasNext ? paginated.get(size).getCuration().getId() : null; + + //7. 큐레이션 단건 응답 포멧 반환 List content = contentResults.stream() .map(result -> CurationContentRes.from(result, preferenceInfo)) .collect(Collectors.toList()); - + //8. 큐레이션 리스트로 감싸기 return CurationListGetRes.from(sortType, content, hasNext, nextCursor); } diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java index e35d7c7..6becb06 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/SystemInstructionPromptTemplate.java @@ -5,6 +5,10 @@ @Component public class SystemInstructionPromptTemplate { + + // 1. 유저의 독서 취향을 가지고 + // 2. 하나씩 뽑아라 아래 항목들중에서 + // 3. 그리고 해당 키워드들을 가지고 큐레이션을 찾아서 유저한테 소개할 것이다. private static final String SYSTEM_PROMPT = """ Based on the user's reading preferences, select exactly one item from each of Mood, Genre, Keyword, and ReadingStyle. You must only choose from the provided lists. diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java index 004e25c..f68d47c 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java +++ b/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java @@ -5,6 +5,7 @@ public record ReadingPreferenceUpdateReq( Long preferenceId, String mbti, + List favoriteAuthors, // 좋아하는 책 List favoriteBooks, // 좋아하는 책 List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java index 349eedd..939294a 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java @@ -25,6 +25,11 @@ public class ReadingPreference { private String mbti; + @ElementCollection + @CollectionTable(name = "preference_authors", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "authors") + private List favoriteAuthors; + @ElementCollection @CollectionTable(name = "preference_favorite_books", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "book") @@ -57,6 +62,7 @@ public class ReadingPreference { public void update(ReadingPreferenceUpdateReq req) { if (req.mbti() != null) this.mbti = req.mbti(); + if (req.favoriteAuthors() != null) this.favoriteAuthors = req.favoriteAuthors(); if (req.favoriteBooks() != null) this.favoriteBooks = req.favoriteBooks(); if (req.moods() != null) this.moods = req.moods(); if (req.readingHabits() != null) this.readingHabits = req.readingHabits(); From ab51f16c104efeaab174ec738b64c6290266f280 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 20:10:44 +0900 Subject: [PATCH 079/291] =?UTF-8?q?chore=20:=20MVP=20=EC=9D=B4=ED=9B=84=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=ED=95=A0=20Todo=201=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/dto/base/get/list/CurationContentRes.java | 6 ++++++ .../domain/curation/util/gemini/service/GeminiService.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 70efb74..0b8563f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -6,6 +6,7 @@ import BookPick.mvp.domain.user.entity.User; import java.util.List; +import java.util.Random; public record CurationContentRes( Long curationId, @@ -69,6 +70,7 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo) { Integer similarity = 50; +// Random random = new Random(); User user = matchResult.getUser(); @@ -87,6 +89,10 @@ static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceI // 3. 각 키워드들마다 매칭되는거 있으면 +10 similarity += matchResult.getTotalMatchCount()*10; + + //4. 1의 자리수 랜덤값으로 조정하여 다채롭게 (mvp단계 한정) +// similarity+=random.nextInt(10); + return similarity; } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index 3f621f8..e3eed82 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -65,7 +65,7 @@ public List recommendCurationsWithMatch(ContentPromptTempla recommendedKeyword, recommendedStyle )) - .sorted((a, b) -> Integer.compare(b.getTotalMatchCount(), a.getTotalMatchCount())) + .sorted((a, b) -> Integer.compare(b.getTotalMatchCount(), a.getTotalMatchCount())) // Todo 1. 현재 MatchCount가지고 정렬 -> 취향유사도 해당 로직에서 계산해서 정렬 필요 .collect(Collectors.toList()); } } \ No newline at end of file From 54eeca75897a2c4ae22690ef3489843ebf6b3fc6 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 8 Nov 2025 20:42:52 +0900 Subject: [PATCH 080/291] =?UTF-8?q?feat=20:=20=EA=B9=83=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/comment/controller/CommentController.java | 2 +- .../BookPick/mvp/domain/comment/entity/Comment.java | 2 +- .../comment/exception/CommentNotFoundException.java | 2 +- .../mvp/domain/comment/service/CommentService.java | 4 ++-- .../curation/controller/base/CurationController.java | 12 ------------ 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index 0df2b5d..b3b961c 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -10,7 +10,7 @@ import BookPick.mvp.domain.comment.dto.update.CommentUpdateRes; import BookPick.mvp.domain.comment.service.CommentService; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java index d1cd19a..7cfccab 100644 --- a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java +++ b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.comment.entity; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import jakarta.validation.constraints.Size; diff --git a/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java b/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java index 439c9ec..538170c 100644 --- a/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/comment/exception/CommentNotFoundException.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.comment.exception; -import BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class CommentNotFoundException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 4f74122..58b0bbf 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -10,8 +10,8 @@ import BookPick.mvp.domain.comment.entity.Comment; import BookPick.mvp.domain.comment.exception.CommentNotFoundException; import BookPick.mvp.domain.comment.repository.CommentRepository; -import BookPick.mvp.domain.curation.converter.exception.CurationNotFoundException; -import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 36033fd..521fc36 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -2,7 +2,6 @@ package BookPick.mvp.domain.curation.controller.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; -<<<<<<< HEAD:src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; @@ -10,17 +9,6 @@ import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.base.CurationService; -======= -import BookPick.mvp.domain.curation.SortType; -import BookPick.mvp.domain.curation.dto.create.CurationCreateReq; -import BookPick.mvp.domain.curation.dto.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.get.list.CurationListGetRes; -import BookPick.mvp.domain.curation.dto.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.delete.CurationDeleteRes; -import BookPick.mvp.domain.curation.service.CurationService; ->>>>>>> develop:src/main/java/BookPick/mvp/domain/curation/controller/CurationController.java import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.servlet.http.HttpServletRequest; From 26364e0a9ecbd5b6b7bfa5da1cea32e45cdb43ba Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 9 Nov 2025 18:45:07 +0900 Subject: [PATCH 081/291] =?UTF-8?q?feat=20:=20jjwt=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20jwtUtil=20?= =?UTF-8?q?=EC=98=A4=ED=86=A0=EC=99=80=EC=9D=B4=EC=96=B4=EB=A7=81=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 ++++--- .../BookPick/mvp/domain/auth/service/AuthService.java | 4 ++-- src/main/java/BookPick/mvp/global/config/JwtFilter.java | 3 ++- .../java/BookPick/mvp/global/config/SecurityConfig.java | 3 ++- src/main/java/BookPick/mvp/global/util/JwtUtil.java | 8 ++++---- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/build.gradle b/build.gradle index 448de7f..02c0277 100644 --- a/build.gradle +++ b/build.gradle @@ -41,15 +41,16 @@ dependencies { // ✅ Spring Framework 6.2.x / Boot 3.5.x 호환 springdoc implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.11' + implementation 'org.springframework.boot:spring-boot-starter-actuator' // (WebFlux면 위를 webflux-ui로 교체) // (선택) 개발 편의 - 자동 재시작/리로드 developmentOnly 'org.springframework.boot:spring-boot-devtools' // JWT (JJWT 0.12.x) - implementation 'io.jsonwebtoken:jjwt-api:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' - runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.5' // Gson 사용 중. Jackson 쓰려면 jjwt-jackson로 교체 + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-gson:0.12.6' // Gson 사용 중. Jackson 쓰려면 jjwt-jackson로 교체 // DB runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java index d2e8f44..8027f33 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java @@ -65,8 +65,8 @@ public LoginRes login(LoginReq req, HttpServletResponse res) { firstLoginCheck(req.email()); - String accessToken = JwtUtil.createAccessToken(auth); // Access O - String refreshToken = JwtUtil.createRefreshToken(auth); // Refresh X + String accessToken = jwtUtil.createAccessToken(auth); // Access O + String refreshToken = jwtUtil.createRefreshToken(auth); // Refresh X CustomUserDetails customUserDetails = (CustomUserDetails) auth.getPrincipal(); diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 82a8ba1..2d7d041 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -24,6 +24,7 @@ public class JwtFilter extends OncePerRequestFilter { private static final String BEARER = "Bearer"; + private final JwtUtil jwtUtil; @Override @@ -44,7 +45,7 @@ protected void doFilterInternal( return; } - Claims claims = JwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) + Claims claims = jwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) Long userId = claims.get("userId", Number.class).longValue(); String email = claims.get("email").toString(); diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java index 05e89f6..af656f8 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SecurityConfig.java @@ -22,9 +22,10 @@ @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { - private final JwtFilter jwtFilter; + private final JwtFilter jwtFilter; + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 0e6e2fe..3b4d201 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -22,7 +22,7 @@ @Component public class JwtUtil { // 1. 키발급 - static final SecretKey key = + final SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode( "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" )); @@ -32,7 +32,7 @@ public class JwtUtil { private static final long REFRESH_TTL_MS = 1000L * 60 * 60 * 24 * 14; // 14일 // 2. JWT 생성 - public static String createAccessToken(Authentication auth) { + public String createAccessToken(Authentication auth) { CustomUserDetails usr = (CustomUserDetails) auth.getPrincipal(); String authorities = auth.getAuthorities().stream() //getAuthorities -> List return @@ -52,7 +52,7 @@ public static String createAccessToken(Authentication auth) { } // ✅ 2-1. Refresh 토큰 생성 (여기 추가) - public static String createRefreshToken(Authentication auth) { + public String createRefreshToken(Authentication auth) { CustomUserDetails usr = (CustomUserDetails) auth.getPrincipal(); // refresh 토큰에는 최소 정보만: subject/email + typ 정도만 권장 @@ -67,7 +67,7 @@ public static String createRefreshToken(Authentication auth) { //3. JWT 오픈 - public static Claims extractToken(String token) { + public Claims extractToken(String token) { try { Claims claims = Jwts.parser().verifyWith(key).build() From ee1c039325dfd61972119cb148805ce91076a39d Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 9 Nov 2025 19:28:45 +0900 Subject: [PATCH 082/291] =?UTF-8?q?fix=20:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=97=90=EB=9F=AC=20=EC=A4=91=EB=8B=A8=EC=A0=90=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94=20->=20=EC=B6=94=ED=9B=84?= =?UTF-8?q?=20=EB=8B=A4=EB=A5=B8=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=A9?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=98=88=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../BookPick/mvp/global/util/JwtUtil.java | 9 ++- .../java/BookPick/mvp/GeminiClientMain.java | 67 ------------------- 3 files changed, 8 insertions(+), 70 deletions(-) delete mode 100644 src/test/java/BookPick/mvp/GeminiClientMain.java diff --git a/build.gradle b/build.gradle index 02c0277..8bf8d12 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.5' + id 'org.springframework.boot' version '3.5.6' id 'io.spring.dependency-management' version '1.1.7' } diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 3b4d201..9efe96b 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -70,9 +70,14 @@ public String createRefreshToken(Authentication auth) { public Claims extractToken(String token) { try { - Claims claims = Jwts.parser().verifyWith(key).build() - .parseSignedClaims(token).getPayload(); + Claims claims = Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); return claims; + + } catch (ExpiredJwtException e) { throw new JwtTokenExpiredException(); } catch (JwtException | IllegalArgumentException e) { diff --git a/src/test/java/BookPick/mvp/GeminiClientMain.java b/src/test/java/BookPick/mvp/GeminiClientMain.java deleted file mode 100644 index 900a07c..0000000 --- a/src/test/java/BookPick/mvp/GeminiClientMain.java +++ /dev/null @@ -1,67 +0,0 @@ -package BookPick.mvp; - -import BookPick.mvp.domain.curation.model.Curation; -import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; -import BookPick.mvp.domain.curation.util.gemini.prompt.ContentPromptTemplate; -import BookPick.mvp.domain.curation.util.gemini.service.GeminiService; -import BookPick.mvp.domain.preference.entity.ReadingPreference; -import BookPick.mvp.domain.user.entity.User; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -@SpringBootApplication -public class GeminiClientMain { - - public static void main(String[] args) { - SpringApplication.run(GeminiClientMain.class, args); - } - - @Autowired - private GeminiService geminiService; - - @Bean - CommandLineRunner runTest() { - return args -> { - User user = new User(); // 테스트용 유저 생성 - ReadingPreference preference = ReadingPreference.builder() - .user(user) - .mbti("INFP") - .moods(List.of("새벽 시간, 카페, 혼자만의 시간")) - .readingHabits(List.of("한 번에 완독하는 편, 조용한 곳에서만 읽는 편")) - .genres(List.of("에세이, 철학, 소설")) - .keywords(List.of("성장, 공감, 현실")) - .trends(List.of("몰입형, 감성적, 깊이 있는 사색")) - .build(); - - ContentPromptTemplate template = ContentPromptTemplate.builder() - .mbti(preference.getMbti()) - .mood(String.join(", ", preference.getMoods())) - .readingMethod(String.join(", ", preference.getReadingHabits())) - .genre(String.join(", ", preference.getGenres())) - .keyword(String.join(", ", preference.getKeywords())) - .readingStyle(String.join(", ", preference.getTrends())) - .build(); - - System.out.println("=== 추천된 큐레이션 ==="); - List results = geminiService.recommendCurationsWithMatch(template); - - System.out.println("총 " + results.size() + "개의 큐레이션 발견\n"); - - results.forEach(result -> { - Curation c = result.getCuration(); - System.out.println("📚 책 제목: " + c.getBookTitle()); - System.out.println(" 저자: " + c.getBookAuthor()); - System.out.println(" 총 일치: " + result.getTotalMatchCount() + "개"); - System.out.println("\n=== 일치한 태그 ==="); - System.out.print(result.getMatchedString()); - System.out.println(" 인기도: " + c.getPopularityScore()); - System.out.println("-----------------------------------"); - }); - }; - } -} From 1a93af47475a09aebd0295241e6342660bb39d0c Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 9 Nov 2025 22:55:27 +0900 Subject: [PATCH 083/291] =?UTF-8?q?feat=20:=20=EC=9E=91=EA=B0=80=20DTO=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/author/Entity/Author.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/author/Entity/Author.java diff --git a/src/main/java/BookPick/mvp/domain/author/Entity/Author.java b/src/main/java/BookPick/mvp/domain/author/Entity/Author.java new file mode 100644 index 0000000..52af1c7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/author/Entity/Author.java @@ -0,0 +1,26 @@ +package BookPick.mvp.domain.author.Entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +@Entity +public class Author { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String name; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + private LocalDateTime deletedAt; +} From cf33c40cf1f669da33dded11bb2b07ef5aa1c432 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 15:43:28 +0900 Subject: [PATCH 084/291] =?UTF-8?q?fix=20:=20=EC=9D=B8=ED=84=B0=ED=94=84?= =?UTF-8?q?=EB=A6=AC=ED=84=B0=20=EC=98=A4=EB=A5=98=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20@NotNull=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/author/Entity/Author.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/author/Entity/Author.java b/src/main/java/BookPick/mvp/domain/author/Entity/Author.java index 52af1c7..1ccc9dc 100644 --- a/src/main/java/BookPick/mvp/domain/author/Entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/Entity/Author.java @@ -15,7 +15,6 @@ public class Author { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @NotNull private String name; private LocalDateTime createdAt; From 6a774a708f6f679d709e7bd34f828cd10ff05f58 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 23:22:17 +0900 Subject: [PATCH 085/291] =?UTF-8?q?feat=20:=20=EC=9E=91=EA=B0=80=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/author/{Entity => entity}/Author.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) rename src/main/java/BookPick/mvp/domain/author/{Entity => entity}/Author.java (73%) diff --git a/src/main/java/BookPick/mvp/domain/author/Entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java similarity index 73% rename from src/main/java/BookPick/mvp/domain/author/Entity/Author.java rename to src/main/java/BookPick/mvp/domain/author/entity/Author.java index 1ccc9dc..08bd436 100644 --- a/src/main/java/BookPick/mvp/domain/author/Entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/entity/Author.java @@ -1,14 +1,16 @@ -package BookPick.mvp.domain.author.Entity; +package BookPick.mvp.domain.author.entity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import org.springframework.context.annotation.Bean; import java.time.LocalDateTime; @Entity +@Builder public class Author { @Id @@ -22,4 +24,8 @@ public class Author { private LocalDateTime updatedAt; private LocalDateTime deletedAt; + + public Author() { + + } } From 976b09e9b18dc78701b36042c65514b7db5f8249 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 23:22:33 +0900 Subject: [PATCH 086/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/book/entity/Book.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/book/entity/Book.java diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java new file mode 100644 index 0000000..70d4e06 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -0,0 +1,36 @@ +package BookPick.mvp.domain.book.entity; + +import BookPick.mvp.domain.author.entity.Author; +import jakarta.persistence.*; +import lombok.Builder; + +import java.util.HashSet; +import java.util.Set; + +@Entity +@Builder +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @ManyToMany + @JoinTable( + name = "book_id", + joinColumns = @JoinColumn(name = "book_id"), + inverseJoinColumns = @JoinColumn(name = "author_id") + ) + private Set authors = new HashSet<>(); // 하나의 책마다 여러 작가들 존재 + + private String image; + + private String isbn; + + + public Book() { + + } +} From 69f5de97c97d4a8f152bf26379110935538595e9 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 23:23:50 +0900 Subject: [PATCH 087/291] =?UTF-8?q?feat(author)=20:=20=EC=9E=91=EA=B0=80?= =?UTF-8?q?=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../author/repository/AuthorRepository.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java diff --git a/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java new file mode 100644 index 0000000..cdb8cde --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java @@ -0,0 +1,15 @@ +package BookPick.mvp.domain.author.repository; + + +// 1. 작가 레포가 왜 필요할까? +// 2. 추후 이러한 작가들이 많은 사용자들이 찾더라 저장 필요 + +import BookPick.mvp.domain.author.entity.Author; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AuthorRepository extends JpaRepository { + + +} From 7fe116733e7b7fc80a9d052710c4750ca33527c4 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 23:23:58 +0900 Subject: [PATCH 088/291] =?UTF-8?q?feat(book)=20:=20=EC=B1=85=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/book/repository/BookRepository.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java diff --git a/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java new file mode 100644 index 0000000..38431a7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java @@ -0,0 +1,16 @@ +package BookPick.mvp.domain.book.repository; + + +// 1. 책 레포 필요한 이유 +// 2. 추후 어떠한 책들이 인기가 있었는지 체크하기 위해 +// 3. 큐레이션 작성된 책들 count 필요 + +import BookPick.mvp.domain.book.entity.Book; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface BookRepository extends JpaRepository { + + +} From 4542fe34a5f9ec0d03dbf438de78fd8917ff0509 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 23:44:40 +0900 Subject: [PATCH 089/291] =?UTF-8?q?fix(book)=20:=20Builder=EC=9D=98=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=ED=95=98=EB=8A=94=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=ED=95=84=EC=9A=94=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?@AllArgsConstructor=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/author/entity/Author.java | 4 ++++ src/main/java/BookPick/mvp/domain/book/entity/Book.java | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/author/entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java index 08bd436..66d8eb7 100644 --- a/src/main/java/BookPick/mvp/domain/author/entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/entity/Author.java @@ -4,6 +4,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import lombok.AllArgsConstructor; import lombok.Builder; import org.springframework.context.annotation.Bean; @@ -11,6 +12,7 @@ @Entity @Builder +@AllArgsConstructor public class Author { @Id @@ -19,6 +21,8 @@ public class Author { private String name; + private Integer curated_count; + private LocalDateTime createdAt; private LocalDateTime updatedAt; diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java index 70d4e06..ea75354 100644 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -2,6 +2,7 @@ import BookPick.mvp.domain.author.entity.Author; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Builder; import java.util.HashSet; @@ -9,6 +10,7 @@ @Entity @Builder +@AllArgsConstructor public class Book { @Id From 2d38385e54b39dd36bf51428edd12c348f4c2de8 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 10 Nov 2025 23:54:08 +0900 Subject: [PATCH 090/291] =?UTF-8?q?chore(ReadingPreference)=20:=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=AA=85=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?Preference=20->=20ReadingPreference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...yRegisteredReadingPreferenceException.java | 2 +- .../UserReadingPreferenceNotExisted.java | 2 +- .../PrferenceErrorCode.java | 2 +- .../ReadingPreferenceController.java | 16 ++++++------- .../Create/ReadingPreferenceCreateReq.java | 2 +- .../Create/ReadingPreferenceCreateRes.java | 4 ++-- .../Delete/ReadingPreferenceDeleteRes.java | 2 +- .../dto/Get/ReadingPreferenceGetReq.java | 2 +- .../dto/Get/ReadingPreferenceGetRes.java | 4 ++-- .../Update/ReadingPreferenceUpdateReq.java | 2 +- .../Update/ReadingPreferenceUpdateRes.java | 4 ++-- .../entity/ReadingPreference.java | 4 ++-- .../ReadingPreferenceRepository.java | 4 ++-- .../service/ReadingPreferenceService.java | 24 +++++++++---------- .../dto/prefer/ReadingPreferenceInfo.java | 2 +- .../service/list/CurationListService.java | 4 ++-- .../list/Handler/CurationPageHandler.java | 3 +-- 17 files changed, 41 insertions(+), 42 deletions(-) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/Exception/AlreadyRegisteredReadingPreferenceException.java (85%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/Exception/UserReadingPreferenceNotExisted.java (83%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/PrferenceErrorCode.java (90%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/controller/ReadingPreferenceController.java (83%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Create/ReadingPreferenceCreateReq.java (88%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Create/ReadingPreferenceCreateRes.java (68%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Delete/ReadingPreferenceDeleteRes.java (86%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Get/ReadingPreferenceGetReq.java (60%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Get/ReadingPreferenceGetRes.java (87%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Update/ReadingPreferenceUpdateReq.java (89%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/dto/Update/ReadingPreferenceUpdateRes.java (86%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/entity/ReadingPreference.java (94%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/repository/ReadingPreferenceRepository.java (69%) rename src/main/java/BookPick/mvp/domain/{preference => ReadingPreference}/service/ReadingPreferenceService.java (77%) diff --git a/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java similarity index 85% rename from src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java index 0593f0e..af48835 100644 --- a/src/main/java/BookPick/mvp/domain/preference/Exception/AlreadyRegisteredReadingPreferenceException.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference.Exception; +package BookPick.mvp.domain.ReadingPreference.Exception; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java index 5a7e588..32db36f 100644 --- a/src/main/java/BookPick/mvp/domain/preference/Exception/UserReadingPreferenceNotExisted.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference.Exception; +package BookPick.mvp.domain.ReadingPreference.Exception; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java similarity index 90% rename from src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java index 75d038a..eb32108 100644 --- a/src/main/java/BookPick/mvp/domain/preference/PrferenceErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference; +package BookPick.mvp.domain.ReadingPreference; import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; import lombok.AllArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java index 3952361..09a9e0b 100644 --- a/src/main/java/BookPick/mvp/domain/preference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -1,13 +1,13 @@ -package BookPick.mvp.domain.preference.controller; +package BookPick.mvp.domain.ReadingPreference.controller; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateReq; -import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateRes; -import BookPick.mvp.domain.preference.dto.Delete.ReadingPreferenceDeleteRes; -import BookPick.mvp.domain.preference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; -import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateRes; -import BookPick.mvp.domain.preference.service.ReadingPreferenceService; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; +import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; +import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java similarity index 88% rename from src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateReq.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java index 9b9addc..48fe4ef 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference.dto.Create; +package BookPick.mvp.domain.ReadingPreference.dto.Create; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java similarity index 68% rename from src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateRes.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java index ea500a5..1145228 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Create/ReadingPreferenceCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java @@ -1,7 +1,7 @@ -package BookPick.mvp.domain.preference.dto.Create; +package BookPick.mvp.domain.ReadingPreference.dto.Create; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; public record ReadingPreferenceCreateRes( Long readingPreferenceId diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Delete/ReadingPreferenceDeleteRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java similarity index 86% rename from src/main/java/BookPick/mvp/domain/preference/dto/Delete/ReadingPreferenceDeleteRes.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java index 5afb46b..7aea8f1 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Delete/ReadingPreferenceDeleteRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference.dto.Delete; +package BookPick.mvp.domain.ReadingPreference.dto.Delete; import java.time.LocalDateTime; diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java similarity index 60% rename from src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetReq.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java index 4a68237..d6bd7b8 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference.dto.Get; +package BookPick.mvp.domain.ReadingPreference.dto.Get; // -- 독서 취향 조회 요청 -- public record ReadingPreferenceGetReq () diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java similarity index 87% rename from src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetRes.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java index 0175320..b1e1ada 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Get/ReadingPreferenceGetRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.preference.dto.Get; +package BookPick.mvp.domain.ReadingPreference.dto.Get; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java similarity index 89% rename from src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java index f68d47c..9831e2f 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.preference.dto.Update; +package BookPick.mvp.domain.ReadingPreference.dto.Update; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java similarity index 86% rename from src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateRes.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java index e9ba89e..f830ff5 100644 --- a/src/main/java/BookPick/mvp/domain/preference/dto/Update/ReadingPreferenceUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.preference.dto.Update; +package BookPick.mvp.domain.ReadingPreference.dto.Update; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java similarity index 94% rename from src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 939294a..b15c20f 100644 --- a/src/main/java/BookPick/mvp/domain/preference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.preference.entity; +package BookPick.mvp.domain.ReadingPreference.entity; -import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java similarity index 69% rename from src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java index c1a6d06..92d9029 100644 --- a/src/main/java/BookPick/mvp/domain/preference/repository/ReadingPreferenceRepository.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/repository/ReadingPreferenceRepository.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.preference.repository; +package BookPick.mvp.domain.ReadingPreference.repository; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java similarity index 77% rename from src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 69d7ad8..a607907 100644 --- a/src/main/java/BookPick/mvp/domain/preference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -1,15 +1,15 @@ -package BookPick.mvp.domain.preference.service; - -import BookPick.mvp.domain.preference.Exception.AlreadyRegisteredReadingPreferenceException; -import BookPick.mvp.domain.preference.Exception.UserReadingPreferenceNotExisted; -import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateReq; -import BookPick.mvp.domain.preference.dto.Create.ReadingPreferenceCreateRes; -import BookPick.mvp.domain.preference.dto.Delete.ReadingPreferenceDeleteRes; -import BookPick.mvp.domain.preference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateReq; -import BookPick.mvp.domain.preference.dto.Update.ReadingPreferenceUpdateRes; -import BookPick.mvp.domain.preference.entity.ReadingPreference; -import BookPick.mvp.domain.preference.repository.ReadingPreferenceRepository; +package BookPick.mvp.domain.ReadingPreference.service; + +import BookPick.mvp.domain.ReadingPreference.Exception.AlreadyRegisteredReadingPreferenceException; +import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; +import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java index 66788da..2588a0d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.dto.prefer; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index bd5290c..fb9bfc9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -9,8 +9,8 @@ import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; -import BookPick.mvp.domain.preference.Exception.UserReadingPreferenceNotExisted; -import BookPick.mvp.domain.preference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index 83fb4d0..da32b0e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -7,8 +7,7 @@ import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; -import BookPick.mvp.domain.preference.Exception.UserReadingPreferenceNotExisted; -import BookPick.mvp.domain.preference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; From 89afcee55fd1a24066e458d1c1b586232c79d6ec Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 00:03:35 +0900 Subject: [PATCH 091/291] chore: meaningless commit --- .../controller/ReadingPreferenceController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java index 09a9e0b..614f18c 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -32,6 +32,7 @@ public ResponseEntity> create( @Valid @RequestBody ReadingPreferenceCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceCreateRes res = readingPreferenceService.addReadingPreference(currentUser.getId(), req); + return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_REGISTER_SUCCESS, res)); } @@ -41,6 +42,7 @@ public ResponseEntity> create( public ResponseEntity> getDetails( @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceGetRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); + return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_READ_SUCCESS, res)); } @@ -51,6 +53,7 @@ public ResponseEntity> update( @Valid @RequestBody ReadingPreferenceUpdateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceUpdateRes res = readingPreferenceService.modifyReadingPreference(currentUser.getId(), req); + return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res)); } @@ -60,6 +63,7 @@ public ResponseEntity> update( public ResponseEntity> delete( @AuthenticationPrincipal CustomUserDetails currentUser) { ReadingPreferenceDeleteRes res = readingPreferenceService.removeReadingPreference(currentUser.getId()); + return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_DELETE_SUCCESS, res)); } From 18fe2fc3b29467205575bcae1b37507f690f791f Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 21:02:26 +0900 Subject: [PATCH 092/291] chore: meaningless commit --- .../Create/ReadingPreferenceCreateReq.java | 4 ++- .../entity/ReadingPreference.java | 13 +++++--- .../service/ReadingPreferenceService.java | 30 +++++++++++++++++++ .../BookPick/mvp/domain/book/entity/Book.java | 4 +++ .../book/repository/BookRepository.java | 3 ++ .../dto/prefer/ReadingPreferenceInfo.java | 3 +- 6 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java index 48fe4ef..f9abb1c 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java @@ -1,10 +1,12 @@ package BookPick.mvp.domain.ReadingPreference.dto.Create; +import BookPick.mvp.domain.book.entity.Book; + import java.util.List; public record ReadingPreferenceCreateReq( String mbti, - List favoriteBooks, // 좋아하는 책 + List favoriteBooks, // 좋아하는 책 List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index b15c20f..b383417 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.ReadingPreference.entity; import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; @@ -30,10 +31,14 @@ public class ReadingPreference { @Column(name = "authors") private List favoriteAuthors; - @ElementCollection - @CollectionTable(name = "preference_favorite_books", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "book") - private List favoriteBooks; + + @ManyToMany + @JoinTable( + name = "preference_favorite_books", + joinColumns = @JoinColumn(name = "preference_id"), + inverseJoinColumns = @JoinColumn(name = "book_id") + ) + private List favoriteBooks; @ElementCollection @CollectionTable(name = "preference_moods", joinColumns = @JoinColumn(name = "preference_id")) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index a607907..1ac8bef 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -10,6 +10,9 @@ import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.author.repository.AuthorRepository; +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.repository.BookRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; @@ -18,24 +21,51 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; @Service @RequiredArgsConstructor public class ReadingPreferenceService { private final ReadingPreferenceRepository readingPreferenceRepository; private final UserRepository userRepository; + private final BookRepository bookRepository; + private final AuthorRepository authorRepository; + // -- 유저 독서 취향 등록 -- @Transactional public ReadingPreferenceCreateRes addReadingPreference(Long userId, ReadingPreferenceCreateReq req) { + // 1. 유저 검색 User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); + // 2. 독서취향이 이미 존재하면 이미 존재하는 독서취향입니다. + // Todo 1. 독서취향 생성은 처음 회원가입시 바로 생성되고 null값 넣는 것으로 변경 필요 if (readingPreferenceRepository.existsByUserId(userId)) { throw new AlreadyRegisteredReadingPreferenceException(); } + // 3. 독서취향에서 책 꺼내서, 첵 db에 책 등록되어있지 않으면 저장. 등록되어있으면 가져오기 + List preferedBooks = req.favoriteBooks(); + for (Book preferedBook : preferedBooks){ + Optional existingBook = bookRepository.findByTitle(preferedBook.getTitle()); + if(existingBook==null){ + bookRepository.save(preferedBook); + } + else{ + + } + } + + + // 4. 독서취향에서 작가 꺼내서, 작가가 db에 등록되어있지 않으면 저장, 등록되어 있으면 가져오기 + String userPreferAuthorName = req + + + + ReadingPreference readingPreference = ReadingPreference.builder() .user(user) diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java index ea75354..6553c1b 100644 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -4,6 +4,8 @@ import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; +import lombok.Setter; import java.util.HashSet; import java.util.Set; @@ -11,6 +13,8 @@ @Entity @Builder @AllArgsConstructor +@Getter +@Setter public class Book { @Id diff --git a/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java index 38431a7..0c15576 100644 --- a/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java +++ b/src/main/java/BookPick/mvp/domain/book/repository/BookRepository.java @@ -9,8 +9,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface BookRepository extends JpaRepository { + Optional findByTitle(String title); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java index 2588a0d..eac03f4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.curation.dto.prefer; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.book.entity.Book; import java.util.List; @@ -8,7 +9,7 @@ public record ReadingPreferenceInfo( Long userId, String mbti, List favoriteAuthors, - List favoriteBooks, + List favoriteBooks, List readingHabits, List moods, List genres, From 2ccafbdcfd6ef8f65c5dce89055adc7061c46163 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 21:21:09 +0900 Subject: [PATCH 093/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=91=EA=B0=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A7=91=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EC=B1=85=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9E=91=EA=B0=80=EA=B0=80=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9C=BC=EB=A9=B4=20DB=EC=97=90=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Create/ReadingPreferenceCreateReq.java | 2 ++ .../service/ReadingPreferenceService.java | 31 +++++++------------ .../mvp/domain/author/entity/Author.java | 4 +++ .../author/repository/AuthorRepository.java | 3 ++ .../author/service/AuthorSaveService.java | 27 ++++++++++++++++ .../domain/book/service/BookSaveService.java | 31 +++++++++++++++++++ 6 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java create mode 100644 src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java index f9abb1c..1040b85 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.ReadingPreference.dto.Create; +import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.book.entity.Book; import java.util.List; @@ -7,6 +8,7 @@ public record ReadingPreferenceCreateReq( String mbti, List favoriteBooks, // 좋아하는 책 + List favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 1ac8bef..50538f8 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -10,13 +10,17 @@ import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.author.repository.AuthorRepository; +import BookPick.mvp.domain.author.service.AuthorSaveService; import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.book.repository.BookRepository; +import BookPick.mvp.domain.book.service.BookSaveService; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,14 +33,17 @@ public class ReadingPreferenceService { private final ReadingPreferenceRepository readingPreferenceRepository; private final UserRepository userRepository; - private final BookRepository bookRepository; - private final AuthorRepository authorRepository; + private final BookSaveService bookSaveService; + private final AuthorSaveService authorSaveService; + + // -- 유저 독서 취향 등록 -- @Transactional public ReadingPreferenceCreateRes addReadingPreference(Long userId, ReadingPreferenceCreateReq req) { + // 1. 유저 검색 User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); @@ -47,24 +54,8 @@ public ReadingPreferenceCreateRes addReadingPreference(Long userId, ReadingPrefe throw new AlreadyRegisteredReadingPreferenceException(); } - // 3. 독서취향에서 책 꺼내서, 첵 db에 책 등록되어있지 않으면 저장. 등록되어있으면 가져오기 - List preferedBooks = req.favoriteBooks(); - for (Book preferedBook : preferedBooks){ - Optional existingBook = bookRepository.findByTitle(preferedBook.getTitle()); - if(existingBook==null){ - bookRepository.save(preferedBook); - } - else{ - - } - } - - - // 4. 독서취향에서 작가 꺼내서, 작가가 db에 등록되어있지 않으면 저장, 등록되어 있으면 가져오기 - String userPreferAuthorName = req - - - + bookSaveService.saveIfNotExists(req.favoriteBooks()); + authorSaveService.saveIfNotExists(req.favoriteAuthors()); ReadingPreference readingPreference = ReadingPreference.builder() diff --git a/src/main/java/BookPick/mvp/domain/author/entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java index 66d8eb7..195132f 100644 --- a/src/main/java/BookPick/mvp/domain/author/entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/entity/Author.java @@ -6,6 +6,8 @@ import jakarta.persistence.Id; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; +import lombok.Setter; import org.springframework.context.annotation.Bean; import java.time.LocalDateTime; @@ -13,6 +15,8 @@ @Entity @Builder @AllArgsConstructor +@Getter +@Setter public class Author { @Id diff --git a/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java index cdb8cde..14a135f 100644 --- a/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java +++ b/src/main/java/BookPick/mvp/domain/author/repository/AuthorRepository.java @@ -8,8 +8,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface AuthorRepository extends JpaRepository { + Optional findByName(String name); } diff --git a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java new file mode 100644 index 0000000..593d8c6 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java @@ -0,0 +1,27 @@ +package BookPick.mvp.domain.author.service; + +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.author.repository.AuthorRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class AuthorSaveService { + private final AuthorRepository authorRepository; + + // 1.독서취향에서 작가 꺼내서, 작가가 db에 등록되어있지 않으면 저장 + public void saveIfNotExists(List authors) { + List preferredAuthors = authors; + for (Author preferredAuthor : preferredAuthors) { + Optional existingAuthor = authorRepository.findByName(preferredAuthor.getName()); + if (existingAuthor.isEmpty()) { + authorRepository.save(preferredAuthor); + } + } + } + +} diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java new file mode 100644 index 0000000..89e1f74 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.book.service; + +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.author.repository.AuthorRepository; +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.repository.BookRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class BookSaveService { + private final BookRepository bookRepository; + + // 1. 독서취향에서 책 꺼내서, 첵 db에 책 등록되어있지 않으면 저장 + public void saveIfNotExists(List books) { + List preferredBooks = books; + for (Book preferedBook : preferredBooks) { + Optional existingBook = bookRepository.findByTitle(preferedBook.getTitle()); + if (existingBook.isEmpty()) { + bookRepository.save(preferedBook); + } + } + + } + + +} From 5c6a0e1719a56819449cd015caf15ace98aaca9b Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:23:43 +0900 Subject: [PATCH 094/291] =?UTF-8?q?feat=20:=20=EC=9E=91=EA=B0=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=A5=BC=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EC=97=90=EA=B2=8C=20=EB=B0=9B=EA=B8=B0?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=9E=91=EA=B0=80=20DTO=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/author/dto/preference/AuthorDto.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/author/dto/preference/AuthorDto.java diff --git a/src/main/java/BookPick/mvp/domain/author/dto/preference/AuthorDto.java b/src/main/java/BookPick/mvp/domain/author/dto/preference/AuthorDto.java new file mode 100644 index 0000000..c2409fb --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/author/dto/preference/AuthorDto.java @@ -0,0 +1,9 @@ +package BookPick.mvp.domain.author.dto.preference; + +import java.time.LocalDateTime; + +public record AuthorDto( + String name // 요청/응답용: 작가 이름 +) { + +} From 7963113f4d7136e373fb7bca67cd324f4496fae1 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:24:40 +0900 Subject: [PATCH 095/291] chore: meaningless commit --- .../Create/ReadingPreferenceCreateRes.java | 17 ---------- .../Create/ReadingPreferenceCreateReq.java | 18 ++++++++++ .../Create/ReadingPreferenceCreateRes.java | 17 ++++++++++ .../dto/ETC/Get/ReadingPreferenceGetReq.java | 7 ++++ .../dto/ETC/Get/ReadingPreferenceGetRes.java | 33 +++++++++++++++++++ .../dto/Get/ReadingPreferenceGetReq.java | 7 ---- .../dto/Get/ReadingPreferenceGetRes.java | 33 ------------------- .../mvp/domain/book/dto/BookDtos.java | 24 -------------- 8 files changed, 75 insertions(+), 81 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java deleted file mode 100644 index 1145228..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateRes.java +++ /dev/null @@ -1,17 +0,0 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Create; - - -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; - -public record ReadingPreferenceCreateRes( - Long readingPreferenceId - ) { - static public ReadingPreferenceCreateRes from(ReadingPreference readingPreference) { - return new ReadingPreferenceCreateRes(readingPreference.getId()); - } - } - - - - - diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java new file mode 100644 index 0000000..903fb9e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java @@ -0,0 +1,18 @@ +//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Create; +// +//import BookPick.mvp.domain.author.entity.Author; +//import BookPick.mvp.domain.book.entity.Book; +// +//import java.util.List; +// +//public record ReadingPreferenceReq( +// String mbti, +// List favoriteBooks, // 좋아하는 책 +// List favoriteAuthors, +// List moods, // 독서 선호 분위기 +// List readingHabits, // 독서 습관 +// List genres, // 선호 장르 +// List keywords, // 키워드 +// List trends // +//) { +//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java new file mode 100644 index 0000000..cb8b9e9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java @@ -0,0 +1,17 @@ +//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Create; +// +// +//import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +// +//public record ReadingPreferenceRes( +// Long readingPreferenceId +// ) { +// static public ReadingPreferenceRes from(ReadingPreference readingPreference) { +// return new ReadingPreferenceRes(readingPreference.getId()); +// } +// } +// +// +// +// +// diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java new file mode 100644 index 0000000..666adad --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java @@ -0,0 +1,7 @@ +//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Get; +// +//// -- 독서 취향 조회 요청 -- +//public record ReadingPreferenceGetReq () +//{ +// +//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java new file mode 100644 index 0000000..f5e771d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java @@ -0,0 +1,33 @@ +//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Get; +// +//import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +// +//import java.util.List; +// +//// -- 독서 취향 조회 응답 -- +//public record ReadingPreferenceRes( +// Long preferenceId, +// String mbti, +// List favoriteBooks, // 좋아하는 책 +// List moods, // 독서 선호 분위기 +// List readingHabits, // 독서 습관 +// List genres, // 선호 장르 +// List keywords, // 키워드 +// List trends +//){ +// static public ReadingPreferenceRes from(ReadingPreference rp){ +// return new ReadingPreferenceRes( +// rp.getId(), +// rp.getMbti(), +// rp.getFavoriteBooks(), +// rp.getMoods(), +// rp.getReadingHabits(), +// rp.getGenres(), +// rp.getKeywords(), +// rp.getTrends() +// ); +// } +//} +// +// +// diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java deleted file mode 100644 index d6bd7b8..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetReq.java +++ /dev/null @@ -1,7 +0,0 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Get; - -// -- 독서 취향 조회 요청 -- -public record ReadingPreferenceGetReq () -{ - -} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java deleted file mode 100644 index b1e1ada..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Get/ReadingPreferenceGetRes.java +++ /dev/null @@ -1,33 +0,0 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Get; - -import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; - -import java.util.List; - -// -- 독서 취향 조회 응답 -- -public record ReadingPreferenceGetRes( - Long preferenceId, - String mbti, - List favoriteBooks, // 좋아하는 책 - List moods, // 독서 선호 분위기 - List readingHabits, // 독서 습관 - List genres, // 선호 장르 - List keywords, // 키워드 - List trends -){ - static public ReadingPreferenceGetRes from(ReadingPreference rp){ - return new ReadingPreferenceGetRes( - rp.getId(), - rp.getMbti(), - rp.getFavoriteBooks(), - rp.getMoods(), - rp.getReadingHabits(), - rp.getGenres(), - rp.getKeywords(), - rp.getTrends() - ); - } -} - - - diff --git a/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java b/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java deleted file mode 100644 index 16ab25f..0000000 --- a/src/main/java/BookPick/mvp/domain/book/dto/BookDtos.java +++ /dev/null @@ -1,24 +0,0 @@ -package BookPick.mvp.domain.book.dto; - - -import BookPick.mvp.global.dto.PageInfo; -import java.util.List; - -public class BookDtos { - - // -- R -- - public record BookSearchReq( - String keyword, - Integer page - ){} - public record BookSearchRes( - String title, - String author, - String image -) {} - public record BookSearchPageRes( - List books, - PageInfo pageInfo -) {} - -} From 2ebf8a0331882a1a199ec125d8fbc890cb5d057c Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:24:45 +0900 Subject: [PATCH 096/291] chore: meaningless commit --- .../ETC/Update/ReadingPreferenceUpdateReq.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java new file mode 100644 index 0000000..bf6baf2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java @@ -0,0 +1,16 @@ +//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Update; +// +//import java.util.List; +// +//public record ReadingPreferenceReq( +// Long preferenceId, +// String mbti, +// List favoriteAuthors, // 좋아하는 책 +// List favoriteBooks, // 좋아하는 책 +// List moods, // 독서 선호 분위기 +// List readingHabits, // 독서 습관 +// List genres, // 선호 장르 +// List keywords, // 키워드 +// List trends +//) { +//} From e76012afea336c6ec881258f6da0e9b41603d892 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:25:13 +0900 Subject: [PATCH 097/291] =?UTF-8?q?chore:=20CRUD=20DTO=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5=20DTO?= =?UTF-8?q?=20=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Update/ReadingPreferenceUpdateRes.java | 29 +++++++++++++++++++ .../Update/ReadingPreferenceUpdateReq.java | 16 ---------- 2 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java new file mode 100644 index 0000000..3964522 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java @@ -0,0 +1,29 @@ +//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Update; +// +//import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +// +//import java.util.List; +// +//public record ReadingPreferenceRes( +// Long preferenceId, +// String mbti, +// List favoriteBooks, // 좋아하는 책 +// List moods, // 독서 선호 분위기 +// List readingHabits, // 독서 습관 +// List genres, // 선호 장르 +// List keywords, // 키워드 +// List trends +//) { +// static public ReadingPreferenceRes from(ReadingPreference rp){ +// return new ReadingPreferenceRes( +// rp.getId(), +// rp.getMbti(), +// rp.getFavoriteBooks(), +// rp.getMoods(), +// rp.getReadingHabits(), +// rp.getGenres(), +// rp.getKeywords(), +// rp.getTrends() +// ); +// } +//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java deleted file mode 100644 index 9831e2f..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateReq.java +++ /dev/null @@ -1,16 +0,0 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Update; - -import java.util.List; - -public record ReadingPreferenceUpdateReq( - Long preferenceId, - String mbti, - List favoriteAuthors, // 좋아하는 책 - List favoriteBooks, // 좋아하는 책 - List moods, // 독서 선호 분위기 - List readingHabits, // 독서 습관 - List genres, // 선호 장르 - List keywords, // 키워드 - List trends -) { -} From c86b7418096aa0dbc7b45cfbd481b64816239b96 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:26:18 +0900 Subject: [PATCH 098/291] chore: meaningless commit --- .../ReadingPreferenceController.java | 25 ++++++++--------- .../Delete/ReadingPreferenceDeleteRes.java | 2 +- ...eateReq.java => ReadingPreferenceReq.java} | 12 ++++---- ...dateRes.java => ReadingPreferenceRes.java} | 24 ++++++++++++---- .../service/ReadingPreferenceService.java | 28 ++++++------------- .../book/Controller/BookSearchController.java | 3 +- .../book/dto/search/BookSearchPageRes.java | 11 ++++++++ .../domain/book/dto/search/BookSearchReq.java | 8 ++++++ .../domain/book/dto/search/BookSearchRes.java | 8 ++++++ .../book/service/BookSearchService.java | 5 ++-- .../dto/base/get/list/CurationContentRes.java | 3 +- .../dto/prefer/ReadingPreferenceInfo.java | 5 ++-- 12 files changed, 83 insertions(+), 51 deletions(-) rename src/main/java/BookPick/mvp/domain/ReadingPreference/dto/{ => ETC}/Delete/ReadingPreferenceDeleteRes.java (86%) rename src/main/java/BookPick/mvp/domain/ReadingPreference/dto/{Create/ReadingPreferenceCreateReq.java => ReadingPreferenceReq.java} (56%) rename src/main/java/BookPick/mvp/domain/ReadingPreference/dto/{Update/ReadingPreferenceUpdateRes.java => ReadingPreferenceRes.java} (57%) create mode 100644 src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchPageRes.java create mode 100644 src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchReq.java create mode 100644 src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java index 614f18c..0e03050 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -1,12 +1,9 @@ package BookPick.mvp.domain.ReadingPreference.controller; +import BookPick.mvp.domain.ReadingPreference.dto.ETC.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceRes; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; -import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; @@ -28,10 +25,10 @@ public class ReadingPreferenceController { @Operation(summary = "독서 취향 생성", description = "사용자의 독서 취향을 등록합니다", tags = {"Reading Preference"}) @PostMapping - public ResponseEntity> create( - @Valid @RequestBody ReadingPreferenceCreateReq req, + public ResponseEntity> create( + @Valid @RequestBody ReadingPreferenceReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { - ReadingPreferenceCreateRes res = readingPreferenceService.addReadingPreference(currentUser.getId(), req); + ReadingPreferenceRes res = readingPreferenceService.addReadingPreference(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_REGISTER_SUCCESS, res)); @@ -39,9 +36,9 @@ public ResponseEntity> create( @Operation(summary = "독서 취향 조회", description = "사용자의 독서 취향 상세 조회", tags = {"Reading Preference"}) @GetMapping - public ResponseEntity> getDetails( + public ResponseEntity> getDetails( @AuthenticationPrincipal CustomUserDetails currentUser) { - ReadingPreferenceGetRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); + ReadingPreferenceRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_READ_SUCCESS, res)); @@ -49,10 +46,10 @@ public ResponseEntity> getDetails( @Operation(summary = "독서 취향 수정", description = "사용자의 독서 취향을 수정합니다", tags = {"Reading Preference"}) @PatchMapping - public ResponseEntity> update( - @Valid @RequestBody ReadingPreferenceUpdateReq req, + public ResponseEntity> update( + @Valid @RequestBody ReadingPreferenceReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { - ReadingPreferenceUpdateRes res = readingPreferenceService.modifyReadingPreference(currentUser.getId(), req); + ReadingPreferenceRes res = readingPreferenceService.modifyReadingPreference(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.READING_PREFERENCE_UPDATE_SUCCESS, res)); diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Delete/ReadingPreferenceDeleteRes.java similarity index 86% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Delete/ReadingPreferenceDeleteRes.java index 7aea8f1..e8ead7e 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Delete/ReadingPreferenceDeleteRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Delete/ReadingPreferenceDeleteRes.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Delete; +package BookPick.mvp.domain.ReadingPreference.dto.ETC.Delete; import java.time.LocalDateTime; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java similarity index 56% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java index 1040b85..7d8f23a 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Create/ReadingPreferenceCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java @@ -1,18 +1,20 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Create; +package BookPick.mvp.domain.ReadingPreference.dto; +import BookPick.mvp.domain.author.dto.preference.AuthorDto; import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.book.dto.preference.BookDto; import BookPick.mvp.domain.book.entity.Book; import java.util.List; -public record ReadingPreferenceCreateReq( +public record ReadingPreferenceReq( String mbti, - List favoriteBooks, // 좋아하는 책 - List favoriteAuthors, + List favoriteBooks, // 좋아하는 책 + List favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 List keywords, // 키워드 List trends // ) { -} + } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java similarity index 57% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java index f830ff5..561e0f4 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/Update/ReadingPreferenceUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java @@ -1,29 +1,41 @@ -package BookPick.mvp.domain.ReadingPreference.dto.Update; +package BookPick.mvp.domain.ReadingPreference.dto; + import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.book.entity.Book; import java.util.List; -public record ReadingPreferenceUpdateRes( +public record ReadingPreferenceRes( Long preferenceId, String mbti, - List favoriteBooks, // 좋아하는 책 + List favoriteBooks, // 좋아하는 책 + List favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 List keywords, // 키워드 - List trends + List trends // ) { - static public ReadingPreferenceUpdateRes from(ReadingPreference rp){ - return new ReadingPreferenceUpdateRes( + + static public ReadingPreferenceRes from(ReadingPreference rp) { + return new ReadingPreferenceRes( rp.getId(), rp.getMbti(), rp.getFavoriteBooks(), + rp.getFavoriteAuthors(), rp.getMoods(), rp.getReadingHabits(), rp.getGenres(), rp.getKeywords(), rp.getTrends() ); + } } + + + + + diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 50538f8..a72e9cc 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -2,31 +2,21 @@ import BookPick.mvp.domain.ReadingPreference.Exception.AlreadyRegisteredReadingPreferenceException; import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Create.ReadingPreferenceCreateRes; -import BookPick.mvp.domain.ReadingPreference.dto.Delete.ReadingPreferenceDeleteRes; -import BookPick.mvp.domain.ReadingPreference.dto.Get.ReadingPreferenceGetRes; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateRes; +import BookPick.mvp.domain.ReadingPreference.dto.ETC.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceRes; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; -import BookPick.mvp.domain.author.entity.Author; -import BookPick.mvp.domain.author.repository.AuthorRepository; import BookPick.mvp.domain.author.service.AuthorSaveService; -import BookPick.mvp.domain.book.entity.Book; -import BookPick.mvp.domain.book.repository.BookRepository; import BookPick.mvp.domain.book.service.BookSaveService; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -41,7 +31,7 @@ public class ReadingPreferenceService { // -- 유저 독서 취향 등록 -- @Transactional - public ReadingPreferenceCreateRes addReadingPreference(Long userId, ReadingPreferenceCreateReq req) { + public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceReq req) { // 1. 유저 검색 @@ -72,25 +62,25 @@ public ReadingPreferenceCreateRes addReadingPreference(Long userId, ReadingPrefe ReadingPreference saved = readingPreferenceRepository.save(readingPreference); - return ReadingPreferenceCreateRes.from(saved); + return ReadingPreferenceRes.from(saved); } // -- 유저 독서 취향 단건 조회 -- @Transactional - public ReadingPreferenceGetRes findReadingPreference(Long userId) { + public ReadingPreferenceRes findReadingPreference(Long userId) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); ReadingPreference result = readingPreferenceRepository.findByUserId(userId) .orElseThrow(UserReadingPreferenceNotExisted::new); - return ReadingPreferenceGetRes.from(result); + return ReadingPreferenceRes.from(result); } // -- 본인 유저 독서 수정 -- @Transactional - public ReadingPreferenceUpdateRes modifyReadingPreference(Long userId, ReadingPreferenceUpdateReq req) { + public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferenceReq req) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); @@ -99,7 +89,7 @@ public ReadingPreferenceUpdateRes modifyReadingPreference(Long userId, ReadingPr preference.update(req); - return ReadingPreferenceUpdateRes.from(preference); + return ReadingPreferenceRes.from(preference); } // -- 본인 유저 독서 삭제 -- diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index a372510..b0e43c3 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.book.Controller; -import BookPick.mvp.domain.book.dto.BookDtos.*; +import BookPick.mvp.domain.book.dto.search.BookSearchPageRes; +import BookPick.mvp.domain.book.dto.search.BookSearchReq; import BookPick.mvp.domain.book.service.BookSearchService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; diff --git a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchPageRes.java b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchPageRes.java new file mode 100644 index 0000000..0a5d45b --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchPageRes.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.book.dto.search; + +import BookPick.mvp.global.dto.PageInfo; + +import java.util.List; + +public record BookSearchPageRes( + List books, + PageInfo pageInfo +) { +} diff --git a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchReq.java b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchReq.java new file mode 100644 index 0000000..86f31b1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchReq.java @@ -0,0 +1,8 @@ +package BookPick.mvp.domain.book.dto.search; + +// -- R -- +public record BookSearchReq( + String keyword, + Integer page +) { +} diff --git a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java new file mode 100644 index 0000000..41a0476 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java @@ -0,0 +1,8 @@ +package BookPick.mvp.domain.book.dto.search; + +public record BookSearchRes( + String title, + String author, + String image +) { +} diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java index 12e0ce9..27cebaa 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java @@ -1,6 +1,8 @@ package BookPick.mvp.domain.book.service; -import BookPick.mvp.domain.book.dto.BookDtos.*; +import BookPick.mvp.domain.book.dto.search.BookSearchPageRes; +import BookPick.mvp.domain.book.dto.search.BookSearchReq; +import BookPick.mvp.domain.book.dto.search.BookSearchRes; import BookPick.mvp.global.dto.PageInfo; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; @@ -12,7 +14,6 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 0b8563f..a6d1bfb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.curation.dto.base.get.list; +import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; @@ -80,7 +81,7 @@ static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceI //2. 유저 독서취향의 작가들 안에 존재하면 +20 - List favoriteAuthors = preferenceInfo.favoriteAuthors(); + List favoriteAuthors = preferenceInfo.favoriteAuthors(); if(favoriteAuthors.contains(author)){ similarity+=10; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java index eac03f4..7cb9df5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.curation.dto.prefer; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.book.entity.Book; import java.util.List; @@ -8,8 +9,8 @@ public record ReadingPreferenceInfo( Long userId, String mbti, - List favoriteAuthors, List favoriteBooks, + List favoriteAuthors, List readingHabits, List moods, List genres, @@ -22,8 +23,8 @@ public static ReadingPreferenceInfo from(ReadingPreference preference) { return new ReadingPreferenceInfo( preference.getUser().getId(), preference.getMbti(), - preference.getFavoriteAuthors(), preference.getFavoriteBooks(), + preference.getFavoriteAuthors(), preference.getMoods(), preference.getReadingHabits(), preference.getGenres(), From 82c974549f9cb094d31905dfd6ada2da0b9bed19 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:27:33 +0900 Subject: [PATCH 099/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=EC=97=90=20=EC=9E=91=EA=B1=B0=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=9E=91?= =?UTF-8?q?=EA=B0=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5&=EC=9E=91=EA=B0=80=20=EC=A1=B0=EC=9D=B8=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/ReadingPreference.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index b383417..23a736f 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.ReadingPreference.entity; -import BookPick.mvp.domain.ReadingPreference.dto.Update.ReadingPreferenceUpdateReq; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; +import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; @@ -26,12 +27,6 @@ public class ReadingPreference { private String mbti; - @ElementCollection - @CollectionTable(name = "preference_authors", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "authors") - private List favoriteAuthors; - - @ManyToMany @JoinTable( name = "preference_favorite_books", @@ -40,6 +35,15 @@ public class ReadingPreference { ) private List favoriteBooks; + @ManyToMany + @JoinTable( + name = "preference_favorite_authors", + joinColumns = @JoinColumn(name = "preference_id"), + inverseJoinColumns = @JoinColumn(name = "author_id") + ) + private List favoriteAuthors; + + @ElementCollection @CollectionTable(name = "preference_moods", joinColumns = @JoinColumn(name = "preference_id")) @Column(name = "mood") @@ -65,10 +69,13 @@ public class ReadingPreference { @Column(name = "trend") private List trends; - public void update(ReadingPreferenceUpdateReq req) { + public void update(ReadingPreferenceReq req) { if (req.mbti() != null) this.mbti = req.mbti(); + if (req.favoriteBooks() != null) this.favoriteBooks = req.favoriteBooks; + + +(); if (req.favoriteAuthors() != null) this.favoriteAuthors = req.favoriteAuthors(); - if (req.favoriteBooks() != null) this.favoriteBooks = req.favoriteBooks(); if (req.moods() != null) this.moods = req.moods(); if (req.readingHabits() != null) this.readingHabits = req.readingHabits(); if (req.genres() != null) this.genres = req.genres(); @@ -76,6 +83,6 @@ public void update(ReadingPreferenceUpdateReq req) { if (req.trends() != null) this.trends = req.trends(); } - } +} From d4e4e4e3346be2df3a1533f2649bb0f97157ba6d Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:35:30 +0900 Subject: [PATCH 100/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EC=B6=94=EA=B0=80=EC=8B=9C=20=EC=B1=85=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=9A=94=EC=B2=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/book/dto/preference/BookDto.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java diff --git a/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java new file mode 100644 index 0000000..09a7928 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.book.dto.preference; + +import BookPick.mvp.domain.book.entity.Book; + +import java.util.List; +import java.util.Set; + +public record BookDto( + String title, + Set authors, + String image, + String isbn +) { + public BookDto{ + if(authors==null){ + authors=Set.of(); + } + } +} From 8d21d53ab1f165317f2717241f6fbffefab2698a Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:45:55 +0900 Subject: [PATCH 101/291] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/curation/enums/CurationErrorCode.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java index 24dd1eb..59a80c0 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java @@ -1,12 +1,13 @@ package BookPick.mvp.domain.curation.enums; +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; @AllArgsConstructor @Getter -public enum CurationErrorCode { +public enum CurationErrorCode implements ErrorCodeInterface { CURATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), CURATION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "큐레이션 접근 권한이 없습니다."); From 0d40b17dc6581732623ae8c9216838c18eb67a7d Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 22:48:18 +0900 Subject: [PATCH 102/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EC=88=98=EC=A0=95=EC=8B=9C,=20DB=20=EC=B1=85=20?= =?UTF-8?q?=EB=AA=BB=EC=B0=BE=EC=9D=8C=20=EC=98=88=EC=99=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../author/exceptions/BookNotFoundException.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/author/exceptions/BookNotFoundException.java diff --git a/src/main/java/BookPick/mvp/domain/author/exceptions/BookNotFoundException.java b/src/main/java/BookPick/mvp/domain/author/exceptions/BookNotFoundException.java new file mode 100644 index 0000000..05a7cbb --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/author/exceptions/BookNotFoundException.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.author.exceptions; + +import BookPick.mvp.domain.author.enums.AuthroErrorCode; +import BookPick.mvp.domain.book.dto.preference.BookDto; +import BookPick.mvp.global.exception.BusinessException; + +public class BookNotFoundException extends BusinessException { + public BookNotFoundException(){ + super(AuthroErrorCode.BOOK_NOT_FOUND); + } +} From 6f838c63fc362bada5a55f1b6e54c3d9d7f8a286 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 23:42:38 +0900 Subject: [PATCH 103/291] =?UTF-8?q?feat=20:=20=EA=B0=81=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EC=9E=91=EA=B0=80=20=EC=A0=80=EC=9E=A5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=98=A4=EB=B2=84?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../author/service/AuthorSaveService.java | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java index 593d8c6..d751fe8 100644 --- a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java +++ b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.author.service; +import BookPick.mvp.domain.author.dto.preference.AuthorDto; import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.author.repository.AuthorRepository; import lombok.RequiredArgsConstructor; @@ -7,21 +8,49 @@ import java.util.List; import java.util.Optional; +import java.util.Set; + @Service @RequiredArgsConstructor public class AuthorSaveService { private final AuthorRepository authorRepository; - // 1.독서취향에서 작가 꺼내서, 작가가 db에 등록되어있지 않으면 저장 - public void saveIfNotExists(List authors) { - List preferredAuthors = authors; - for (Author preferredAuthor : preferredAuthors) { - Optional existingAuthor = authorRepository.findByName(preferredAuthor.getName()); - if (existingAuthor.isEmpty()) { - authorRepository.save(preferredAuthor); - } + // 1. Author 리스트 + public void saveAuthorIfNotExists(Set authors) { + for (Author author : authors) { + saveAuthorIfNotExists(author); // 단건 메서드 재사용 + } + } + + // 2. Author 단건 + public void saveAuthorIfNotExists(Author author) { + authorRepository.findByName(author.getName()) + .orElseGet(() -> authorRepository.save(author)); + } + // 3. String 리스트 + public void saveAuthorIfNotExistsByName(Set authorNames) { + for (String name : authorNames) { + saveAuthorIfNotExistsByName(name); // 단건 메서드 재사용 } } + // 4. String 단건 + public void saveAuthorIfNotExistsByName(String name) { + authorRepository.findByName(name) + .orElseGet(() -> authorRepository.save(new Author(null, name, 0, null, null, null))); + } + + // 5.AuthorDto 리스트 + public void saveAuthorIfNotExistsDto(Set authorDtos) { + for (AuthorDto dto : authorDtos) { + saveAuthorIfNotExistsDto(dto); + } + } + + // 6.AuthorDto 단건 + public void saveAuthorIfNotExistsDto(AuthorDto dto) { + authorRepository.findByName(dto.name()) + .orElseGet(() -> authorRepository.save(new Author(null, dto.name(), 0, null, null, null))); + } } From 3e846f159ff37f78f23c70a68601aa7d44549407 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 23:42:46 +0900 Subject: [PATCH 104/291] =?UTF-8?q?feat=20:=20=EA=B0=81=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=A1=9C=20=EB=B3=84=EB=A1=9C=20?= =?UTF-8?q?=EC=B1=85=20=EC=A0=80=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=98=A4=EB=B2=84=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/book/service/BookSaveService.java | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java index 89e1f74..49c577e 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java @@ -2,6 +2,8 @@ import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.author.repository.AuthorRepository; +import BookPick.mvp.domain.author.service.AuthorSaveService; +import BookPick.mvp.domain.book.dto.preference.BookDto; import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.book.repository.BookRepository; import lombok.RequiredArgsConstructor; @@ -9,23 +11,49 @@ import java.util.List; import java.util.Optional; +import java.util.Set; @Service @RequiredArgsConstructor public class BookSaveService { + private final BookRepository bookRepository; + private final AuthorSaveService authorSaveService; - // 1. 독서취향에서 책 꺼내서, 첵 db에 책 등록되어있지 않으면 저장 - public void saveIfNotExists(List books) { - List preferredBooks = books; - for (Book preferedBook : preferredBooks) { - Optional existingBook = bookRepository.findByTitle(preferedBook.getTitle()); - if (existingBook.isEmpty()) { - bookRepository.save(preferedBook); - } + // 1. Book 리스트 + public void saveBookIfNotExists(List books) { + for (Book book : books) { + saveBookIfNotExists(book); // 단건 재사용 } + } + // 2. Book 단건 + public void saveBookIfNotExists(Book book) { + bookRepository.findByTitle(book.getTitle()) + .orElseGet(() -> { + authorSaveService.saveAuthorIfNotExists(book.getAuthors()); // 작가 먼저 저장 + return bookRepository.save(book); + }); } + // 3. BookDto 리스트 + public void saveBookIfNotExistsDto(List bookDtos) { + for (BookDto dto : bookDtos) { + saveBookIfNotExistsDto(dto); + } + } + // 4. BookDto 단건 + public void saveBookIfNotExistsDto(BookDto dto) { + bookRepository.findByTitle(dto.title()) + .orElseGet(() -> { + // authors 저장 + authorSaveService.saveAuthorIfNotExistsByName(dto.authors()); + // Book 객체 변환 후 저장 + Book book = Book.from(dto); + return bookRepository.save(book); + }); + } } + + From 8985e375ef4850c982440c97a77a7a49e414b38d Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 23:43:15 +0900 Subject: [PATCH 105/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EC=88=98=EC=A0=95=20=EC=A6=89=EA=B0=81=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=8F=85=EC=84=9C?= =?UTF-8?q?=EC=B7=A8=ED=96=A5=20=EB=8D=94=ED=8B=B0=20=EC=B2=B4=ED=82=B9=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/ReadingPreference.java | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 23a736f..bc3f312 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -2,11 +2,15 @@ import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.author.service.AuthorSaveService; +import BookPick.mvp.domain.book.dto.preference.BookDto; import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.service.BookSaveService; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; import java.util.List; @Entity @@ -69,20 +73,69 @@ public class ReadingPreference { @Column(name = "trend") private List trends; - public void update(ReadingPreferenceReq req) { - if (req.mbti() != null) this.mbti = req.mbti(); - if (req.favoriteBooks() != null) this.favoriteBooks = req.favoriteBooks; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; -(); - if (req.favoriteAuthors() != null) this.favoriteAuthors = req.favoriteAuthors(); - if (req.moods() != null) this.moods = req.moods(); - if (req.readingHabits() != null) this.readingHabits = req.readingHabits(); - if (req.genres() != null) this.genres = req.genres(); - if (req.keywords() != null) this.keywords = req.keywords(); - if (req.trends() != null) this.trends = req.trends(); + private LocalDateTime deletedAt; + + public void update(ReadingPreferenceReq req, AuthorSaveService authorSaveService, BookSaveService bookSaveService) { + if (req.mbti() != null) this.mbti = req.mbti(); + + // favoriteBooks 처리 + if (req.favoriteBooks() != null) { + this.favoriteBooks.clear(); + List books = req.favoriteBooks().stream() + .map(dto -> { + Book book = Book.from(dto); + bookSaveService.saveBookIfNotExists(book); // DB에 없으면 저장 + return book; + }) + .toList(); + this.favoriteBooks.addAll(books); } + // favoriteAuthors 처리 + if (req.favoriteAuthors() != null) { + this.favoriteAuthors.clear(); + List authors = req.favoriteAuthors().stream() + .map(dto -> { + Author author = Author.from(dto); + authorSaveService.saveAuthorIfNotExists(author); // DB에 없으면 저장 + return author; + }) + .toList(); + this.favoriteAuthors.addAll(authors); + } + + if (req.moods() != null) { + this.moods.clear(); + this.moods.addAll(req.moods()); + } + + if (req.readingHabits() != null) { + this.readingHabits.clear(); + this.readingHabits.addAll(req.readingHabits()); + } + + if (req.genres() != null) { + this.genres.clear(); + this.genres.addAll(req.genres()); + } + + if (req.keywords() != null) { + this.keywords.clear(); + this.keywords.addAll(req.keywords()); + } + + if (req.trends() != null) { + this.trends.clear(); + this.trends.addAll(req.trends()); + } +} + + + } From 78e3ab00a78c115540a8aad5232dbe82b785e5d9 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 23:43:54 +0900 Subject: [PATCH 106/291] =?UTF-8?q?feat=20:=20new=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EC=A7=80=EC=96=91,=20=EB=8F=84=EC=84=9C=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=8E=99=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/domain/book/entity/Book.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java index 6553c1b..8ac55ff 100644 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.book.entity; import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.book.dto.preference.BookDto; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -8,6 +9,7 @@ import lombok.Setter; import java.util.HashSet; +import java.util.List; import java.util.Set; @Entity @@ -39,4 +41,16 @@ public class Book { public Book() { } + + public static Book from(BookDto bookDto) { + + List authors + + return Book.builder() + .title(bookDto.title()) + .authors(new HashSet<>(bookDto.authors())) + .image(bookDto.image()) + .isbn(bookDto.isbn()) + .build(); + } } From 9f591bb74cf09411df911f17d2f1c59469cd3d3e Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 23:44:02 +0900 Subject: [PATCH 107/291] =?UTF-8?q?feat=20:=20new=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EC=A7=80=EC=96=91,=20=EC=9E=91=EA=B0=80=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=ED=8E=99=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/author/entity/Author.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/author/entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java index 195132f..2ef0d4c 100644 --- a/src/main/java/BookPick/mvp/domain/author/entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/entity/Author.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.author.entity; +import BookPick.mvp.domain.author.dto.preference.AuthorDto; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -36,4 +37,15 @@ public class Author { public Author() { } + + public static Author from(AuthorDto dto) { + return Author.builder() + .name(dto.name()) + .curated_count(0) // 초기값 설정 + .createdAt(null) + .updatedAt(null) + .deletedAt(null) + .build(); + } + } From beea63193932153db1b2c1f1a8c0902da5502030 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 11 Nov 2025 23:44:31 +0900 Subject: [PATCH 108/291] =?UTF-8?q?feat=20:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B4=80=EC=8B=AC=EC=82=AC=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EB=B3=84=20=EB=B6=84=EB=A6=AC=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=B1=85=20=EC=B0=BE=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=20=EC=97=86=EC=9D=84=20=EB=95=8C=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/author/enums/AuthroErrorCode.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java diff --git a/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java b/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java new file mode 100644 index 0000000..b68e2ee --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.author.enums; + +import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum AuthroErrorCode implements ErrorCodeInterface { + + + BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 책을 찾을 수 없습니다."); + + + + private final HttpStatus status; + private final String message; +} \ No newline at end of file From 417ff51b8bbb39fd70b9485b5879d8f7bc597620 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 00:13:09 +0900 Subject: [PATCH 109/291] =?UTF-8?q?chore=20:=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EB=B6=84=EB=A5=98=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=20=EC=B1=85=20=EA=B2=80=EC=83=89=20api=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/{service => util/kakaoApi}/BookSearchService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/BookPick/mvp/domain/book/{service => util/kakaoApi}/BookSearchService.java (98%) diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java similarity index 98% rename from src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java rename to src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java index 27cebaa..4edc2d8 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSearchService.java +++ b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.book.service; +package BookPick.mvp.domain.book.util.kakaoApi; import BookPick.mvp.domain.book.dto.search.BookSearchPageRes; import BookPick.mvp.domain.book.dto.search.BookSearchReq; From f28bb407f25f77fae32af56f68cca2aaeb05e9f4 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 00:42:33 +0900 Subject: [PATCH 110/291] =?UTF-8?q?feat=20:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EB=A5=BC=20=EC=9C=84=ED=95=9C,=20=EB=8F=85?= =?UTF-8?q?=EC=84=9C=EC=B7=A8=ED=96=A5=EC=9D=98=20=EC=9E=91=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B1=85=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A7=91?= =?UTF-8?q?=ED=95=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ReadingPreference/entity/ReadingPreference.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index bc3f312..88a508d 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -12,6 +12,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Set; @Entity @Table(name = "user_reading_preference") @@ -37,7 +38,7 @@ public class ReadingPreference { joinColumns = @JoinColumn(name = "preference_id"), inverseJoinColumns = @JoinColumn(name = "book_id") ) - private List favoriteBooks; + private Set favoriteBooks; @ManyToMany @JoinTable( @@ -45,7 +46,7 @@ public class ReadingPreference { joinColumns = @JoinColumn(name = "preference_id"), inverseJoinColumns = @JoinColumn(name = "author_id") ) - private List favoriteAuthors; + private Set favoriteAuthors; @ElementCollection From 602f1cfddf98bb453ba2a0d1983d09da739db031 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 01:02:36 +0900 Subject: [PATCH 111/291] chore: meaningless commit --- .../entity/ReadingPreference.java | 6 ++--- .../mvp/domain/author/entity/Author.java | 3 +++ .../book/Controller/BookSearchController.java | 2 +- .../domain/book/service/BookSaveService.java | 22 ++++++++++++------- .../dto/base/get/list/CurationContentRes.java | 3 ++- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 88a508d..8a873a6 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -15,7 +15,7 @@ import java.util.Set; @Entity -@Table(name = "user_reading_preference") +@Table(name = "reading_preference") @Getter @Builder @NoArgsConstructor @@ -34,7 +34,7 @@ public class ReadingPreference { @ManyToMany @JoinTable( - name = "preference_favorite_books", + name = "preference_books", joinColumns = @JoinColumn(name = "preference_id"), inverseJoinColumns = @JoinColumn(name = "book_id") ) @@ -42,7 +42,7 @@ public class ReadingPreference { @ManyToMany @JoinTable( - name = "preference_favorite_authors", + name = "preference_authors", joinColumns = @JoinColumn(name = "preference_id"), inverseJoinColumns = @JoinColumn(name = "author_id") ) diff --git a/src/main/java/BookPick/mvp/domain/author/entity/Author.java b/src/main/java/BookPick/mvp/domain/author/entity/Author.java index 2ef0d4c..40ca320 100644 --- a/src/main/java/BookPick/mvp/domain/author/entity/Author.java +++ b/src/main/java/BookPick/mvp/domain/author/entity/Author.java @@ -12,6 +12,8 @@ import org.springframework.context.annotation.Bean; import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; @Entity @Builder @@ -48,4 +50,5 @@ public static Author from(AuthorDto dto) { .build(); } + } diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index b0e43c3..225cb61 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.book.dto.search.BookSearchPageRes; import BookPick.mvp.domain.book.dto.search.BookSearchReq; -import BookPick.mvp.domain.book.service.BookSearchService; +import BookPick.mvp.domain.book.util.kakaoApi.BookSearchService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java index 49c577e..96e8550 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -21,7 +22,8 @@ public class BookSaveService { private final AuthorSaveService authorSaveService; // 1. Book 리스트 - public void saveBookIfNotExists(List books) { + public void saveBookIfNotExists(Set books) { + for (Book book : books) { saveBookIfNotExists(book); // 단건 재사용 } @@ -37,20 +39,24 @@ public void saveBookIfNotExists(Book book) { } // 3. BookDto 리스트 - public void saveBookIfNotExistsDto(List bookDtos) { + public Set saveBookIfNotExistsDto(Set bookDtos) { + Set books= new HashSet<>(); for (BookDto dto : bookDtos) { - saveBookIfNotExistsDto(dto); + books.add(saveBookIfNotExistsDto(dto)); } + + return books; } // 4. BookDto 단건 - public void saveBookIfNotExistsDto(BookDto dto) { - bookRepository.findByTitle(dto.title()) - .orElseGet(() -> { + public Book saveBookIfNotExistsDto(BookDto dto) { + + return bookRepository.findByTitle(dto.title()) + .orElseGet(() -> { // 책이 없으면 // authors 저장 - authorSaveService.saveAuthorIfNotExistsByName(dto.authors()); + Set authors = authorSaveService.saveAuthorsIfNotExistsByName(dto.authors()); // Book 객체 변환 후 저장 - Book book = Book.from(dto); + Book book = Book.from(dto, authors); return bookRepository.save(book); }); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index a6d1bfb..7ed0848 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Random; +import java.util.Set; public record CurationContentRes( Long curationId, @@ -81,7 +82,7 @@ static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceI //2. 유저 독서취향의 작가들 안에 존재하면 +20 - List favoriteAuthors = preferenceInfo.favoriteAuthors(); + Set favoriteAuthors = preferenceInfo.favoriteAuthors(); if(favoriteAuthors.contains(author)){ similarity+=10; From f13c452d5430d3362364c8b918a987b6373d808e Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 01:03:18 +0900 Subject: [PATCH 112/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9E=91=EA=B0=80=20=EC=A4=91=EB=B3=B5=20=ED=97=88=EC=9A=A9=20?= =?UTF-8?q?=EB=B6=88=EA=B0=80=EB=A1=9C=20=EC=9D=B8=ED=95=9C,=20=EC=9E=90?= =?UTF-8?q?=EB=A3=8C=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ReadingPreference/dto/ReadingPreferenceReq.java | 5 +++-- .../domain/ReadingPreference/dto/ReadingPreferenceRes.java | 5 +++-- .../domain/curation/dto/prefer/ReadingPreferenceInfo.java | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java index 7d8f23a..82415c2 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java @@ -6,11 +6,12 @@ import BookPick.mvp.domain.book.entity.Book; import java.util.List; +import java.util.Set; public record ReadingPreferenceReq( String mbti, - List favoriteBooks, // 좋아하는 책 - List favoriteAuthors, + Set favoriteBooks, // 좋아하는 책 + Set favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java index 561e0f4..10af9b8 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java @@ -6,12 +6,13 @@ import BookPick.mvp.domain.book.entity.Book; import java.util.List; +import java.util.Set; public record ReadingPreferenceRes( Long preferenceId, String mbti, - List favoriteBooks, // 좋아하는 책 - List favoriteAuthors, + Set favoriteBooks, // 좋아하는 책 + Set favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java index 7cb9df5..a0c97d8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -5,12 +5,13 @@ import BookPick.mvp.domain.book.entity.Book; import java.util.List; +import java.util.Set; public record ReadingPreferenceInfo( Long userId, String mbti, - List favoriteBooks, - List favoriteAuthors, + Set favoriteBooks, + Set favoriteAuthors, List readingHabits, List moods, List genres, From 6847e88c9447c87f780b31c9cece0f31d49a89ac Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 01:03:56 +0900 Subject: [PATCH 113/291] =?UTF-8?q?feat=20:=20=EC=B1=85=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A9=94=EC=84=9C=EB=93=9C=20Book=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20=EB=B0=98=ED=99=98=20=EB=A6=AC=ED=84=B4=20?= =?UTF-8?q?=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReadingPreferenceService.java | 36 +++++++++++++++---- .../author/service/AuthorSaveService.java | 27 +++++++++----- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index a72e9cc..770d7e7 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -7,7 +7,11 @@ import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceRes; import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.author.service.AuthorSaveService; +import BookPick.mvp.domain.book.dto.preference.BookDto; +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.repository.BookRepository; import BookPick.mvp.domain.book.service.BookSaveService; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; @@ -17,6 +21,8 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor @@ -25,8 +31,7 @@ public class ReadingPreferenceService { private final UserRepository userRepository; private final BookSaveService bookSaveService; private final AuthorSaveService authorSaveService; - - + private final BookRepository bookRepository; // -- 유저 독서 취향 등록 -- @@ -44,14 +49,21 @@ public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceR throw new AlreadyRegisteredReadingPreferenceException(); } - bookSaveService.saveIfNotExists(req.favoriteBooks()); - authorSaveService.saveIfNotExists(req.favoriteAuthors()); + // 3. 독서취향 기반 책 저장 (중복 체크) + Set savedBooks = bookSaveService.saveBookIfNotExistsDto(req.favoriteBooks()); + + + // 4. 독서취향 기반 작가 저장 (중복 체크) + Set savedAuthors = authorSaveService.saveAuthorIfNotExistsDto(req.favoriteAuthors()); + + // 5. 책 찾고 ReadingPreference readingPreference = ReadingPreference.builder() .user(user) .mbti(req.mbti()) - .favoriteBooks(req.favoriteBooks()) + .favoriteBooks(savedBooks) + .favoriteAuthors(savedAuthors) .moods(req.moods()) .readingHabits(req.readingHabits()) .genres(req.genres()) @@ -87,7 +99,19 @@ public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferen ReadingPreference preference = readingPreferenceRepository.findByUserId(userId) .orElseThrow(UserReadingPreferenceNotExisted::new); - preference.update(req); + +// // 1. 책 저장 +// List bookDtos = req.favoriteBooks(); +// for (BookDto bookDto : bookDtos){ +// +// +// Book book = bookRepository.findBy +// } + + // 2. 작가 저장 + + // Todo 2. 구현 필요, Service 파라미터로 주면 안돼요! +// preference.update(req, authorSaveService, bookSaveService); return ReadingPreferenceRes.from(preference); } diff --git a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java index d751fe8..1051553 100644 --- a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java +++ b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java @@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.time.LocalDateTime; +import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; @@ -29,28 +31,35 @@ public void saveAuthorIfNotExists(Author author) { .orElseGet(() -> authorRepository.save(author)); } // 3. String 리스트 - public void saveAuthorIfNotExistsByName(Set authorNames) { + public Set saveAuthorsIfNotExistsByName(Set authorNames) { + Set authors= new HashSet<>(); + for (String name : authorNames) { - saveAuthorIfNotExistsByName(name); // 단건 메서드 재사용 + authors.add( saveAuthorIfNotExistsByName(name)); // 단건 메서드 재사용 } + + return authors; } // 4. String 단건 - public void saveAuthorIfNotExistsByName(String name) { - authorRepository.findByName(name) - .orElseGet(() -> authorRepository.save(new Author(null, name, 0, null, null, null))); + public Author saveAuthorIfNotExistsByName(String name) { + return authorRepository.findByName(name) + .orElseGet(() -> authorRepository.save(new Author(null, name, 0, LocalDateTime.now(), null, null))); } // 5.AuthorDto 리스트 - public void saveAuthorIfNotExistsDto(Set authorDtos) { + public Set saveAuthorIfNotExistsDto(Set authorDtos) { + Set authors = new HashSet<>(); for (AuthorDto dto : authorDtos) { - saveAuthorIfNotExistsDto(dto); + authors.add(saveAuthorIfNotExistsDto(dto)); } + + return authors; } // 6.AuthorDto 단건 - public void saveAuthorIfNotExistsDto(AuthorDto dto) { - authorRepository.findByName(dto.name()) + public Author saveAuthorIfNotExistsDto(AuthorDto dto) { + return authorRepository.findByName(dto.name()) .orElseGet(() -> authorRepository.save(new Author(null, dto.name(), 0, null, null, null))); } } From 4d719a6514f235acb7a797e12098bc314f3dd728 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 01:04:09 +0900 Subject: [PATCH 114/291] chore: meaningless commit --- .../BookPick/mvp/domain/book/entity/Book.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java index 8ac55ff..4a576a8 100644 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -27,7 +27,7 @@ public class Book { @ManyToMany @JoinTable( - name = "book_id", + name = "book_author", joinColumns = @JoinColumn(name = "book_id"), inverseJoinColumns = @JoinColumn(name = "author_id") ) @@ -44,11 +44,21 @@ public Book() { public static Book from(BookDto bookDto) { - List authors return Book.builder() .title(bookDto.title()) - .authors(new HashSet<>(bookDto.authors())) + .authors(null) + .image(bookDto.image()) + .isbn(bookDto.isbn()) + .build(); + } + + public static Book from(BookDto bookDto, Set authors) { + + + return Book.builder() + .title(bookDto.title()) + .authors(authors) .image(bookDto.image()) .isbn(bookDto.isbn()) .build(); From 2d3193903a755080fb0103b6b2bfa6eccd5e5f41 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 20:33:03 +0900 Subject: [PATCH 115/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=20?= =?UTF-8?q?=EC=B7=A8=ED=96=A5=20=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Create/ReadingPreferenceCreateReq.java | 18 ---------- .../Create/ReadingPreferenceCreateRes.java | 17 ---------- .../dto/ETC/Get/ReadingPreferenceGetReq.java | 7 ---- .../dto/ETC/Get/ReadingPreferenceGetRes.java | 33 ------------------- .../Update/ReadingPreferenceUpdateReq.java | 16 --------- .../Update/ReadingPreferenceUpdateRes.java | 29 ---------------- .../dto/ReadingPreferenceReq.java | 2 +- .../dto/ReadingPreferenceRes.java | 4 +-- .../entity/ReadingPreference.java | 16 ++++----- .../service/ReadingPreferenceService.java | 2 +- .../dto/prefer/ReadingPreferenceInfo.java | 4 +-- .../list/CurationRecommendationService.java | 2 +- 12 files changed, 15 insertions(+), 135 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java deleted file mode 100644 index 903fb9e..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateReq.java +++ /dev/null @@ -1,18 +0,0 @@ -//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Create; -// -//import BookPick.mvp.domain.author.entity.Author; -//import BookPick.mvp.domain.book.entity.Book; -// -//import java.util.List; -// -//public record ReadingPreferenceReq( -// String mbti, -// List favoriteBooks, // 좋아하는 책 -// List favoriteAuthors, -// List moods, // 독서 선호 분위기 -// List readingHabits, // 독서 습관 -// List genres, // 선호 장르 -// List keywords, // 키워드 -// List trends // -//) { -//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java deleted file mode 100644 index cb8b9e9..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Create/ReadingPreferenceCreateRes.java +++ /dev/null @@ -1,17 +0,0 @@ -//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Create; -// -// -//import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; -// -//public record ReadingPreferenceRes( -// Long readingPreferenceId -// ) { -// static public ReadingPreferenceRes from(ReadingPreference readingPreference) { -// return new ReadingPreferenceRes(readingPreference.getId()); -// } -// } -// -// -// -// -// diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java deleted file mode 100644 index 666adad..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetReq.java +++ /dev/null @@ -1,7 +0,0 @@ -//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Get; -// -//// -- 독서 취향 조회 요청 -- -//public record ReadingPreferenceGetReq () -//{ -// -//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java deleted file mode 100644 index f5e771d..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Get/ReadingPreferenceGetRes.java +++ /dev/null @@ -1,33 +0,0 @@ -//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Get; -// -//import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; -// -//import java.util.List; -// -//// -- 독서 취향 조회 응답 -- -//public record ReadingPreferenceRes( -// Long preferenceId, -// String mbti, -// List favoriteBooks, // 좋아하는 책 -// List moods, // 독서 선호 분위기 -// List readingHabits, // 독서 습관 -// List genres, // 선호 장르 -// List keywords, // 키워드 -// List trends -//){ -// static public ReadingPreferenceRes from(ReadingPreference rp){ -// return new ReadingPreferenceRes( -// rp.getId(), -// rp.getMbti(), -// rp.getFavoriteBooks(), -// rp.getMoods(), -// rp.getReadingHabits(), -// rp.getGenres(), -// rp.getKeywords(), -// rp.getTrends() -// ); -// } -//} -// -// -// diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java deleted file mode 100644 index bf6baf2..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateReq.java +++ /dev/null @@ -1,16 +0,0 @@ -//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Update; -// -//import java.util.List; -// -//public record ReadingPreferenceReq( -// Long preferenceId, -// String mbti, -// List favoriteAuthors, // 좋아하는 책 -// List favoriteBooks, // 좋아하는 책 -// List moods, // 독서 선호 분위기 -// List readingHabits, // 독서 습관 -// List genres, // 선호 장르 -// List keywords, // 키워드 -// List trends -//) { -//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java deleted file mode 100644 index 3964522..0000000 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ETC/Update/ReadingPreferenceUpdateRes.java +++ /dev/null @@ -1,29 +0,0 @@ -//package BookPick.mvp.domain.ReadingPreference.dto.ETC.Update; -// -//import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; -// -//import java.util.List; -// -//public record ReadingPreferenceRes( -// Long preferenceId, -// String mbti, -// List favoriteBooks, // 좋아하는 책 -// List moods, // 독서 선호 분위기 -// List readingHabits, // 독서 습관 -// List genres, // 선호 장르 -// List keywords, // 키워드 -// List trends -//) { -// static public ReadingPreferenceRes from(ReadingPreference rp){ -// return new ReadingPreferenceRes( -// rp.getId(), -// rp.getMbti(), -// rp.getFavoriteBooks(), -// rp.getMoods(), -// rp.getReadingHabits(), -// rp.getGenres(), -// rp.getKeywords(), -// rp.getTrends() -// ); -// } -//} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java index 82415c2..bd88b02 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceReq.java @@ -16,6 +16,6 @@ public record ReadingPreferenceReq( List readingHabits, // 독서 습관 List genres, // 선호 장르 List keywords, // 키워드 - List trends // + List readingStyles // ) { } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java index 10af9b8..25377de 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java @@ -17,7 +17,7 @@ public record ReadingPreferenceRes( List readingHabits, // 독서 습관 List genres, // 선호 장르 List keywords, // 키워드 - List trends // + List readingStyles // ) { static public ReadingPreferenceRes from(ReadingPreference rp) { @@ -30,7 +30,7 @@ static public ReadingPreferenceRes from(ReadingPreference rp) { rp.getReadingHabits(), rp.getGenres(), rp.getKeywords(), - rp.getTrends() + rp.getReadingStyles() ); } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 8a873a6..2de11d3 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -55,8 +55,8 @@ public class ReadingPreference { private List moods; @ElementCollection - @CollectionTable(name = "preference_styles", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "habit") + @CollectionTable(name = "preference_readinghabits", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "reading_habits") private List readingHabits; @ElementCollection @@ -70,9 +70,9 @@ public class ReadingPreference { private List keywords; @ElementCollection - @CollectionTable(name = "preference_trends", joinColumns = @JoinColumn(name = "preference_id")) - @Column(name = "trend") - private List trends; + @CollectionTable(name = "preference_readingstyles", joinColumns = @JoinColumn(name = "preference_id")) + @Column(name = "reading_style") + private List readingStyles; private LocalDateTime createdAt; @@ -129,9 +129,9 @@ public void update(ReadingPreferenceReq req, AuthorSaveService authorSaveService this.keywords.addAll(req.keywords()); } - if (req.trends() != null) { - this.trends.clear(); - this.trends.addAll(req.trends()); + if (req.readingStyles() != null) { + this.readingStyles.clear(); + this.readingStyles.addAll(req.readingStyles()); } } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 770d7e7..20cb350 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -67,7 +67,7 @@ public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceR .moods(req.moods()) .readingHabits(req.readingHabits()) .genres(req.genres()) - .trends(req.trends()) + .readingStyles(req.readingStyles()) .keywords(req.keywords()) .build(); diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java index a0c97d8..2cadb04 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/prefer/ReadingPreferenceInfo.java @@ -16,7 +16,7 @@ public record ReadingPreferenceInfo( List moods, List genres, List keywords, - List trends + List readingStyles ) { // 엔티티 → DTO 변환 @@ -30,7 +30,7 @@ public static ReadingPreferenceInfo from(ReadingPreference preference) { preference.getReadingHabits(), preference.getGenres(), preference.getKeywords(), - preference.getTrends() + preference.getReadingStyles() ); } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java index 1153a39..f0cbc8c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java @@ -28,7 +28,7 @@ public List recommend(ReadingPreferenceInfo preferenceInfo) .readingMethod(String.join(", ", preferenceInfo.readingHabits())) .genre(String.join(", ", preferenceInfo.genres())) .keyword(String.join(", ", preferenceInfo.keywords())) - .readingStyle(String.join(", ", preferenceInfo.trends())) + .readingStyle(String.join(", ", preferenceInfo.readingStyles())) .build() ); } From 469a4bbdde08f761808b24b39890789ded494a8b Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 20:49:30 +0900 Subject: [PATCH 116/291] =?UTF-8?q?feat=20:=20=ED=8F=B4=EB=8D=94=EB=AA=85?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20model=20->=20entity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/book/dto/search/BookSearchRes.java | 3 ++- .../mvp/domain/book/util/kakaoApi/BookSearchService.java | 3 ++- src/main/java/BookPick/mvp/domain/comment/entity/Comment.java | 2 +- .../BookPick/mvp/domain/comment/service/CommentService.java | 2 +- .../domain/curation/dto/base/create/CurationCreateRes.java | 2 +- .../domain/curation/dto/base/get/list/CurationContentRes.java | 4 +--- .../mvp/domain/curation/dto/base/get/one/CurationGetRes.java | 2 +- .../domain/curation/dto/base/update/CurationUpdateRes.java | 2 +- .../mvp/domain/curation/{model => entity}/Curation.java | 2 +- .../mvp/domain/curation/repository/CurationRepository.java | 2 +- .../mvp/domain/curation/service/base/CurationService.java | 2 +- .../mvp/domain/curation/service/list/CurationListService.java | 2 +- .../curation/service/list/CurationRecommendationService.java | 1 - .../domain/curation/util/gemini/dto/CurationMatchResult.java | 2 +- .../domain/curation/util/gemini/service/GeminiService.java | 2 +- .../curation/util/list/Handler/CurationPageHandler.java | 2 +- .../domain/curation/util/list/fetcher/CurationFetcher.java | 2 +- 17 files changed, 18 insertions(+), 19 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/{model => entity}/Curation.java (98%) diff --git a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java index 41a0476..6afb904 100644 --- a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java +++ b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java @@ -3,6 +3,7 @@ public record BookSearchRes( String title, String author, - String image + String image, + String isbn ) { } diff --git a/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java index 4edc2d8..b46bef5 100644 --- a/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java +++ b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java @@ -62,8 +62,9 @@ public BookSearchPageRes getBookSearchList(BookSearchReq req) { List authors = (List) doc.get("authors"); String author = authors != null && !authors.isEmpty() ? authors.get(0) : "저자 미상"; String image = (String) doc.get("thumbnail"); + String isbn = (String) doc.get("isbn"); - books.add(new BookSearchRes(title, author, image)); + books.add(new BookSearchRes(title, author, image, isbn)); } // meta 데이터 추출 → PageInfo 변환 diff --git a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java index 7cfccab..d1cd19a 100644 --- a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java +++ b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.comment.entity; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import jakarta.validation.constraints.Size; diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 58b0bbf..d0ec634 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -11,7 +11,7 @@ import BookPick.mvp.domain.comment.exception.CommentNotFoundException; import BookPick.mvp.domain.comment.repository.CommentRepository; import BookPick.mvp.domain.curation.exception.CurationNotFoundException; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.UserNotFoundException; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java index a4c2f6d..8cc1da3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.dto.base.create; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; public record CurationCreateRes( Long id diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 7ed0848..e8d1d7d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -2,12 +2,10 @@ import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.user.entity.User; -import java.util.List; -import java.util.Random; import java.util.Set; public record CurationContentRes( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index 3a51bde..fadb82b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -1,7 +1,7 @@ // CurationGetRes.java package BookPick.mvp.domain.curation.dto.base.get.one; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import java.time.LocalDateTime; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java index a5ffb5b..56aa6cb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java @@ -1,7 +1,7 @@ // CurationUpdateRes.java package BookPick.mvp.domain.curation.dto.base.update; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; public record CurationUpdateRes( Long id diff --git a/src/main/java/BookPick/mvp/domain/curation/model/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java similarity index 98% rename from src/main/java/BookPick/mvp/domain/curation/model/Curation.java rename to src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 03a1431..2c8c62e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/model/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.model; +package BookPick.mvp.domain.curation.entity; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.user.entity.User; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index d786dbe..9d94962 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.repository; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index cfa884d..ab1d6f3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -7,7 +7,7 @@ import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index fb9bfc9..e84bf54 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -5,7 +5,7 @@ import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.enums.SortType; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java index f0cbc8c..b88168a 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java @@ -1,6 +1,5 @@ package BookPick.mvp.domain.curation.service.list; -import BookPick.mvp.domain.curation.model.Curation; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.gemini.prompt.ContentPromptTemplate; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java index 35d5c5e..8adffc6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.util.gemini.dto; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.user.entity.User; import lombok.Builder; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index e3eed82..7c88918 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.curation.util.gemini.service; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.util.gemini.client.GeminiClient; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index da32b0e..c44dfa5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -5,7 +5,7 @@ import BookPick.mvp.domain.curation.enums.SortType; import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index a65af1f..1d0b14c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.enums.SortType; -import BookPick.mvp.domain.curation.model.Curation; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.service.list.CurationRecommendationService; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; From 7e673cacf3c7255ad80aef077ee1b4e750ba4c0a Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 21:06:43 +0900 Subject: [PATCH 117/291] =?UTF-8?q?feat=20:=20=EC=A0=9C=EB=AA=A9=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=88=84=EB=9D=BD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/base/create/CurationCreateReq.java | 12 ++++--- .../dto/base/get/list/CurationContentRes.java | 4 +-- .../dto/base/get/one/CurationGetRes.java | 2 ++ .../dto/base/update/CurationUpdateReq.java | 1 + .../mvp/domain/curation/entity/Curation.java | 34 +++++++------------ 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java index 9ec262e..46594df 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java @@ -6,11 +6,13 @@ // 메인 요청 DTO public record CurationCreateReq( - ThumbnailDto thumbnail, - BookDto book, - String review, - RecommendDto recommend -) {} + String title, + ThumbnailDto thumbnail, + BookDto book, + String review, + RecommendDto recommend +) { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index e8d1d7d..155ec00 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -27,7 +27,7 @@ public record CurationContentRes( public static CurationContentRes from(Curation curation) { return new CurationContentRes( curation.getId(), - curation.getBookTitle(), + curation.getTitle(), curation.getUser().getId(), curation.getUser().getNickname(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), @@ -46,7 +46,7 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr Curation curation = matchResult.getCuration(); return new CurationContentRes( curation.getId(), - curation.getBookTitle(), + curation.getTitle(), curation.getUser().getId(), matchResult.getUser().getNickname(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index fadb82b..836f26b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -8,6 +8,7 @@ public record CurationGetRes( Long id, Long userId, + String title, ThumbnailInfo thumbnail, BookInfo book, String review, @@ -19,6 +20,7 @@ public static CurationGetRes from(Curation curation) { return new CurationGetRes( curation.getId(), curation.getUser().getId(), + curation.getTitle(), new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getReview(), diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java index 557e5fb..be16365 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java @@ -6,6 +6,7 @@ import BookPick.mvp.domain.curation.dto.base.create.Req.ThumbnailDto; public record CurationUpdateReq( + String title, ThumbnailDto thumbnail, BookDto book, String review, diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 2c8c62e..a80f177 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -3,10 +3,7 @@ import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -14,12 +11,14 @@ import java.time.LocalDateTime; import java.util.List; +@Builder @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) @EntityListeners(AuditingEntityListener.class) @Table(name = "curation") +@AllArgsConstructor public class Curation { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -29,9 +28,15 @@ public class Curation { @JoinColumn(name = "user_id", nullable = false) private User user; + private String title; + + + // Todo 2. 추후 썸네일 클래스로 변경 필요 private String thumbnailUrl; private String thumbnailColor; + + // Todo 1. 추후 Book 클래스로 변경 필요 @Column(nullable = false) private String bookTitle; private String bookAuthor; @@ -83,26 +88,13 @@ public class Curation { private LocalDateTime deletedAt; + public Curation() { - @Builder - public Curation(User user, String thumbnailUrl, String thumbnailColor, - String bookTitle, String bookAuthor, String bookIsbn, - String review, List moods, List genres, - List keywords, List styles) { - this.user = user; - this.thumbnailUrl = thumbnailUrl; - this.thumbnailColor = thumbnailColor; - this.bookTitle = bookTitle; - this.bookAuthor = bookAuthor; - this.bookIsbn = bookIsbn; - this.review = review; - this.moods = moods; - this.genres = genres; - this.keywords = keywords; - this.styles = styles; } + public void update(CurationUpdateReq req) { + this.title = req.title(); this.thumbnailUrl = req.thumbnail().imageUrl(); this.thumbnailColor = req.thumbnail().imageColor(); this.bookTitle = req.book().title(); From 55c8e8e7ed0c5aa0df22db468544d1c20716c6a4 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 21:08:09 +0900 Subject: [PATCH 118/291] =?UTF-8?q?fix=20:=20=EB=B9=8C=EB=8D=94=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=B8=ED=95=B4=20=EA=B0=92=20null=EA=B0=92=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20Builder.Default=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/curation/entity/Curation.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index a80f177..b04f83c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -18,7 +18,7 @@ @Table(name = "curation") @AllArgsConstructor public class Curation { - + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -66,15 +66,22 @@ public class Curation { @Column(name = "style") private List styles; + + + @Builder.Default @Column(name = "like_count") private Integer likeCount = 0; + @Builder.Default @Column(name = "view_count") private Integer viewCount = 0; + @Builder.Default @Column(name = "comment_count") private Integer commentCount = 0; + + @Builder.Default @Column(name = "popularity_score") private Integer popularityScore = 0; From 392aecfd3b1bddcdb6ea44ff5b38e449e5272b30 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 21:10:49 +0900 Subject: [PATCH 119/291] =?UTF-8?q?fix=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=EC=8B=9C=20title=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/curation/service/base/CurationService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index ab1d6f3..c3dca9a 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -44,6 +44,7 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { Curation curation = Curation.builder() .user(user) + .title(req.title()) .thumbnailUrl(req.thumbnail().imageUrl()) .thumbnailColor(req.thumbnail().imageColor()) .bookTitle(req.book().title()) From c3bd94ab1d79fd09d3c504e0d53e71e85ef05f35 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 21:46:39 +0900 Subject: [PATCH 120/291] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EB=8F=85=EC=84=9C=20=EC=B7=A8=ED=96=A5=EC=9D=B4=20=EC=A1=B4?= =?UTF-8?q?=EC=9E=AC=ED=95=98=EC=A7=80=20=EC=95=8A=EC=95=84=EB=8F=84=20?= =?UTF-8?q?=EC=A0=95=EC=83=81=EC=A0=81=EC=9D=B4=EB=8B=A4=EB=9D=BC=EA=B3=A0?= =?UTF-8?q?=20=ED=8C=90=EB=8B=A8=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?,404=20->=20200=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 1a1e4ef..4262c7d 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -30,7 +30,7 @@ public enum ErrorCode implements ErrorCodeInterface { // -- Reading Preference -- READING_PREFERENCE_ALREADY_RESiGSTER(HttpStatus.CONFLICT, "이미 독서 취향이 존재합니다."), - READING_PREFERENCE_NOT_EXISTED(HttpStatus.NOT_FOUND, "사용자의 독서 취향이 존재하지 않습니다."), + READING_PREFERENCE_NOT_EXISTED(HttpStatus.OK, "사용자의 독서 취향이 존재하지 않습니다."), // 독서취향이 존재하지 않더라도 문제가 없어서 OK // -- Curation -- CURATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), From e80ae6ea7c366568835ac415f868aa563767a078 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 22:32:13 +0900 Subject: [PATCH 121/291] =?UTF-8?q?fix=20:=20User=20Error=20=EB=B0=8F=20Su?= =?UTF-8?q?ccess=20Code=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreference/PrferenceErrorCode.java | 2 +- .../domain/author/enums/AuthroErrorCode.java | 2 +- .../curation/enums/CurationErrorCode.java | 2 +- .../util/gemini/enums/GeminiErrorCode.java | 2 +- .../mvp/domain/user/enums/UserErrorCode.java | 22 ++++++++++++++++ .../mvp/domain/user/enums/UserRole.java | 18 +++++++++++++ .../domain/user/enums/UserSuccessCode.java | 22 ++++++++++++++++ .../exception/AlreadyDeletedException.java | 10 +++++++ .../user/exception/NotHaveAdminRole.java | 10 +++++++ .../exception/PasswordMismatchException.java | 10 +++++++ .../WrongCurrentPasswordException.java | 11 ++++++++ .../BookPick/mvp/global/api/ApiResponse.java | 26 ++++++++++++------- .../mvp/global/api/ErrorCode/ErrorCode.java | 1 + .../ErrorCodeInterface.java | 3 ++- .../global/enums/SuccessCodeInterface.java | 9 +++++++ .../global/exception/BusinessException.java | 3 +-- .../exception/GlobalExceptionHandler.java | 2 +- 17 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java create mode 100644 src/main/java/BookPick/mvp/domain/user/enums/UserRole.java create mode 100644 src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java rename src/main/java/BookPick/mvp/global/{api/ErrorCode => enums}/ErrorCodeInterface.java (76%) create mode 100644 src/main/java/BookPick/mvp/global/enums/SuccessCodeInterface.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java index eb32108..6eaf14f 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.ReadingPreference; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java b/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java index b68e2ee..50f1cff 100644 --- a/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/author/enums/AuthroErrorCode.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.author.enums; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java index 59a80c0..7b64ef4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.enums; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java index ede3a04..dd67358 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/enums/GeminiErrorCode.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.curation.util.gemini.enums; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java b/src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java new file mode 100644 index 0000000..362b75a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java @@ -0,0 +1,22 @@ +package BookPick.mvp.domain.user.enums; + +import BookPick.mvp.global.enums.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum UserErrorCode implements ErrorCodeInterface { + + // -- User -- + User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 + NOT_HAVE_ADMIN_ROLE(HttpStatus.BAD_REQUEST, "관리자 권한이 없는 유저입니다."), + ALREADY_DELETE_USR(HttpStatus.CONFLICT, "이미 삭제한 유저입니다."), //409, 이미 삭제한 유저기때문에 conflict 충돌이라는 의미 + PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "변경할 비밀번호와 확인 비밀번호가 일치하지 않습니다."), //409, 이미 삭제한 유저기때문에 conflict 충돌이라는 의미 + WRONG_CURRENT_PASSWORD(HttpStatus.UNAUTHORIZED, "현재 비밀번호가 올바르지 않습니다."); + + private final HttpStatus status; + private final String message; + +} diff --git a/src/main/java/BookPick/mvp/domain/user/enums/UserRole.java b/src/main/java/BookPick/mvp/domain/user/enums/UserRole.java new file mode 100644 index 0000000..d58e13e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/enums/UserRole.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.user.enums; + +import lombok.Getter; + +@Getter +public enum UserRole { + ROLE_USER("user"), + ROLE_ADMIN("admin"); + + private final String description; + + UserRole(String description) { + this.description = description; + } + + + ; +} diff --git a/src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java b/src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java new file mode 100644 index 0000000..5c3e7cf --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java @@ -0,0 +1,22 @@ +package BookPick.mvp.domain.user.enums; + +import BookPick.mvp.global.enums.SuccessCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum UserSuccessCode implements SuccessCodeInterface { + + CREATE_USER_SUCCESS(HttpStatus.CREATED, "사용자 생성을 성공하였습니다."), + GET_USERS_SUCCESS(HttpStatus.OK, "사용자 목록 조회를 성공하였습니다."), + GET_MY_PROFILE_SUCCESS(HttpStatus.OK, "사용자 프로필 조회를 성공하였습니다."), + UPDATE_MY_PROFILE_SUCCESS(HttpStatus.OK, "사용자 프로필 수정을 성공하였습니다."), + SOFT_DELETE_MY_PROFILE_SUCCESS(HttpStatus.OK, "사용자 임시 삭제를 성공하였습니다."), + HARD_DELETE_MY_PROFILE_SUCCESS(HttpStatus.OK, "사용자 완전 삭제를 성공하였습니다."), + PASSWORD_CHANGE_SUCCESS(HttpStatus.OK, "비밀번호 변경을 성공하였습니다."); + + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java b/src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java new file mode 100644 index 0000000..a1e69f9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.user.exception; + +import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class AlreadyDeletedException extends BusinessException { + public AlreadyDeletedException(){ + super(UserErrorCode.ALREADY_DELETE_USR); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java b/src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java new file mode 100644 index 0000000..2ed19c7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.user.exception; + +import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class NotHaveAdminRole extends BusinessException { + public NotHaveAdminRole(){ + super(UserErrorCode.NOT_HAVE_ADMIN_ROLE); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java b/src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java new file mode 100644 index 0000000..06fdf0d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.user.exception; + +import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class PasswordMismatchException extends BusinessException { + public PasswordMismatchException() { + super(UserErrorCode.PASSWORD_MISMATCH); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java b/src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java new file mode 100644 index 0000000..bdf7af7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.user.exception; + + +import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class WrongCurrentPasswordException extends BusinessException { + public WrongCurrentPasswordException(){ + super(UserErrorCode.WRONG_CURRENT_PASSWORD); + } +} diff --git a/src/main/java/BookPick/mvp/global/api/ApiResponse.java b/src/main/java/BookPick/mvp/global/api/ApiResponse.java index 6ce56e1..2c12ad6 100644 --- a/src/main/java/BookPick/mvp/global/api/ApiResponse.java +++ b/src/main/java/BookPick/mvp/global/api/ApiResponse.java @@ -1,15 +1,14 @@ package BookPick.mvp.global.api; + import BookPick.mvp.global.api.ErrorCode.ErrorCode; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; import BookPick.mvp.global.api.SuccessCode.SuccessCode; -import org.springframework.http.HttpStatus; - - +import BookPick.mvp.global.enums.ErrorCodeInterface; +import BookPick.mvp.global.enums.SuccessCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; - +import org.springframework.http.HttpStatus; @Getter @Setter @@ -19,13 +18,22 @@ public class ApiResponse { private String message; // ex. 이미 존재하는 이메일입니다. (사람용 친화) private T data; - // -- Success -- - public static ApiResponse success(SuccessCode successCode, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 - return new ApiResponse(successCode.getStatus().value(), successCode.getMessage(), data); + public static ApiResponse success(SuccessCode successMessage, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 + return new ApiResponse(successMessage.getStatus().value(), successMessage.getMessage(), data); + } + public static ApiResponse success(SuccessCodeInterface successMessage, T data) { // 를 반환 타입 앞에 써주는 이유 : 컴파일시 해당 메서드의 반환 타입을 알기 위해서, 런타임시 파라미터로 들어온 T를 static 상황(컴파일 상황)에서는 모르기 때문이다. 컴퓨일이 런타임 이전에 일어나기 때문에 + return new ApiResponse(successMessage.getStatus().value(), successMessage.getMessage(), data); } // -- Error -- + public static ApiResponse error(ErrorCode errorCode) { + return new ApiResponse( + errorCode.getStatus().value(), + errorCode.getMessage(), // @Valid 같은 데서 넘어온 메시지 + null + ); + } public static ApiResponse error(ErrorCodeInterface errorCode) { return new ApiResponse( errorCode.getStatus().value(), @@ -33,7 +41,6 @@ public static ApiResponse error(ErrorCodeInterface errorCode) { null ); } - public static ApiResponse customError(HttpStatus httpStatus, String message, T data) { return new ApiResponse( httpStatus.value(), @@ -42,4 +49,3 @@ public static ApiResponse customError(HttpStatus httpStatus, String messa ); } } - diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 4262c7d..92d804c 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -1,5 +1,6 @@ package BookPick.mvp.global.api.ErrorCode; +import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java b/src/main/java/BookPick/mvp/global/enums/ErrorCodeInterface.java similarity index 76% rename from src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java rename to src/main/java/BookPick/mvp/global/enums/ErrorCodeInterface.java index ce07e83..f6dc22c 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCodeInterface.java +++ b/src/main/java/BookPick/mvp/global/enums/ErrorCodeInterface.java @@ -1,4 +1,4 @@ -package BookPick.mvp.global.api.ErrorCode; +package BookPick.mvp.global.enums; import org.springframework.http.HttpStatus; @@ -6,3 +6,4 @@ public interface ErrorCodeInterface { HttpStatus getStatus(); String getMessage(); } + diff --git a/src/main/java/BookPick/mvp/global/enums/SuccessCodeInterface.java b/src/main/java/BookPick/mvp/global/enums/SuccessCodeInterface.java new file mode 100644 index 0000000..7b5fd34 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/enums/SuccessCodeInterface.java @@ -0,0 +1,9 @@ +package BookPick.mvp.global.enums; + +import org.springframework.http.HttpStatus; + +public interface SuccessCodeInterface { + public HttpStatus getStatus(); + public String getMessage(); + +} diff --git a/src/main/java/BookPick/mvp/global/exception/BusinessException.java b/src/main/java/BookPick/mvp/global/exception/BusinessException.java index 6c6b779..f3053f5 100644 --- a/src/main/java/BookPick/mvp/global/exception/BusinessException.java +++ b/src/main/java/BookPick/mvp/global/exception/BusinessException.java @@ -1,7 +1,6 @@ package BookPick.mvp.global.exception; -import BookPick.mvp.global.api.ErrorCode.ErrorCode; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.Getter; @Getter diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index b70caf1..c9e546c 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -3,7 +3,7 @@ import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.ErrorCode.ErrorCode; -import BookPick.mvp.global.api.ErrorCode.ErrorCodeInterface; +import BookPick.mvp.global.enums.ErrorCodeInterface; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; From 6540f7d3c4f4dceea2a31e3890569c6c4287cae3 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 12 Nov 2025 22:36:39 +0900 Subject: [PATCH 122/291] =?UTF-8?q?feat=20:=20User=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=B4=88=EA=B8=B0=20=EC=84=A4=EC=A0=95=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 13 --- .../user/controller/base/UserController.java | 84 ++++++++++++++ .../passWord/PasswordContorller.java | 34 ++++++ .../controller/profile/ProfileController.java | 58 ++++++++++ .../user/dto/base/create/UserCreateReq.java | 24 ++++ .../user/dto/base/create/UserCreateRes.java | 11 ++ .../domain/user/dto/base/read/UserGetRes.java | 32 ++++++ .../user/dto/base/soft/UserSoftDeleteRes.java | 19 ++++ .../user/dto/base/update/UserUpdateReq.java | 32 ++++++ .../user/dto/base/update/UserUpdateRes.java | 31 ++++++ .../user/dto/passWord/PassWordChangeReq.java | 19 ++++ .../user/dto/passWord/PassWordChangeRes.java | 19 ++++ .../BookPick/mvp/domain/user/entity/User.java | 10 +- .../mvp/domain/user/service/UserService.java | 22 ---- .../domain/user/service/base/UserService.java | 105 ++++++++++++++++++ .../service/passWord/PassWordService.java | 48 ++++++++ .../mvp/domain/user/util/AdminManager.java | 20 ++++ 17 files changed, 543 insertions(+), 38 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/user/controller/UserController.java create mode 100644 src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java create mode 100644 src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java create mode 100644 src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeReq.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/service/UserService.java create mode 100644 src/main/java/BookPick/mvp/domain/user/service/base/UserService.java create mode 100644 src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java create mode 100644 src/main/java/BookPick/mvp/domain/user/util/AdminManager.java diff --git a/src/main/java/BookPick/mvp/domain/user/controller/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/UserController.java deleted file mode 100644 index daba4c4..0000000 --- a/src/main/java/BookPick/mvp/domain/user/controller/UserController.java +++ /dev/null @@ -1,13 +0,0 @@ -package BookPick.mvp.domain.user.controller; - -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/users") -public class UserController { - - - - - -} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java new file mode 100644 index 0000000..ac266ab --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -0,0 +1,84 @@ +package BookPick.mvp.domain.user.controller.base; + + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateReq; +import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateRes; +import springboot.kakao_boot_camp.domain.user.dto.base.read.UserGetRes; +import springboot.kakao_boot_camp.domain.user.dto.base.soft.UserSoftDeleteRes; +import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateReq; +import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateRes; +import springboot.kakao_boot_camp.domain.user.exception.NotHaveAdminRole; +import springboot.kakao_boot_camp.domain.user.service.base.UserService; +import springboot.kakao_boot_camp.domain.user.util.AdminManager; +import springboot.kakao_boot_camp.global.api.ApiResponse; +import springboot.kakao_boot_camp.global.api.SuccessCode; +import springboot.kakao_boot_camp.security.CustomUserDetails; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class UserController { + + private final UserService userService; + private final AdminManager adminManager; + + // 유저 생성 + @PostMapping + public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, + @RequestBody @Valid UserCreateReq req) { + if (adminManager.isAdmin(currentUser.getAuthorities())) { + throw new NotHaveAdminRole(); + } + + UserCreateRes res = userService.CreateUser(req); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(SuccessCode.CREATE_USER_SUCCESS, res)); + } + + // 유저 조회 + @GetMapping + public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { + UserGetRes res = userService.userProfileGet(currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, res)); + } + + + // 유저 수정 + @PatchMapping + public ResponseEntity> updateUserProfile(@AuthenticationPrincipal CustomUserDetails currentUser + , @RequestBody @Valid UserUpdateReq req) { + UserUpdateRes res = userService.userProfileUpdate(currentUser.getId(), req); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); + } + + + // 유저 소프트 삭제 + public ResponseEntity> softDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { + UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, res)); + } + + + // 유저 하드 삭제 + public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { + if (adminManager.isAdmin(currentUser.getAuthorities())) { + userService.hardDeleteUserProfile(currentUser.getId()); + } + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, null)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java b/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java new file mode 100644 index 0000000..a81dcb1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java @@ -0,0 +1,34 @@ +package BookPick.mvp.domain.user.controller.passWord; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +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 org.springframework.web.bind.annotation.RestController; +import springboot.kakao_boot_camp.domain.user.dto.passWord.PassWordChangeReq; +import springboot.kakao_boot_camp.domain.user.service.passWord.PassWordService; +import springboot.kakao_boot_camp.global.api.ApiResponse; +import springboot.kakao_boot_camp.global.api.SuccessCode; +import springboot.kakao_boot_camp.security.CustomUserDetails; + + +@RestController +@RequestMapping("/api/v1/my/password") +@RequiredArgsConstructor +public class PasswordContorller { + private final PassWordService passWordService; + + @PostMapping("/change") + public ResponseEntity> changePassword(@AuthenticationPrincipal CustomUserDetails currentUser, + @RequestBody @Valid PassWordChangeReq req) { + passWordService.PassWordChange(currentUser.getId(), req); + + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.PASSWORD_CHANGE_SUCCESS, null)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java new file mode 100644 index 0000000..0ad658a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java @@ -0,0 +1,58 @@ +package BookPick.mvp.domain.user.controller.profile; + + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import springboot.kakao_boot_camp.domain.user.dto.base.read.UserGetRes; +import springboot.kakao_boot_camp.domain.user.dto.base.soft.UserSoftDeleteRes; +import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateReq; +import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateRes; +import springboot.kakao_boot_camp.domain.user.service.base.UserService; +import springboot.kakao_boot_camp.domain.user.util.AdminManager; +import springboot.kakao_boot_camp.global.api.ApiResponse; +import springboot.kakao_boot_camp.global.api.SuccessCode; +import springboot.kakao_boot_camp.security.CustomUserDetails; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/profiles") +public class ProfileController { + + private final UserService userService; + private final AdminManager adminManager; + + + // 1. 프로필 조회 + @GetMapping("/me") + public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { + UserGetRes res = userService.userProfileGet(currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, res)); + } + + + // 2. 프로필 수정 + @PatchMapping("/me") + public ResponseEntity> updateUserProfile(@AuthenticationPrincipal @Valid CustomUserDetails currentUser + , @RequestBody @Valid UserUpdateReq req) { + UserUpdateRes res = userService.userProfileUpdate(currentUser.getId(), req); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); + } + + + // 3. 회원 탈퇴 (소프트 삭제) + @DeleteMapping("/me") + public ResponseEntity> softDeleteProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { + UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.SOFT_DELETE_MY_PROFILE_SUCCESS, res)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java new file mode 100644 index 0000000..e9c20e0 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java @@ -0,0 +1,24 @@ +package BookPick.mvp.domain.user.dto.base.create; + +import springboot.kakao_boot_camp.domain.user.enums.UserRole; +import springboot.kakao_boot_camp.domain.user.model.User; + +public record UserCreateReq( + Long id, + String email, + String passWord, + String nickName, + String profileImage, + UserRole role +) { + public UserCreateReq from(User user){ + return new UserCreateReq( + user.getId(), + user.getEmail(), + user.getPassWord(), + user.getNickName(), + user.getProfileImage(), + user.getRole() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java new file mode 100644 index 0000000..41287b0 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.user.dto.base.create; + +import springboot.kakao_boot_camp.domain.user.model.User; + +public record UserCreateRes( + Long id +) { + public static UserCreateRes from(User user){ + return new UserCreateRes(user.getId()); + } + } diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java new file mode 100644 index 0000000..4e46112 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java @@ -0,0 +1,32 @@ +package BookPick.mvp.domain.user.dto.base.read; + +import springboot.kakao_boot_camp.domain.user.enums.UserRole; +import springboot.kakao_boot_camp.domain.user.model.User; + +import java.time.LocalDateTime; + +public record UserGetRes( + Long userId, + String email, + String nickName, + String profileImage, + UserRole role, + LocalDateTime createdAt, + LocalDateTime updatedAt, + LocalDateTime deletedAt, + Boolean deleted +) { + public static UserGetRes from(User user) { + return new UserGetRes( + user.getId(), + user.getEmail(), + user.getNickName(), + user.getProfileImage(), + user.getRole(), + user.getCreatedAt(), + user.getUpdatedAt(), + user.getDeletedAt(), + user.isDeleted() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java new file mode 100644 index 0000000..89c3c89 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.dto.base.soft; + +import springboot.kakao_boot_camp.domain.user.model.User; + +import java.time.LocalDateTime; + +public record UserSoftDeleteRes( + Long userId, + Boolean deleted, + LocalDateTime deletedAt +) { + public static UserSoftDeleteRes from(User user) { + return new UserSoftDeleteRes( + user.getId(), + user.isDeleted(), + user.getDeletedAt() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java new file mode 100644 index 0000000..7ece6da --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java @@ -0,0 +1,32 @@ +package BookPick.mvp.domain.user.dto.base.update; + +import springboot.kakao_boot_camp.domain.user.enums.UserRole; +import springboot.kakao_boot_camp.domain.user.model.User; + +import java.time.LocalDateTime; + +public record UserUpdateReq( + Long userId, + String email, + String nickName, + String profileImage, + UserRole role, + LocalDateTime createdAt, + LocalDateTime updatedAt, + LocalDateTime deletedAt, + Boolean deleted +) { + public UserUpdateReq from(User user) { + return new UserUpdateReq( + user.getId(), + user.getEmail(), + user.getNickName(), + user.getProfileImage(), + user.getRole(), + user.getCreatedAt(), + user.getUpdatedAt(), + user.getDeletedAt(), + user.isDeleted() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java new file mode 100644 index 0000000..36365d3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.user.dto.base.update; + +import springboot.kakao_boot_camp.domain.user.enums.UserRole; +import springboot.kakao_boot_camp.domain.user.model.User; + +import java.time.LocalDateTime; +public record UserUpdateRes( + Long userId, + String email, + String nickName, + String profileImage, + UserRole role, + LocalDateTime createdAt, + LocalDateTime updatedAt, + LocalDateTime deletedAt, + Boolean deleted +) { + public static UserUpdateRes from(User user) { + return new UserUpdateRes( + user.getId(), + user.getEmail(), + user.getNickName(), + user.getProfileImage(), + user.getRole(), + user.getCreatedAt(), + user.getUpdatedAt(), + user.getDeletedAt(), + user.isDeleted() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeReq.java b/src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeReq.java new file mode 100644 index 0000000..83aefea --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeReq.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.dto.passWord; + +import jakarta.validation.constraints.NotNull; + +public record PassWordChangeReq( + + @NotNull + String nowPassWord, + + @NotNull + String newPassWord, + + @NotNull + String confirmPassword +) { + public static PassWordChangeReq from(String nowPassWord, String newPassWord, String confirmPassword){ + return new PassWordChangeReq(nowPassWord, newPassWord, confirmPassword); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeRes.java b/src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeRes.java new file mode 100644 index 0000000..fda1940 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/passWord/PassWordChangeRes.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.dto.passWord; + +import jakarta.validation.constraints.NotNull; + +public record PassWordChangeRes( + + @NotNull + String nowPassWord, + + @NotNull + String newPassWord, + + @NotNull + String confirmPassword +) { + public static PassWordChangeRes from(String nowPassWord, String newPassWord, String confirmPassword){ + return new PassWordChangeRes(nowPassWord, newPassWord, confirmPassword); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index d8a30a9..90082c6 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -48,7 +48,7 @@ public class User { @Column(name = "profile_image_url", length = 500) private String profileImageUrl; // 프로필 사진 경로 - @Column(name ="is_first_login", nullable = false) + @Column(name = "is_first_login", nullable = false) @Builder.Default private boolean isFirstLogin = true; @@ -61,8 +61,12 @@ public class User { private LocalDateTime updatedAt; // 수정 시각 - public void isNotFirstLogin(){ - this.isFirstLogin=false; + @Column(name = "deleted_at", nullable = false) + private LocalDateTime deletedAt; // 삭제 시각 + + + public void isNotFirstLogin() { + this.isFirstLogin = false; } diff --git a/src/main/java/BookPick/mvp/domain/user/service/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/UserService.java deleted file mode 100644 index be70d57..0000000 --- a/src/main/java/BookPick/mvp/domain/user/service/UserService.java +++ /dev/null @@ -1,22 +0,0 @@ -package BookPick.mvp.domain.user.service; - -import BookPick.mvp.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.format.DateTimeFormatter; - -@Service -@Transactional -@RequiredArgsConstructor -public class UserService { - - private final UserRepository repo; - private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_INSTANT; - private final PasswordEncoder passwordEncoder; - - - -} diff --git a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java new file mode 100644 index 0000000..139d413 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java @@ -0,0 +1,105 @@ +package BookPick.mvp.domain.user.service.base; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateReq; +import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateRes; +import springboot.kakao_boot_camp.domain.user.dto.base.read.UserGetRes; +import springboot.kakao_boot_camp.domain.user.dto.base.soft.UserSoftDeleteRes; +import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateReq; +import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateRes; +import springboot.kakao_boot_camp.domain.user.exception.AlreadyDeletedException; +import springboot.kakao_boot_camp.domain.user.exception.UserNotFoundException; +import springboot.kakao_boot_camp.domain.user.model.User; +import springboot.kakao_boot_camp.domain.user.repository.UserRepository; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class UserService { + final private UserRepository userRepository; + + // 1. 유저 생성 (관리자 권한) + public UserCreateRes CreateUser(UserCreateReq req) { + + User user = User.builder() + .email(req.email()) + .passWord(req.passWord()) + .nickName(req.nickName()) + .profileImage(req.profileImage()) + .role(req.role()) + .build(); + + User result = userRepository.save(user); + + + return UserCreateRes.from(result); + } + + + + // 2.1 개인 정보 조회 (개인 권한) + public UserGetRes userProfileGet(Long userId) { + + User result = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + return UserGetRes.from(result); + } + + + + // 3.1 유저 수정 + @Transactional + public UserUpdateRes userProfileUpdate(Long userId, UserUpdateReq req) { + + if (userId == null) { + throw new UserNotFoundException(); + } + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + // 더티 체킹 + if (req.email() != null) user.setEmail(req.email()); + if (req.nickName() != null) user.setNickName(req.nickName()); + if (req.profileImage() != null) user.setProfileImage(req.profileImage()); + + return UserUpdateRes.from(user); + + } + + + + // 4.1 유저 소프트 삭제 + @Transactional + public UserSoftDeleteRes softDeleteProfile(Long userId) { + + // 델리트 1로 바꾸고 삭제 시간 업데이트 + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + if (user.isDeleted()) { + throw new AlreadyDeletedException(); + } + + user.setDeleted(true); + user.setDeletedAt(LocalDateTime.now()); + + return UserSoftDeleteRes.from(user); + } + // 4.2 유저 하드 삭제 + @Transactional + public void hardDeleteUserProfile(Long userId) { + + // 델리트 1로 바꾸고 삭제 시간 업데이트 + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + userRepository.deleteById(userId); + + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java b/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java new file mode 100644 index 0000000..1ff8a94 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java @@ -0,0 +1,48 @@ +package BookPick.mvp.domain.user.service.passWord; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import springboot.kakao_boot_camp.domain.user.dto.passWord.PassWordChangeReq; +import springboot.kakao_boot_camp.domain.user.exception.PasswordMismatchException; +import springboot.kakao_boot_camp.domain.user.exception.UserNotFoundException; +import springboot.kakao_boot_camp.domain.user.exception.WrongCurrentPasswordException; +import springboot.kakao_boot_camp.domain.user.model.User; +import springboot.kakao_boot_camp.domain.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class PassWordService { + final private UserRepository userRepository; + final private PasswordEncoder passwordEncoder; + + + // 1. 비밀번호 변경 + // TODO 1. 자동입력 방지 문자 추가 + @Transactional + public void PassWordChange(Long userId, PassWordChangeReq req) { + if (userId == null) { + throw new UserNotFoundException(); + } + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + // 1) 새로운 비밀번호 컨펌 비밀번호 체크 + if (!req.newPassWord().equals(req.confirmPassword())) { + throw new PasswordMismatchException(); + } + + // 2) 입력받은 비밀번호와 기존 비밀번호 체크 + if (!passwordEncoder.matches(req.nowPassWord(), user.getPassWord())) { + throw new WrongCurrentPasswordException(); + } + + // 3) 기존 비밀번호를 입력받은 비밀번호로 변경 + user.setPassWord(passwordEncoder.encode(req.newPassWord())); + + + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java b/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java new file mode 100644 index 0000000..ddc4db8 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java @@ -0,0 +1,20 @@ +package BookPick.mvp.domain.user.util; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; +import springboot.kakao_boot_camp.domain.user.enums.UserRole; + +import java.util.Collection; + +@Component +public class AdminManager { + + public boolean isAdmin(Collection authorities){ + for (GrantedAuthority authority : authorities){ + if(authority.getAuthority().equals(UserRole.ROLE_ADMIN.name())){ + return true; + } + } + return false; + } +} From 0e2600ac501b4fab33185b72193f11fe3dea9e12 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 14 Nov 2025 20:20:38 +0900 Subject: [PATCH 123/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C,=20=EC=B6=94=EA=B0=80=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B3=A0=20=EC=86=8C=ED=94=84=ED=8A=B8=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/base/UserController.java | 50 +++++++++---------- .../passWord/PasswordContorller.java | 13 ++--- .../controller/profile/ProfileController.java | 33 ++++++------ .../mvp/domain/user/dto/base/UserReq.java | 24 +++++++++ .../{read/UserGetRes.java => UserRes.java} | 18 +++---- .../user/dto/base/create/UserCreateReq.java | 24 --------- .../user/dto/base/create/UserCreateRes.java | 11 ---- .../{soft => delete}/UserSoftDeleteRes.java | 5 +- .../user/dto/base/update/UserUpdateReq.java | 32 ------------ .../user/dto/base/update/UserUpdateRes.java | 31 ------------ .../BookPick/mvp/domain/user/entity/User.java | 7 ++- .../domain/user/service/base/UserService.java | 39 +++++++-------- .../service/passWord/PassWordService.java | 16 +++--- .../mvp/domain/user/util/AdminManager.java | 4 +- .../mvp/global/service/S3Service.java | 30 +++++++++++ 15 files changed, 147 insertions(+), 190 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java rename src/main/java/BookPick/mvp/domain/user/dto/base/{read/UserGetRes.java => UserRes.java} (58%) delete mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java rename src/main/java/BookPick/mvp/domain/user/dto/base/{soft => delete}/UserSoftDeleteRes.java (77%) delete mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java delete mode 100644 src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java create mode 100644 src/main/java/BookPick/mvp/global/service/S3Service.java diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java index ac266ab..f4848b7 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -1,24 +1,22 @@ package BookPick.mvp.domain.user.controller.base; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.user.dto.base.UserReq; +import BookPick.mvp.domain.user.dto.base.UserRes; +import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; +import BookPick.mvp.domain.user.enums.UserSuccessCode; +import BookPick.mvp.domain.user.exception.NotHaveAdminRole; +import BookPick.mvp.domain.user.service.base.UserService; +import BookPick.mvp.domain.user.util.AdminManager; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateReq; -import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateRes; -import springboot.kakao_boot_camp.domain.user.dto.base.read.UserGetRes; -import springboot.kakao_boot_camp.domain.user.dto.base.soft.UserSoftDeleteRes; -import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateReq; -import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateRes; -import springboot.kakao_boot_camp.domain.user.exception.NotHaveAdminRole; -import springboot.kakao_boot_camp.domain.user.service.base.UserService; -import springboot.kakao_boot_camp.domain.user.util.AdminManager; -import springboot.kakao_boot_camp.global.api.ApiResponse; -import springboot.kakao_boot_camp.global.api.SuccessCode; -import springboot.kakao_boot_camp.security.CustomUserDetails; @RestController @RequiredArgsConstructor @@ -30,36 +28,36 @@ public class UserController { // 유저 생성 @PostMapping - public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, - @RequestBody @Valid UserCreateReq req) { + public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, + @RequestBody @Valid UserReq req) { if (adminManager.isAdmin(currentUser.getAuthorities())) { throw new NotHaveAdminRole(); } - UserCreateRes res = userService.CreateUser(req); + UserRes res = userService.CreateUser(req); return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(SuccessCode.CREATE_USER_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.CREATE_USER_SUCCESS, res)); } // 유저 조회 @GetMapping - public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { - UserGetRes res = userService.userProfileGet(currentUser.getId()); + public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { + UserRes res = userService.userProfileGet(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); } // 유저 수정 @PatchMapping - public ResponseEntity> updateUserProfile(@AuthenticationPrincipal CustomUserDetails currentUser - , @RequestBody @Valid UserUpdateReq req) { - UserUpdateRes res = userService.userProfileUpdate(currentUser.getId(), req); + public ResponseEntity> updateUserProfile(@AuthenticationPrincipal CustomUserDetails currentUser + , @RequestBody @Valid UserReq req) { + UserRes res = userService.userProfileUpdate(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); } @@ -68,17 +66,17 @@ public ResponseEntity> softDeleteUser(@Authentica UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); } // 유저 하드 삭제 - public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { + public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { if (adminManager.isAdmin(currentUser.getAuthorities())) { userService.hardDeleteUserProfile(currentUser.getId()); } return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, null)); + .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, null)); } } diff --git a/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java b/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java index a81dcb1..df4c1cb 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java @@ -1,5 +1,11 @@ package BookPick.mvp.domain.user.controller.passWord; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.user.dto.passWord.PassWordChangeReq; +import BookPick.mvp.domain.user.enums.UserSuccessCode; +import BookPick.mvp.domain.user.service.passWord.PassWordService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -9,11 +15,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import springboot.kakao_boot_camp.domain.user.dto.passWord.PassWordChangeReq; -import springboot.kakao_boot_camp.domain.user.service.passWord.PassWordService; -import springboot.kakao_boot_camp.global.api.ApiResponse; -import springboot.kakao_boot_camp.global.api.SuccessCode; -import springboot.kakao_boot_camp.security.CustomUserDetails; @RestController @@ -29,6 +30,6 @@ public ResponseEntity> changePassword(@AuthenticationPrincipal return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.PASSWORD_CHANGE_SUCCESS, null)); + .body(ApiResponse.success(UserSuccessCode.PASSWORD_CHANGE_SUCCESS, null)); } } diff --git a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java index 0ad658a..22d6a6e 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java @@ -1,21 +1,20 @@ package BookPick.mvp.domain.user.controller.profile; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.user.dto.base.UserReq; +import BookPick.mvp.domain.user.dto.base.UserRes; +import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; +import BookPick.mvp.domain.user.enums.UserSuccessCode; +import BookPick.mvp.domain.user.service.base.UserService; +import BookPick.mvp.domain.user.util.AdminManager; +import BookPick.mvp.global.api.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import springboot.kakao_boot_camp.domain.user.dto.base.read.UserGetRes; -import springboot.kakao_boot_camp.domain.user.dto.base.soft.UserSoftDeleteRes; -import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateReq; -import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateRes; -import springboot.kakao_boot_camp.domain.user.service.base.UserService; -import springboot.kakao_boot_camp.domain.user.util.AdminManager; -import springboot.kakao_boot_camp.global.api.ApiResponse; -import springboot.kakao_boot_camp.global.api.SuccessCode; -import springboot.kakao_boot_camp.security.CustomUserDetails; @RestController @RequiredArgsConstructor @@ -28,22 +27,22 @@ public class ProfileController { // 1. 프로필 조회 @GetMapping("/me") - public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { - UserGetRes res = userService.userProfileGet(currentUser.getId()); + public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { + UserRes res = userService.userProfileGet(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.GET_MY_PROFILE_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); } // 2. 프로필 수정 @PatchMapping("/me") - public ResponseEntity> updateUserProfile(@AuthenticationPrincipal @Valid CustomUserDetails currentUser - , @RequestBody @Valid UserUpdateReq req) { - UserUpdateRes res = userService.userProfileUpdate(currentUser.getId(), req); + public ResponseEntity> updateUserProfile(@AuthenticationPrincipal @Valid CustomUserDetails currentUser + , @RequestBody @Valid UserReq req) { + UserRes res = userService.userProfileUpdate(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); } @@ -53,6 +52,6 @@ public ResponseEntity> softDeleteProfile(@Authent UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.SOFT_DELETE_MY_PROFILE_SUCCESS, res)); + .body(ApiResponse.success(UserSuccessCode.SOFT_DELETE_MY_PROFILE_SUCCESS, res)); } } diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java new file mode 100644 index 0000000..3b3dc78 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java @@ -0,0 +1,24 @@ +package BookPick.mvp.domain.user.dto.base; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.user.entity.User; + +public record UserReq( + Long id, + String email, + String passWord, + String nickName, + String profileImage, + Roles role +) { + public UserReq from(User user){ + return new UserReq( + user.getId(), + user.getEmail(), + user.getPassword(), + user.getNickname(), + user.getProfileImageUrl(), + user.getRole() + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java similarity index 58% rename from src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java rename to src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java index 4e46112..eb9c517 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/read/UserGetRes.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java @@ -1,27 +1,27 @@ -package BookPick.mvp.domain.user.dto.base.read; +package BookPick.mvp.domain.user.dto.base; -import springboot.kakao_boot_camp.domain.user.enums.UserRole; -import springboot.kakao_boot_camp.domain.user.model.User; +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.user.entity.User; import java.time.LocalDateTime; -public record UserGetRes( +public record UserRes( Long userId, String email, String nickName, String profileImage, - UserRole role, + Roles role, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime deletedAt, Boolean deleted ) { - public static UserGetRes from(User user) { - return new UserGetRes( + public static UserRes from(User user) { + return new UserRes( user.getId(), user.getEmail(), - user.getNickName(), - user.getProfileImage(), + user.getNickname(), + user.getProfileImageUrl(), user.getRole(), user.getCreatedAt(), user.getUpdatedAt(), diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java deleted file mode 100644 index e9c20e0..0000000 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateReq.java +++ /dev/null @@ -1,24 +0,0 @@ -package BookPick.mvp.domain.user.dto.base.create; - -import springboot.kakao_boot_camp.domain.user.enums.UserRole; -import springboot.kakao_boot_camp.domain.user.model.User; - -public record UserCreateReq( - Long id, - String email, - String passWord, - String nickName, - String profileImage, - UserRole role -) { - public UserCreateReq from(User user){ - return new UserCreateReq( - user.getId(), - user.getEmail(), - user.getPassWord(), - user.getNickName(), - user.getProfileImage(), - user.getRole() - ); - } -} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java deleted file mode 100644 index 41287b0..0000000 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/create/UserCreateRes.java +++ /dev/null @@ -1,11 +0,0 @@ -package BookPick.mvp.domain.user.dto.base.create; - -import springboot.kakao_boot_camp.domain.user.model.User; - -public record UserCreateRes( - Long id -) { - public static UserCreateRes from(User user){ - return new UserCreateRes(user.getId()); - } - } diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/delete/UserSoftDeleteRes.java similarity index 77% rename from src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java rename to src/main/java/BookPick/mvp/domain/user/dto/base/delete/UserSoftDeleteRes.java index 89c3c89..a224c01 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/soft/UserSoftDeleteRes.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/delete/UserSoftDeleteRes.java @@ -1,6 +1,7 @@ -package BookPick.mvp.domain.user.dto.base.soft; +package BookPick.mvp.domain.user.dto.base.delete; -import springboot.kakao_boot_camp.domain.user.model.User; + +import BookPick.mvp.domain.user.entity.User; import java.time.LocalDateTime; diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java deleted file mode 100644 index 7ece6da..0000000 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateReq.java +++ /dev/null @@ -1,32 +0,0 @@ -package BookPick.mvp.domain.user.dto.base.update; - -import springboot.kakao_boot_camp.domain.user.enums.UserRole; -import springboot.kakao_boot_camp.domain.user.model.User; - -import java.time.LocalDateTime; - -public record UserUpdateReq( - Long userId, - String email, - String nickName, - String profileImage, - UserRole role, - LocalDateTime createdAt, - LocalDateTime updatedAt, - LocalDateTime deletedAt, - Boolean deleted -) { - public UserUpdateReq from(User user) { - return new UserUpdateReq( - user.getId(), - user.getEmail(), - user.getNickName(), - user.getProfileImage(), - user.getRole(), - user.getCreatedAt(), - user.getUpdatedAt(), - user.getDeletedAt(), - user.isDeleted() - ); - } -} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java deleted file mode 100644 index 36365d3..0000000 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/update/UserUpdateRes.java +++ /dev/null @@ -1,31 +0,0 @@ -package BookPick.mvp.domain.user.dto.base.update; - -import springboot.kakao_boot_camp.domain.user.enums.UserRole; -import springboot.kakao_boot_camp.domain.user.model.User; - -import java.time.LocalDateTime; -public record UserUpdateRes( - Long userId, - String email, - String nickName, - String profileImage, - UserRole role, - LocalDateTime createdAt, - LocalDateTime updatedAt, - LocalDateTime deletedAt, - Boolean deleted -) { - public static UserUpdateRes from(User user) { - return new UserUpdateRes( - user.getId(), - user.getEmail(), - user.getNickName(), - user.getProfileImage(), - user.getRole(), - user.getCreatedAt(), - user.getUpdatedAt(), - user.getDeletedAt(), - user.isDeleted() - ); - } -} diff --git a/src/main/java/BookPick/mvp/domain/user/entity/User.java b/src/main/java/BookPick/mvp/domain/user/entity/User.java index 90082c6..fad140b 100644 --- a/src/main/java/BookPick/mvp/domain/user/entity/User.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/User.java @@ -14,6 +14,7 @@ @Entity @Table(name = "user") @Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor @@ -60,11 +61,15 @@ public class User { @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; // 수정 시각 + @Column(name = "deleted") + private boolean deleted = false; - @Column(name = "deleted_at", nullable = false) + @Column(name = "deleted_at") private LocalDateTime deletedAt; // 삭제 시각 + + public void isNotFirstLogin() { this.isFirstLogin = false; } diff --git a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java index 139d413..5029bd2 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java @@ -1,18 +1,15 @@ package BookPick.mvp.domain.user.service.base; +import BookPick.mvp.domain.user.dto.base.UserReq; +import BookPick.mvp.domain.user.dto.base.UserRes; +import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.AlreadyDeletedException; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateReq; -import springboot.kakao_boot_camp.domain.user.dto.base.create.UserCreateRes; -import springboot.kakao_boot_camp.domain.user.dto.base.read.UserGetRes; -import springboot.kakao_boot_camp.domain.user.dto.base.soft.UserSoftDeleteRes; -import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateReq; -import springboot.kakao_boot_camp.domain.user.dto.base.update.UserUpdateRes; -import springboot.kakao_boot_camp.domain.user.exception.AlreadyDeletedException; -import springboot.kakao_boot_camp.domain.user.exception.UserNotFoundException; -import springboot.kakao_boot_camp.domain.user.model.User; -import springboot.kakao_boot_camp.domain.user.repository.UserRepository; import java.time.LocalDateTime; @@ -22,38 +19,38 @@ public class UserService { final private UserRepository userRepository; // 1. 유저 생성 (관리자 권한) - public UserCreateRes CreateUser(UserCreateReq req) { + public UserRes CreateUser(UserReq req) { User user = User.builder() .email(req.email()) - .passWord(req.passWord()) - .nickName(req.nickName()) - .profileImage(req.profileImage()) + .password(req.passWord()) + .nickname(req.nickName()) + .profileImageUrl(req.profileImage()) .role(req.role()) .build(); User result = userRepository.save(user); - return UserCreateRes.from(result); + return UserRes.from(result); } // 2.1 개인 정보 조회 (개인 권한) - public UserGetRes userProfileGet(Long userId) { + public UserRes userProfileGet(Long userId) { User result = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); - return UserGetRes.from(result); + return UserRes.from(result); } // 3.1 유저 수정 @Transactional - public UserUpdateRes userProfileUpdate(Long userId, UserUpdateReq req) { + public UserRes userProfileUpdate(Long userId, UserReq req) { if (userId == null) { throw new UserNotFoundException(); @@ -63,10 +60,10 @@ public UserUpdateRes userProfileUpdate(Long userId, UserUpdateReq req) { // 더티 체킹 if (req.email() != null) user.setEmail(req.email()); - if (req.nickName() != null) user.setNickName(req.nickName()); - if (req.profileImage() != null) user.setProfileImage(req.profileImage()); + if (req.nickName() != null) user.setNickname(req.nickName()); + if (req.profileImage() != null) user.setProfileImageUrl(req.profileImage()); - return UserUpdateRes.from(user); + return UserRes.from(user); } diff --git a/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java b/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java index 1ff8a94..d458db9 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java @@ -1,15 +1,15 @@ package BookPick.mvp.domain.user.service.passWord; +import BookPick.mvp.domain.user.dto.passWord.PassWordChangeReq; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.PasswordMismatchException; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.WrongCurrentPasswordException; +import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import springboot.kakao_boot_camp.domain.user.dto.passWord.PassWordChangeReq; -import springboot.kakao_boot_camp.domain.user.exception.PasswordMismatchException; -import springboot.kakao_boot_camp.domain.user.exception.UserNotFoundException; -import springboot.kakao_boot_camp.domain.user.exception.WrongCurrentPasswordException; -import springboot.kakao_boot_camp.domain.user.model.User; -import springboot.kakao_boot_camp.domain.user.repository.UserRepository; @Service @RequiredArgsConstructor @@ -34,12 +34,12 @@ public void PassWordChange(Long userId, PassWordChangeReq req) { } // 2) 입력받은 비밀번호와 기존 비밀번호 체크 - if (!passwordEncoder.matches(req.nowPassWord(), user.getPassWord())) { + if (!passwordEncoder.matches(req.nowPassWord(), user.getPassword())) { throw new WrongCurrentPasswordException(); } // 3) 기존 비밀번호를 입력받은 비밀번호로 변경 - user.setPassWord(passwordEncoder.encode(req.newPassWord())); + user.setPassword(passwordEncoder.encode(req.newPassWord())); } diff --git a/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java b/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java index ddc4db8..ba69444 100644 --- a/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java +++ b/src/main/java/BookPick/mvp/domain/user/util/AdminManager.java @@ -1,8 +1,8 @@ package BookPick.mvp.domain.user.util; +import BookPick.mvp.domain.auth.Roles; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; -import springboot.kakao_boot_camp.domain.user.enums.UserRole; import java.util.Collection; @@ -11,7 +11,7 @@ public class AdminManager { public boolean isAdmin(Collection authorities){ for (GrantedAuthority authority : authorities){ - if(authority.getAuthority().equals(UserRole.ROLE_ADMIN.name())){ + if(authority.getAuthority().equals(Roles.ROLE_ADMIN.name())){ return true; } } diff --git a/src/main/java/BookPick/mvp/global/service/S3Service.java b/src/main/java/BookPick/mvp/global/service/S3Service.java new file mode 100644 index 0000000..d886381 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/service/S3Service.java @@ -0,0 +1,30 @@ +//package BookPick.mvp.global.service; +// +// +//import lombok.RequiredArgsConstructor; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +//import java.time.Duration; +// +//@Service +//@RequiredArgsConstructor +//public class S3Service { +// +// // +// @Value("${spring.cloud.aws.s3.bucket}") //application.properties에 있는 버킷명 가져와서 아래 버킷 변수에 넣어달라는 어노테이션 +// private String bucket; +// private final S3Presigner s3Presigner; +// +// public String createPresignedUrl(String path) { +// var putObjectRequest = PutObjectRequest.builder() +// .bucket(bucket) // 올릴 버킷명 +// .key(path) // 경로 +// .build(); +// var preSignRequest = PutObjectPresignRequest.builder() +// .signatureDuration(Duration.ofMinutes(3)) //url 유효기간 +// .putObjectRequest(putObjectRequest) +// .build(); +// return s3Presigner.presignPutObject(preSignRequest).url().toString(); //presigned url Return +// } +// +//} From 1043f50aac53ebc0f1b6197cccd6738c6b412c0a Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 14 Nov 2025 22:41:16 +0900 Subject: [PATCH 124/291] =?UTF-8?q?feat=20:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=20=ED=83=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/global/config/SwaggerConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java b/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java index d2d5c47..3c11b34 100644 --- a/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java +++ b/src/main/java/BookPick/mvp/global/config/SwaggerConfig.java @@ -19,7 +19,8 @@ public OpenAPI openAPI() { .addTagsItem(new Tag().name("Reading Preference").description("유저 독서 취향 관련 API")) .addTagsItem(new Tag().name("Curation").description("큐레이션 관련 API")) .addTagsItem(new Tag().name("Book Search").description("책 검색 관련 API")) - .addTagsItem(new Tag().name("Auth").description("유저 인증 관련 API")); + .addTagsItem(new Tag().name("Auth").description("인증 관련 API")) + .addTagsItem(new Tag().name("User").description("유저 관련 API")); } private Info apiInfo() { From 2a23f6f2c8524199239625066b90fd9538f805da Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 14 Nov 2025 22:49:39 +0900 Subject: [PATCH 125/291] =?UTF-8?q?feat=20:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20url=20=EC=A4=91=EB=B3=B5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EB=AC=B8=EC=A0=9C=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=A6=AC=EC=86=8C=EC=8A=A4=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/user/controller/base/UserController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java index f4848b7..88f6f33 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -11,6 +11,7 @@ import BookPick.mvp.domain.user.util.AdminManager; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -28,6 +29,7 @@ public class UserController { // 유저 생성 @PostMapping + @Operation(summary = "유저 추가", description = "새로운 유저를 추가합니다", tags = {"User"}) public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, @RequestBody @Valid UserReq req) { if (adminManager.isAdmin(currentUser.getAuthorities())) { @@ -42,6 +44,7 @@ public ResponseEntity> createUser(@AuthenticationPrincipal // 유저 조회 @GetMapping + @Operation(summary = "유저 프로필 조회", description = "로그인한 사용자의 프로필을 조회합니다.", tags = {"User"}) public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { UserRes res = userService.userProfileGet(currentUser.getId()); @@ -50,8 +53,10 @@ public ResponseEntity> getUseProfile(@AuthenticationPrincip } + // 유저 수정 @PatchMapping + @Operation(summary = "유저 프로필 수정", description = "로그인한 사용자의 프로필을 수정합니다.", tags = {"User"}) public ResponseEntity> updateUserProfile(@AuthenticationPrincipal CustomUserDetails currentUser , @RequestBody @Valid UserReq req) { UserRes res = userService.userProfileUpdate(currentUser.getId(), req); @@ -62,6 +67,8 @@ public ResponseEntity> updateUserProfile(@AuthenticationPri // 유저 소프트 삭제 + @DeleteMapping("/soft") + @Operation(summary = "유저 프로필 소프트 삭제", description = "로그인한 사용자의 프로필을 임시 삭제합니다.", tags = {"User"}) public ResponseEntity> softDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); @@ -71,6 +78,8 @@ public ResponseEntity> softDeleteUser(@Authentica // 유저 하드 삭제 + @DeleteMapping("/hard") + @Operation(summary = "유저 프로필 하드 삭제", description = "로그인한 사용자의 프로필을 완전히 삭제합니다.", tags = {"User"}) public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { if (adminManager.isAdmin(currentUser.getAuthorities())) { userService.hardDeleteUserProfile(currentUser.getId()); From 9541f52e8c720f5fed87cc5af40e099b64936796 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 15 Nov 2025 00:13:29 +0900 Subject: [PATCH 126/291] =?UTF-8?q?feat=20:=20enums=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreference/{ => enums}/PrferenceErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/java/BookPick/mvp/domain/ReadingPreference/{ => enums}/PrferenceErrorCode.java (89%) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/PrferenceErrorCode.java similarity index 89% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/enums/PrferenceErrorCode.java index 6eaf14f..6d3efc4 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/PrferenceErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/PrferenceErrorCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference; +package BookPick.mvp.domain.ReadingPreference.enums; import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; From 42e400462d0ddf054f30b37a906834765855b036 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 15 Nov 2025 00:34:37 +0900 Subject: [PATCH 127/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/enums/AuthErrorCode.java | 23 +++++++ .../auth/exception/NotAuthenticateUser.java | 13 ++++ .../draft/CurationDraftController.java | 64 +++++++++++++++++++ .../domain/curation/dto/base/CurationReq.java | 19 ++++++ .../domain/curation/dto/base/CurationRes.java | 47 ++++++++++++++ .../dto/base/create/CurationCreateReq.java | 6 +- .../dto/base/create/{Req => ETC}/BookDto.java | 2 +- .../create/{Req => ETC}/RecommendDto.java | 2 +- .../create/{Req => ETC}/ThumbnailDto.java | 2 +- .../dto/base/delete/CurationDeleteReq.java | 4 -- .../dto/base/update/CurationUpdateReq.java | 6 +- .../mvp/domain/curation/entity/Curation.java | 35 +++++++++- .../curation/enums/CurationSuccessCode.java | 16 +++++ .../service/draft/CurationDraftService.java | 31 +++++++++ 14 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java rename src/main/java/BookPick/mvp/domain/curation/dto/base/create/{Req => ETC}/BookDto.java (62%) rename src/main/java/BookPick/mvp/domain/curation/dto/base/create/{Req => ETC}/RecommendDto.java (72%) rename src/main/java/BookPick/mvp/domain/curation/dto/base/create/{Req => ETC}/ThumbnailDto.java (62%) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java diff --git a/src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java b/src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java new file mode 100644 index 0000000..3ebb990 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java @@ -0,0 +1,23 @@ +package BookPick.mvp.domain.auth.enums; + +import BookPick.mvp.global.enums.ErrorCodeInterface; +import jakarta.persistence.GeneratedValue; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum AuthErrorCode implements ErrorCodeInterface { + // -- Auth + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청 인자입니다."), // 400 회원 가입 + DUPLICATE_EMAIL(HttpStatus.CONFLICT, "중복된 이메일 입니다."), // 409 회원 가입 + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘 못 되었습니다."), // 401 로그인 + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."); + + private final HttpStatus status; + private final String message; + + +} + diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java b/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java new file mode 100644 index 0000000..f18aa4f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.auth.exception; + + +import BookPick.mvp.domain.auth.enums.AuthErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class NotAuthenticateUser extends BusinessException { + public NotAuthenticateUser() { + super(AuthErrorCode.UNAUTHORIZED); + } + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java new file mode 100644 index 0000000..be3cd2c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java @@ -0,0 +1,64 @@ +package BookPick.mvp.domain.curation.controller.draft; + +import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.CurationReq; +import BookPick.mvp.domain.curation.dto.base.CurationRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.enums.CurationSuccessCode; +import BookPick.mvp.domain.curation.service.draft.CurationDraftService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/curation/draft") +@RequiredArgsConstructor +public class CurationDraftController { + + private final CurationDraftService curationDraftService; + + // 1. 임시 저장 컨트롤러 + // 확장성 : 임시저장, 드래프트 공유 및 유효기간 관리 등 + @PostMapping + @Operation(summary = "게시글 임시 저장", description = "유저가 작성한 큐레이션을 임시저장합니다", tags = {"Curation"}) + public ResponseEntity> saveDraft(@AuthenticationPrincipal CustomUserDetails currentUser, @RequestBody @Valid CurationReq req) { + + + // 1) 로그인 만료 검사 + if (currentUser == null) { + throw new NotAuthenticateUser(); + } + + CurationRes res = curationDraftService.draftSave(currentUser.getId(), req); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(CurationSuccessCode.CREATE_DRAFTED_CURATION_SUCCESS, res)); + } + + + // 2. 임시 저장 조회 컨트롤러 +// @Operation(summary = "임시저장 단건 조회", description = "작성자 임시저장 큐레이션 단건 조회", tags = {"Curation"}) +// @GetMapping("/{curationId}") +// public ResponseEntity> getCuration( +// @PathVariable Long curationId, +// HttpServletRequest req) { +// +// // 1) 로그인 만료 검사 +// if (currentUser == null) { +// throw new NotAuthenticateUser(); +// } +// CurationGetRes res = curationService.findCuration(curationId, req); +// return ResponseEntity.ok() +// .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); +// } + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java new file mode 100644 index 0000000..163b1c7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.curation.dto.base; + +import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; + +// 메인 요청 DTO +public record CurationReq( + String title, + ThumbnailDto thumbnail, + BookDto book, + String review, + RecommendDto recommend +) { +} + + + + diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java new file mode 100644 index 0000000..8712016 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java @@ -0,0 +1,47 @@ +// CurationGetRes.java +package BookPick.mvp.domain.curation.dto.base; + +import BookPick.mvp.domain.curation.entity.Curation; + +import java.time.LocalDateTime; +import java.util.List; + +public record CurationRes( + Long id, + Long userId, + String title, + ThumbnailInfo thumbnail, + BookInfo book, + String review, + RecommendInfo recommend, + Boolean isDeleted, + LocalDateTime createdAt, + LocalDateTime updatedAt, + LocalDateTime deletedAt +) { + public static CurationRes from(Curation curation) { + return new CurationRes( + curation.getId(), + curation.getUser().getId(), + curation.getTitle(), + new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), + new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), + curation.getReview(), + new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), + curation.isDraft(), + curation.getCreatedAt(), + curation.getUpdatedAt(), + curation.getDeletedAt() + ); + } + + public record ThumbnailInfo(String imageUrl, String imageColor) { + } + + public record BookInfo(String title, String author, String isbn) { + } + + public record RecommendInfo(List moods, List genres, + List keywords, List styles) { + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java index 46594df..6599ada 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java @@ -1,8 +1,8 @@ package BookPick.mvp.domain.curation.dto.base.create; -import BookPick.mvp.domain.curation.dto.base.create.Req.BookDto; -import BookPick.mvp.domain.curation.dto.base.create.Req.RecommendDto; -import BookPick.mvp.domain.curation.dto.base.create.Req.ThumbnailDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; // 메인 요청 DTO public record CurationCreateReq( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/BookDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java similarity index 62% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/BookDto.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java index e6d795c..585494f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/BookDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.base.create.Req; +package BookPick.mvp.domain.curation.dto.base.create.ETC; // 책 정보 public record BookDto( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/RecommendDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/RecommendDto.java similarity index 72% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/RecommendDto.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/RecommendDto.java index f7b04c9..7ee7158 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/RecommendDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/RecommendDto.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.base.create.Req; +package BookPick.mvp.domain.curation.dto.base.create.ETC; import java.util.List; public record RecommendDto( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/ThumbnailDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/ThumbnailDto.java similarity index 62% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/ThumbnailDto.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/ThumbnailDto.java index 3ab3635..070b5f1 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/Req/ThumbnailDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/ThumbnailDto.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.base.create.Req; +package BookPick.mvp.domain.curation.dto.base.create.ETC; // 썸네일 정보 public record ThumbnailDto( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java deleted file mode 100644 index d4e5156..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteReq.java +++ /dev/null @@ -1,4 +0,0 @@ -package BookPick.mvp.domain.curation.dto.base.delete; - -public class CurationDeleteReq { -} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java index be16365..a75a667 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java @@ -1,9 +1,9 @@ // CurationUpdateReq.java package BookPick.mvp.domain.curation.dto.base.update; -import BookPick.mvp.domain.curation.dto.base.create.Req.BookDto; -import BookPick.mvp.domain.curation.dto.base.create.Req.RecommendDto; -import BookPick.mvp.domain.curation.dto.base.create.Req.ThumbnailDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; public record CurationUpdateReq( String title, diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index b04f83c..e8bb02b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -1,5 +1,7 @@ package BookPick.mvp.domain.curation.entity; +import BookPick.mvp.domain.curation.dto.base.CurationReq; +import BookPick.mvp.domain.curation.dto.base.CurationRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; @@ -14,6 +16,7 @@ @Builder @Entity @Getter +@Setter @EntityListeners(AuditingEntityListener.class) @Table(name = "curation") @AllArgsConstructor @@ -67,7 +70,6 @@ public class Curation { private List styles; - @Builder.Default @Column(name = "like_count") private Integer likeCount = 0; @@ -85,6 +87,8 @@ public class Curation { @Column(name = "popularity_score") private Integer popularityScore = 0; + @Column(name = "is_draft") + private boolean isDraft = false; @CreatedDate @Column(updatable = false) @@ -114,11 +118,40 @@ public void update(CurationUpdateReq req) { this.styles = req.recommend().styles(); } + public static Curation createDraft(User user, CurationReq req) { + Curation curation = Curation.from(user, req); + curation.setDraft(true); + + return curation; + } + + public void increaseViewCount() { this.viewCount++; updatePopularityScore(); // 인기도 재계산 } + public void updatePopularityScore() { this.popularityScore = (likeCount * 3) + (commentCount * 2) + (viewCount * 1); } + + + // 팩토리 메서드 + public static Curation from(User user, CurationReq req) { + return Curation.builder() + .user(user) + .title(req.title()) + .thumbnailUrl(req.thumbnail().imageUrl()) + .thumbnailColor(req.thumbnail().imageColor()) + .bookTitle(req.book().title()) + .bookAuthor(req.book().author()) + .bookIsbn(req.book().isbn()) + .review(req.review()) + .moods(req.recommend().moods()) + .genres(req.recommend().genres()) + .keywords(req.recommend().keywords()) + .styles(req.recommend().styles()) + .build(); + } + } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java new file mode 100644 index 0000000..dc87474 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java @@ -0,0 +1,16 @@ +package BookPick.mvp.domain.curation.enums; + +import BookPick.mvp.global.enums.SuccessCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum CurationSuccessCode implements SuccessCodeInterface { + + CREATE_DRAFTED_CURATION_SUCCESS(HttpStatus.CREATED, "큐레이션 임시저장에 성공했습니다"); + + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java new file mode 100644 index 0000000..ce8d108 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.curation.service.draft; + +import BookPick.mvp.domain.curation.dto.base.CurationReq; +import BookPick.mvp.domain.curation.dto.base.CurationRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.user.dto.base.UserReq; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CurationDraftService { + private final CurationRepository curationRepository; + private final UserRepository userRepository; + + public CurationRes draftSave(Long userId, CurationReq req){ + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + Curation draftedCuration = Curation.createDraft(user, req); + Curation saved = curationRepository.save(draftedCuration); + + return CurationRes.from(saved); + } + } + + From 5aac1f4a862131699cdfcbc0e5b08863013a5e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 16 Nov 2025 18:58:56 +0900 Subject: [PATCH 128/291] chore: meaningless commit --- .../mvp/domain/auth/enums/AuthErrorCode.java | 23 ------------------- .../auth/exception/NotAuthenticateUser.java | 4 +--- .../draft/CurationDraftController.java | 4 ---- .../mvp/global/api/ErrorCode/ErrorCode.java | 2 +- .../config/CorsConfig.java | 2 +- .../config/JwtConfig.java | 2 +- .../config/SecurityConfig.java | 4 ++-- 7 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java rename src/main/java/BookPick/mvp/{global => security}/config/CorsConfig.java (95%) rename src/main/java/BookPick/mvp/{global => security}/config/JwtConfig.java (95%) rename src/main/java/BookPick/mvp/{global => security}/config/SecurityConfig.java (97%) diff --git a/src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java b/src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java deleted file mode 100644 index 3ebb990..0000000 --- a/src/main/java/BookPick/mvp/domain/auth/enums/AuthErrorCode.java +++ /dev/null @@ -1,23 +0,0 @@ -package BookPick.mvp.domain.auth.enums; - -import BookPick.mvp.global.enums.ErrorCodeInterface; -import jakarta.persistence.GeneratedValue; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; - -@Getter -@AllArgsConstructor -public enum AuthErrorCode implements ErrorCodeInterface { - // -- Auth - INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청 인자입니다."), // 400 회원 가입 - DUPLICATE_EMAIL(HttpStatus.CONFLICT, "중복된 이메일 입니다."), // 409 회원 가입 - AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘 못 되었습니다."), // 401 로그인 - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."); - - private final HttpStatus status; - private final String message; - - -} - diff --git a/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java b/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java index f18aa4f..8f17a13 100644 --- a/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java +++ b/src/main/java/BookPick/mvp/domain/auth/exception/NotAuthenticateUser.java @@ -1,13 +1,11 @@ package BookPick.mvp.domain.auth.exception; - -import BookPick.mvp.domain.auth.enums.AuthErrorCode; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; public class NotAuthenticateUser extends BusinessException { public NotAuthenticateUser() { - super(AuthErrorCode.UNAUTHORIZED); + super(ErrorCode.UNAUTHORIZED); } } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java index be3cd2c..76012e3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java @@ -4,14 +4,10 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.CurationRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.enums.CurationSuccessCode; import BookPick.mvp.domain.curation.service.draft.CurationDraftService; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 92d804c..3316cd9 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -13,7 +13,7 @@ public enum ErrorCode implements ErrorCodeInterface { INVALID_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."), // 400 회원 가입 DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일 입니다."), // 409 회원 가입 AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 잘 못 되었습니다."), // 401 로그인 - + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."), Token_Expired(HttpStatus.UNAUTHORIZED, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), Invalid_Token_Type(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), diff --git a/src/main/java/BookPick/mvp/global/config/CorsConfig.java b/src/main/java/BookPick/mvp/security/config/CorsConfig.java similarity index 95% rename from src/main/java/BookPick/mvp/global/config/CorsConfig.java rename to src/main/java/BookPick/mvp/security/config/CorsConfig.java index 1a62f22..0bc4c45 100644 --- a/src/main/java/BookPick/mvp/global/config/CorsConfig.java +++ b/src/main/java/BookPick/mvp/security/config/CorsConfig.java @@ -1,4 +1,4 @@ -package BookPick.mvp.global.config; +package BookPick.mvp.security.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/BookPick/mvp/global/config/JwtConfig.java b/src/main/java/BookPick/mvp/security/config/JwtConfig.java similarity index 95% rename from src/main/java/BookPick/mvp/global/config/JwtConfig.java rename to src/main/java/BookPick/mvp/security/config/JwtConfig.java index fb9f5d1..e2176a8 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtConfig.java +++ b/src/main/java/BookPick/mvp/security/config/JwtConfig.java @@ -1,4 +1,4 @@ -package BookPick.mvp.global.config; +package BookPick.mvp.security.config; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; diff --git a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java similarity index 97% rename from src/main/java/BookPick/mvp/global/config/SecurityConfig.java rename to src/main/java/BookPick/mvp/security/config/SecurityConfig.java index af656f8..a917e00 100644 --- a/src/main/java/BookPick/mvp/global/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java @@ -1,10 +1,10 @@ -package BookPick.mvp.global.config; +package BookPick.mvp.security.config; +import BookPick.mvp.global.config.JwtFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; From 3ced6ed010d00bfdf9f6872b51ebfb0ff79816d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 16 Nov 2025 19:43:44 +0900 Subject: [PATCH 129/291] =?UTF-8?q?feat=20:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 39 -------- .../auth/controller/LoginController.java | 47 ++++++++++ .../auth/controller/LogoutController.java | 31 +++++++ .../auth/controller/SignUpController.java | 33 +++++++ .../controller/TokenRefreshController.java | 49 ++++++++++ .../mvp/domain/auth/dto/Create/AuthDtos.java | 70 -------------- .../mvp/domain/auth/dto/LoginReq.java | 12 +++ .../mvp/domain/auth/dto/LoginRes.java | 41 ++++++++ .../BookPick/mvp/domain/auth/dto/SignReq.java | 12 +++ .../BookPick/mvp/domain/auth/dto/SignRes.java | 9 ++ .../mvp/domain/auth/service/AuthService.java | 93 ------------------- .../mvp/domain/auth/service/LoginService.java | 48 ++++++++++ .../domain/auth/service/LogoutService.java | 93 +++++++++++++++++++ .../domain/auth/service/SignUpService.java | 64 +++++++++++++ .../auth/service/TokenRefreshService.java | 57 ++++++++++++ .../Manager/login/jwt/JwtAuthManager.java | 26 ++++++ .../login/jwt/RefreshTokenCookieManager.java | 52 +++++++++++ .../login/jwt/TokenBlacklistManager.java | 37 ++++++++ .../util/Manager/signup/SignUpManager.java | 18 ++++ .../global/api/SuccessCode/SuccessCode.java | 2 + .../BookPick/mvp/global/util/JwtUtil.java | 82 +++++++++++++--- src/main/resources/application.yml | 5 +- 22 files changed, 705 insertions(+), 215 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java delete mode 100644 src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/dto/LoginReq.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/dto/LoginRes.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/dto/SignReq.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/dto/SignRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/auth/service/AuthService.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/LoginService.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/RefreshTokenCookieManager.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/TokenBlacklistManager.java create mode 100644 src/main/java/BookPick/mvp/domain/auth/util/Manager/signup/SignUpManager.java diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java b/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java deleted file mode 100644 index cc4b4f9..0000000 --- a/src/main/java/BookPick/mvp/domain/auth/controller/AuthController.java +++ /dev/null @@ -1,39 +0,0 @@ -package BookPick.mvp.domain.auth.controller; - -import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.domain.auth.dto.Create.AuthDtos.*; -import BookPick.mvp.domain.auth.service.AuthService; -import BookPick.mvp.global.api.SuccessCode.SuccessCode; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import io.swagger.v3.oas.annotations.Operation; - -@RestController -@RequestMapping("/api/v1/auth") -@RequiredArgsConstructor -public class AuthController { - - private final AuthService authService; - - @Operation(summary = "회원가입", description = "사용자 회원가입", tags = {"Auth"}) - @PostMapping("/signup") - public ResponseEntity> signUp(@Valid @RequestBody SignReq req) { - SignRes signRes = authService.signUp(req); - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS, signRes)); - } - - @Operation(summary = "로그인", description = "사용자 로그인", tags = {"Auth"}) - @PostMapping("/login") - public ResponseEntity> login(@Valid @RequestBody LoginReq req, HttpServletResponse res) { - LoginRes loginRes = authService.login(req, res); - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, loginRes)); - } -} - diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java b/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java new file mode 100644 index 0000000..69a6066 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java @@ -0,0 +1,47 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.domain.auth.service.LoginService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import BookPick.mvp.domain.auth.dto.LoginReq; +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.RefreshTokenCookieManager; + +@RequestMapping("/api/v1/auth/login") +@RequiredArgsConstructor +@RestController +public class LoginController { + + private final LoginService loginService; + private final RefreshTokenCookieManager refreshTokenCookieManager; + + + @PostMapping + public ResponseEntity> login( + @RequestBody @Valid LoginReq req, + HttpServletRequest servletRequest, + HttpServletResponse servletResponse) { + + // 1. 액세스 토큰 포함 DTO 생성 + LoginRes res = loginService.login(req, servletRequest); + + // 2. 리프레시 토큰 포함 + refreshTokenCookieManager.addRefreshTokenCookie(servletResponse, res.refreshToken()); + + // 3. Login response Dto에서 Refresh token 빼기 + LoginRes result = LoginRes.fromWithoutRefreshToken(res, res.accessToken()); + return ResponseEntity + .ok(ApiResponse.success(SuccessCode.LOGIN_SUCCESS, result)); + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java b/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java new file mode 100644 index 0000000..ac0c357 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import BookPick.mvp.domain.auth.service.LogoutService; +import BookPick.mvp.domain.auth.service.CustomUserDetails; + +@RestController +@RequestMapping("/api/v1/auth/logout") +@RequiredArgsConstructor +public class LogoutController { + + private final LogoutService logoutService; + + @PostMapping + public ResponseEntity> logout(HttpServletRequest request, HttpServletResponse response, + @AuthenticationPrincipal CustomUserDetails currentUser) { + + logoutService.logout(currentUser, request, response); + + return ResponseEntity.ok(ApiResponse.success(SuccessCode.LOGOUT_SUCCESS, null)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java b/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java new file mode 100644 index 0000000..187b4af --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.auth.controller; + + +import BookPick.mvp.domain.auth.dto.SignReq; +import BookPick.mvp.domain.auth.dto.SignRes; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import BookPick.mvp.domain.auth.service.SignUpService; + +@RequiredArgsConstructor +@RestController // v1, v2 같은 버전은 추후 버전 관리를 위해 필요한 것인데 해당 프로젝트는 학습용 이므로 추후에 유지 보수 예정 X -> 따라서 버전 명 명시 안할 예정 +@RequestMapping("/api/v1/auth/signup") +public class SignUpController { + private final SignUpService authService; + + @PostMapping + public ResponseEntity> signUp(@RequestBody @Valid SignReq req, HttpServletResponse servletRes) { + SignRes res = authService.signUp(req); //data 얻기 + + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.success(SuccessCode.REGISTER_SUCCESS, res)); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java b/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java new file mode 100644 index 0000000..f1208bc --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java @@ -0,0 +1,49 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.service.TokenRefreshService; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.RefreshTokenCookieManager; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import BookPick.mvp.domain.auth.service.CustomUserDetails; + +@RestController +@RequestMapping("/api/v1/auth/token") +@RequiredArgsConstructor +public class TokenRefreshController { + + private final RefreshTokenCookieManager refreshTokenCookieManager; + private final TokenRefreshService tokenRefreshService; + + /** + * 🔄 Refresh Token을 이용해 Access Token 재발급 + */ + @PostMapping("/refresh") + public ResponseEntity> refreshAccessToken( + HttpServletRequest request, + HttpServletResponse response, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + + // 1️⃣ 쿠키에서 refresh token 추출 + String refreshToken = refreshTokenCookieManager.getRefreshTokenFromCookie(request); + + // 2️⃣ 새 access + refresh token 생성 + LoginRes newTokens = tokenRefreshService.refreshTokens(currentUser, refreshToken); + + // 3️⃣ 새 refresh token을 쿠키에 다시 설정 + refreshTokenCookieManager.addRefreshTokenCookie(response, newTokens.refreshToken()); + + // 4️⃣ refresh token은 response에 포함하지 않음 + LoginRes result = LoginRes.fromWithoutRefreshToken(newTokens, newTokens.accessToken()); + return ResponseEntity.ok(ApiResponse.success(SuccessCode.TOKEN_REFERSH_SUCCESS, result)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java b/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java deleted file mode 100644 index 95244ea..0000000 --- a/src/main/java/BookPick/mvp/domain/auth/dto/Create/AuthDtos.java +++ /dev/null @@ -1,70 +0,0 @@ -package BookPick.mvp.domain.auth.dto.Create; - - -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; -import jakarta.validation.constraints.Email; - -public class AuthDtos { - - // -- SignUp -- - public record SignReq( - @NotBlank @Email String email, - @Size(min = 8, max = 72) String password - ) { - } - - public record SignRes( - long userId - ) { - public static SignRes from(long userId) { - return new SignRes(userId); - } - } - - // -- Login -- - public record LoginReq( - @NotBlank @Email String email, - @Size(min = 8, max = 72) String password - ) { - } - - public record LoginRes( - long userId, - String email, - String nickname, - String bio, - String profileImageUrl, - boolean isFirstLogin, - - String accessToken - ) { - - public static LoginRes from(CustomUserDetails customUserDetails, String accessToken) { - return new LoginRes( - customUserDetails.getId(), - customUserDetails.getUsername(), // username = email - customUserDetails.getNickname(), - customUserDetails.getBio(), - customUserDetails.getProfileImageUrl(), - customUserDetails.isFirstLogin(), - accessToken - ); - } - - - // 3. 로그아 - public record LogoutReq( - @NotBlank String refreshToken - ) { - } - - // 3. 토큰 재발급 요청 - public record RefreshToken( - @NotBlank String refreshToken - ) { - } - } -} - diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/LoginReq.java b/src/main/java/BookPick/mvp/domain/auth/dto/LoginReq.java new file mode 100644 index 0000000..39a8260 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/dto/LoginReq.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +// -- Login -- +public record LoginReq( + @NotBlank @Email String email, + @Size(min = 8, max = 72) String password +) { +} diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/LoginRes.java b/src/main/java/BookPick/mvp/domain/auth/dto/LoginRes.java new file mode 100644 index 0000000..f9deb3e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/dto/LoginRes.java @@ -0,0 +1,41 @@ +package BookPick.mvp.domain.auth.dto; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; + +public record LoginRes( + long userId, + String email, + String nickname, + String bio, + String profileImageUrl, + boolean isFirstLogin, + + String accessToken, + String refreshToken + +) { + public static LoginRes from(CustomUserDetails customUserDetails, String accessToken, String refreshToken) { + return new LoginRes( + customUserDetails.getId(), + customUserDetails.getUsername(), // username = email + customUserDetails.getNickname(), + customUserDetails.getBio(), + customUserDetails.getProfileImageUrl(), + customUserDetails.isFirstLogin(), + accessToken, + refreshToken + ); + } + public static LoginRes fromWithoutRefreshToken(LoginRes res ,String accessToken) { + return new LoginRes( + res.userId(), + res.email(), + res.nickname(), + res.bio(), + res.profileImageUrl(), + res.isFirstLogin(), + accessToken, + null + ); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/SignReq.java b/src/main/java/BookPick/mvp/domain/auth/dto/SignReq.java new file mode 100644 index 0000000..5044d3a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/dto/SignReq.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +// -- SignUp -- +public record SignReq( + @NotBlank @Email String email, + @Size(min = 8, max = 72) String password +) { +} diff --git a/src/main/java/BookPick/mvp/domain/auth/dto/SignRes.java b/src/main/java/BookPick/mvp/domain/auth/dto/SignRes.java new file mode 100644 index 0000000..1237d61 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/dto/SignRes.java @@ -0,0 +1,9 @@ +package BookPick.mvp.domain.auth.dto; + +public record SignRes( + long userId +) { + public static SignRes from(long userId) { + return new SignRes(userId); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java b/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java deleted file mode 100644 index 8027f33..0000000 --- a/src/main/java/BookPick/mvp/domain/auth/service/AuthService.java +++ /dev/null @@ -1,93 +0,0 @@ -package BookPick.mvp.domain.auth.service; - -import BookPick.mvp.domain.auth.Roles; -import BookPick.mvp.domain.auth.dto.Create.AuthDtos.*; -import BookPick.mvp.domain.auth.exception.DuplicateEmailException; -import BookPick.mvp.domain.auth.exception.InvalidLoginException; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; -import BookPick.mvp.domain.user.repository.UserRepository; -import BookPick.mvp.global.util.JwtUtil; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Slf4j -@Service -@Transactional -@RequiredArgsConstructor -public class AuthService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JwtUtil jwtUtil; - private final AuthenticationManagerBuilder authenticationManagerBuilder; - - - @Transactional - public SignRes signUp(SignReq req) { - - // 1. 중복 확인 - if (userRepository.existsAllByEmail((req.email()))) { - throw new DuplicateEmailException(); - } - - // 2. 신규 유저 생성 - User user = User.builder() - .email(req.email()) - .password(passwordEncoder.encode(req.password())) - .role(Roles.ROLE_USER) - .build(); - - // 3. DB 저장 - User saved = userRepository.save(user); - - // 4. 응답 - return SignRes.from(saved.getId()); - } - - - // access Token O, refresh X - @Transactional - public LoginRes login(LoginReq req, HttpServletResponse res) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.email(), req.password()); // 임시로 이메일과 아이디가 담김 - - try { - Authentication auth = authenticationManagerBuilder.getObject().authenticate(authToken); // -> UserDetailsService.loadUserByUsername(), 비밀 번호 및 아이디 검증 - - firstLoginCheck(req.email()); - - String accessToken = jwtUtil.createAccessToken(auth); // Access O - String refreshToken = jwtUtil.createRefreshToken(auth); // Refresh X - - - CustomUserDetails customUserDetails = (CustomUserDetails) auth.getPrincipal(); - - return LoginRes.from(customUserDetails, "Bearer " + accessToken); - - } catch (BadCredentialsException | UsernameNotFoundException e) { - throw new InvalidLoginException(); - } catch (AuthenticationException e) { - throw new InvalidLoginException(); - } - } - - @Transactional - void firstLoginCheck(String email) { - User user = userRepository.findByEmail(email) - .orElseThrow(UserNotFoundException::new); - - if (user.isFirstLogin()) { - user.isNotFirstLogin(); // 더티체킹 - } - } - -} diff --git a/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java b/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java new file mode 100644 index 0000000..f8e5f72 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java @@ -0,0 +1,48 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.dto.LoginReq; +import BookPick.mvp.domain.auth.dto.LoginRes; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import BookPick.mvp.domain.auth.exception.InvalidLoginException; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.JwtAuthManager; + + +@Service +@RequiredArgsConstructor +public class LoginService { + private final JwtAuthManager jwtAuthManager; + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + + // 1. jwt 기반 로그인 + public LoginRes login(LoginReq req, HttpServletRequest servletReq) throws RuntimeException { + + // 1. 토큰 생성 + var token = new UsernamePasswordAuthenticationToken(req.email(), req.password()); + + // 2. 로그인 검증 + try { + var auth = authenticationManagerBuilder.getObject().authenticate(token); + + + JwtAuthManager.TokenPair tokenPair = jwtAuthManager.createTokens(auth); + + + return LoginRes.from((CustomUserDetails) auth.getPrincipal(), tokenPair.accessToken(), tokenPair.refreshToken()); + + } catch (BadCredentialsException | UsernameNotFoundException e) { + throw new InvalidLoginException(); + } catch (AuthenticationException e) { + throw new InvalidLoginException(); + } + } + +} + diff --git a/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java b/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java new file mode 100644 index 0000000..bbb3ee1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java @@ -0,0 +1,93 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.TokenBlacklistManager; +import BookPick.mvp.global.util.JwtUtil; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.stereotype.Service; +import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; + +import java.time.Instant; + +@Slf4j +@Service +@RequiredArgsConstructor +public class LogoutService { + + private final JwtUtil jwtUtil; + private final TokenBlacklistManager tokenBlacklistManager; + + + public void logout(CustomUserDetails currentUser, HttpServletRequest request, HttpServletResponse response) { + + + // 1. 인증 상태 검증 + if (currentUser == null) { + throw new NotAuthenticateUser(); + } + + + // 2. 쿠키 검증 + Cookie[] cookies = request.getCookies(); + if (cookies == null) return; + + + // 3. 리프레시 토큰 검증 + for (Cookie cookie : cookies) { + if ("refreshToken".equals(cookie.getName())) { + + // 3.1 리프레시 토큰 획득 + String refreshToken = cookie.getValue(); + if (refreshToken == null || refreshToken.isEmpty()) return; + + + // 3.2 클레임 토큰 획득 + Claims claims; + try { + claims = jwtUtil.extractRefreshToken(refreshToken); + } catch (Exception e) { + throw new JwtTokenExpiredException(); + } + + // 3.3 토큰의 userId와 현재 인증된 user 비교 + Long tokenUserId; + try { + tokenUserId = claims.get("userId", Number.class).longValue(); + } catch (Exception e) { + throw new AccessDeniedException("Invalid token structure"); + } + if (!tokenUserId.equals(currentUser.getId())) { + throw new AccessDeniedException("Token does not belong to current user"); + } + + + // 3.4 jti 및 만료시간 계산 후 블랙리스트 추가 + String jti = claims.getId(); + long expMillis = claims.getExpiration().getTime() - System.currentTimeMillis(); + if (jti != null && expMillis > 0) { + tokenBlacklistManager.add(jti, Instant.now().plusMillis(expMillis)); + } + + + // 3.5 클라이언트 쿠키 제거 + Cookie del = new Cookie("refreshToken", null); + del.setHttpOnly(true); + del.setSecure(true); + del.setPath("/"); + del.setMaxAge(0); + response.addCookie(del); + + + // 3.6 쿠키 더 순회하지 않고 종료 + break; + } + } + } +} + diff --git a/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java b/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java new file mode 100644 index 0000000..ae32244 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java @@ -0,0 +1,64 @@ +package BookPick.mvp.domain.auth.service; + + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.dto.SignReq; +import BookPick.mvp.domain.auth.dto.SignRes; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.enums.UserRole; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; +import BookPick.mvp.domain.auth.util.Manager.signup.SignUpManager; + +import java.time.LocalDateTime; + + +@Service +@RequiredArgsConstructor +public class SignUpService { //Dto로 컨트롤러에서 받음 + + private final UserRepository userRepo; + private final PasswordEncoder passwordEncoder; + private final SignUpManager signUpManager; + + public SignRes signUp(SignReq req) throws RuntimeException { + + + // 1. 중복 확인 + if (userRepo.existsByEmail(req.email())) { + throw new DuplicateEmailException(); + } + + + Roles userRole = Roles.ROLE_USER; + + if (signUpManager.isAdmin( req.email())){ + userRole=Roles.ROLE_ADMIN; + } + + User user = User.builder() + .email(req.email()) + .password(passwordEncoder.encode(req.password())) + .nickname(null) + .profileImageUrl(null) + .role(userRole) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + + // 3. DB에 저장 + User savedUSer = userRepo.save(user); + + // 4. Sign Response DTO 반환 + return new SignRes(savedUSer.getId()); + + } + + + +} + diff --git a/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java b/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java new file mode 100644 index 0000000..0f130ea --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java @@ -0,0 +1,57 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.JwtAuthManager; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.TokenBlacklistManager; +import BookPick.mvp.global.util.JwtUtil; +import io.jsonwebtoken.Claims; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TokenRefreshService { + + private final JwtAuthManager jwtAuthManager; + private final TokenBlacklistManager tokenBlacklistManager; + private final JwtUtil jwtUtil; + + public LoginRes refreshTokens(CustomUserDetails customUserDetails, String refreshToken) { + + if (refreshToken == null || refreshToken.isBlank()) { + throw new RuntimeException("리프레시 토큰이 없습니다."); + } + + // 🔒 블랙리스트 확인 + if (tokenBlacklistManager.isBlacklisted(refreshToken)) { + throw new RuntimeException("유효하지 않은 리프레시 토큰입니다."); + } + + // ✅ 토큰 유효성 검증 + if (!jwtUtil.validateToken(refreshToken, false)) { + throw new RuntimeException("리프레시 토큰이 만료되었거나 유효하지 않습니다."); + } + + // Claims 추출 + Claims claims = jwtUtil.extractRefreshToken(refreshToken); + Long userId = claims.get("userId", Long.class); + + if (!customUserDetails.getId().equals(userId)) { + throw new RuntimeException("토큰 사용자 정보와 일치하지 않습니다."); + } + + // Authentication 객체 생성 + Authentication auth = new UsernamePasswordAuthenticationToken( + customUserDetails, + null, + customUserDetails.getAuthorities() + ); + + // 새 토큰 발급 + JwtAuthManager.TokenPair tokenPair = jwtAuthManager.createTokens(auth); + + return LoginRes.from(customUserDetails, tokenPair.accessToken(), tokenPair.refreshToken()); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java new file mode 100644 index 0000000..e49bbe7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java @@ -0,0 +1,26 @@ +package BookPick.mvp.domain.auth.util.Manager.login.jwt; + +import BookPick.mvp.global.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + + +@Component +@RequiredArgsConstructor +public class JwtAuthManager { + private final JwtUtil jwtUtil; + + public TokenPair createTokens(Authentication token){ + String accessToken = jwtUtil.createAccessToken(token); + String refreshToken = jwtUtil.createRefreshToken(token); + + return TokenPair.from(accessToken, refreshToken); + } + + public record TokenPair(String accessToken, String refreshToken) { + public static TokenPair from(String accessToken, String refreshToken){ + return new TokenPair(accessToken, refreshToken); + } + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/RefreshTokenCookieManager.java b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/RefreshTokenCookieManager.java new file mode 100644 index 0000000..2c6763f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/RefreshTokenCookieManager.java @@ -0,0 +1,52 @@ +package BookPick.mvp.domain.auth.util.Manager.login.jwt; + + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; + +@Component +public class RefreshTokenCookieManager { + + + private static final String COOKIE_NAME = "refreshToken"; + private static final int MAX_AGE_SECONDS = 60 * 60 * 24 * 7; // 7일 + + /** + * HttpOnly, Secure 쿠키로 리프레시 토큰 세팅 + */ + public void addRefreshTokenCookie(HttpServletResponse response, String refreshToken) { + Cookie refreshCookie = new Cookie(COOKIE_NAME, refreshToken); + refreshCookie.setHttpOnly(true); // JS에서 접근 불가 + refreshCookie.setSecure(true); // HTTPS 환경에서만 전송 + refreshCookie.setPath("/"); // 전체 경로에서 사용 + refreshCookie.setMaxAge(MAX_AGE_SECONDS); + response.addCookie(refreshCookie); + } + + /** + * 로그아웃 시 쿠키 제거 + */ + public void clearRefreshTokenCookie(HttpServletResponse response) { + Cookie refreshCookie = new Cookie(COOKIE_NAME, null); + refreshCookie.setHttpOnly(true); + refreshCookie.setSecure(true); + refreshCookie.setPath("/"); + refreshCookie.setMaxAge(0); // 즉시 삭제 + response.addCookie(refreshCookie); + } + + + public String getRefreshTokenFromCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) return null; + + for (Cookie cookie : cookies) { + if (COOKIE_NAME.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; // 없으면 null 반환 + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/TokenBlacklistManager.java b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/TokenBlacklistManager.java new file mode 100644 index 0000000..2dea09c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/TokenBlacklistManager.java @@ -0,0 +1,37 @@ +package BookPick.mvp.domain.auth.util.Manager.login.jwt; + +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class TokenBlacklistManager { + + // key: JWT ID (jti), value: 만료 시간 + private final Map blacklist = new ConcurrentHashMap<>(); + + // 블랙리스트 등록 + public void add(String jti, Instant expiration) { + blacklist.put(jti, expiration); + } + + // 블랙리스트 체크 + public boolean isBlacklisted(String jti) { + Instant exp = blacklist.get(jti); + if (exp == null) return false; + + // 이미 만료된 토큰은 제거 + if (Instant.now().isAfter(exp)) { + blacklist.remove(jti); + return false; + } + return true; + } + + // 테스트용: 전체 제거 + public void clear() { + blacklist.clear(); + } +} diff --git a/src/main/java/BookPick/mvp/domain/auth/util/Manager/signup/SignUpManager.java b/src/main/java/BookPick/mvp/domain/auth/util/Manager/signup/SignUpManager.java new file mode 100644 index 0000000..768a95d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/auth/util/Manager/signup/SignUpManager.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.auth.util.Manager.signup; + +import org.springframework.stereotype.Component; + +@Component +public class SignUpManager { + + private String amdinEmail="admin@admin.com"; + + public boolean isAdmin(Object object){ + if(object.equals(amdinEmail)){ + return true; + } + return false; + } + + +} diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java index 3d96838..8b721b8 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java @@ -11,6 +11,8 @@ public enum SuccessCode { // -- Auth -- REGISTER_SUCCESS(HttpStatus.OK, "회원가입을 성공하였습니다."), LOGIN_SUCCESS(HttpStatus.OK, "로그인에 성공했습니다"), + TOKEN_REFERSH_SUCCESS(HttpStatus.OK, "액세스 토큰 재발급에 성공하였습니다."), + LOGOUT_SUCCESS(HttpStatus.OK, "로그아웃에 성공했습니다"), // -- User -- GET_USERS_SUCCESS(HttpStatus.OK, "사용자 목록 조회를 성공하였습니다."), diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 9efe96b..1d81127 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -7,6 +7,7 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -22,14 +23,24 @@ @Component public class JwtUtil { // 1. 키발급 - final SecretKey key = - Keys.hmacShaKeyFor(Decoders.BASE64.decode( - "jwtpassword123jwtpassword123jwtpassword123jwtpassword123jwtpassword" - )); - // (추가) 토큰 수명 상수 - private static final long ACCESS_TTL_MS = 1000L * 60 * 60; // 1시간 - private static final long REFRESH_TTL_MS = 1000L * 60 * 60 * 24 * 14; // 14일 + private final SecretKey accessKey; + private final SecretKey refreshKey; + private final long accessTtl; + private final long refreshTtl; + + // 생성자 주입 + public JwtUtil( + @Value("${jwt.access.secret}") String accessSecret, + @Value("${jwt.refresh.secret}") String refreshSecret, + @Value("${jwt.access.expiration}") long accessTtl, + @Value("${jwt.refresh.expiration}") long refreshTtl + ) { + this.accessKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(accessSecret)); + this.refreshKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(refreshSecret)); + this.accessTtl = accessTtl; + this.refreshTtl = refreshTtl; + } // 2. JWT 생성 public String createAccessToken(Authentication auth) { @@ -46,7 +57,7 @@ public String createAccessToken(Authentication auth) { .claim("authorities", authorities) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // expiration : 만료 - .signWith(key) + .signWith(accessKey) .compact(); return jwt; } @@ -60,8 +71,8 @@ public String createRefreshToken(Authentication auth) { .claim("email", usr.getUsername()) .claim("typ", "refresh") // (권장) 토큰 타입 명시 .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + REFRESH_TTL_MS)) - .signWith(key) + .expiration(new Date(System.currentTimeMillis() + this.refreshTtl)) + .signWith(refreshKey) .compact(); } @@ -71,7 +82,7 @@ public Claims extractToken(String token) { try { Claims claims = Jwts.parser() - .verifyWith(key) + .verifyWith(accessKey) .build() .parseSignedClaims(token) .getPayload(); @@ -84,6 +95,55 @@ public Claims extractToken(String token) { throw new InvalidTokenTypeException(); } } + + // Access Token 파싱 + public Claims extractAccessToken(String token) { + return extractToken(token, accessKey); + } + + // Refresh Token 파싱 + public Claims extractRefreshToken(String token) { + return extractToken(token, refreshKey); + } + + // 공통 파싱 로직 + public static Claims extractToken(String token, SecretKey key) { + try { + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (ExpiredJwtException e) { + // 토큰 만료 예외를 커스텀 예외로 던짐 + throw new JwtTokenExpiredException(); + } catch (JwtException | IllegalArgumentException e) { + // 잘못된 토큰 예외를 커스텀 예외로 + System.out.println("JWT Parsing Error: " + e.getClass().getSimpleName() + " - " + e.getMessage()); + e.printStackTrace(); + + throw new InvalidTokenTypeException(); + } + + } + + // 토큰 유효성 검증 (Access / Refresh 구분) + public boolean validateToken(String token, boolean isAccessToken) { + try { + if (isAccessToken) { + extractAccessToken(token); + } else { + extractRefreshToken(token); + } + return true; // 예외가 안 나면 유효 + } catch (JwtTokenExpiredException e) { + System.out.println("토큰 만료: " + e.getMessage()); + return false; + } catch (InvalidTokenTypeException | JwtException e) { + System.out.println("유효하지 않은 토큰: " + e.getMessage()); + return false; + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 192d3b5..fad6a57 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,6 +24,7 @@ server: include-exception: true + logging: level: root: INFO @@ -33,10 +34,10 @@ logging: jwt: access: - secret: aGVsbG93b3JsZGhlbGxvd29ybGRoZWxsb3dvcmxk + secret: c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM expiration: 900000 # 15분 = 15 * 60 * 1000 refresh: - secret: cmVmcmVzaHNlY3JldGtleWZvcmp3dHRva2Vu + secret: d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 api : From 190c59969c24def6da95a012e8018f7ec93153fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 16 Nov 2025 19:48:24 +0900 Subject: [PATCH 130/291] =?UTF-8?q?docs=20:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20swagger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/auth/controller/LoginController.java | 2 ++ .../BookPick/mvp/domain/auth/controller/LogoutController.java | 2 ++ .../BookPick/mvp/domain/auth/controller/SignUpController.java | 2 ++ .../mvp/domain/auth/controller/TokenRefreshController.java | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java b/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java index 69a6066..408c590 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/LoginController.java @@ -3,6 +3,7 @@ import BookPick.mvp.domain.auth.service.LoginService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; @@ -26,6 +27,7 @@ public class LoginController { @PostMapping + @Operation(summary = "로그인", description = "로그인", tags = {"Auth"}) public ResponseEntity> login( @RequestBody @Valid LoginReq req, HttpServletRequest servletRequest, diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java b/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java index ac0c357..118ba05 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/LogoutController.java @@ -2,6 +2,7 @@ import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -21,6 +22,7 @@ public class LogoutController { private final LogoutService logoutService; @PostMapping + @Operation(summary = "로그아웃", description = "로그아웃", tags = {"Auth"}) public ResponseEntity> logout(HttpServletRequest request, HttpServletResponse response, @AuthenticationPrincipal CustomUserDetails currentUser) { diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java b/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java index 187b4af..6be591f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/SignUpController.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.auth.dto.SignRes; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -23,6 +24,7 @@ public class SignUpController { private final SignUpService authService; @PostMapping + @Operation(summary = "회원가입", description = "회원가입", tags = {"Auth"}) public ResponseEntity> signUp(@RequestBody @Valid SignReq req, HttpServletResponse servletRes) { SignRes res = authService.signUp(req); //data 얻기 diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java b/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java index f1208bc..a46c524 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.auth.util.Manager.login.jwt.RefreshTokenCookieManager; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -27,6 +28,7 @@ public class TokenRefreshController { * 🔄 Refresh Token을 이용해 Access Token 재발급 */ @PostMapping("/refresh") + @Operation(summary = "리프레시 토큰 재발급", description = "리프레시 토큰 재발급", tags = {"Auth"}) public ResponseEntity> refreshAccessToken( HttpServletRequest request, HttpServletResponse response, From bc5e831c3e4cdc785a3f82c50bab966d5c36a299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 16 Nov 2025 20:00:18 +0900 Subject: [PATCH 131/291] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=EB=A5=BC=20=ED=99=95=EC=9D=B8=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=ED=95=84=ED=84=B0=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/global/api/ErrorCode/ErrorCode.java | 5 ++ .../BookPick/mvp/global/config/JwtFilter.java | 73 +++++++++++++------ .../CustomAuthenticationEntryPoint.java | 37 ++++++++++ 3 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 src/main/java/BookPick/mvp/security/handler/CustomAuthenticationEntryPoint.java diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 3316cd9..9cabdaa 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -17,6 +17,11 @@ public enum ErrorCode implements ErrorCodeInterface { Token_Expired(HttpStatus.UNAUTHORIZED, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), Invalid_Token_Type(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), + // -- JWT -- + TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), + INVALID_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), + TOKEN_LOGOUTED(HttpStatus.UNAUTHORIZED, "이미 로그아웃된 토큰입니다."), + // -- User -- User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index 2d7d041..d4d4a51 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -1,7 +1,12 @@ package BookPick.mvp.global.config; +import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; +import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.TokenBlacklistManager; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.util.JwtUtil; +import BookPick.mvp.security.handler.CustomAuthenticationEntryPoint; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -10,6 +15,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -23,8 +29,11 @@ @RequiredArgsConstructor public class JwtFilter extends OncePerRequestFilter { + private static final String BEARER = "Bearer"; private final JwtUtil jwtUtil; + private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; + private final TokenBlacklistManager tokenBlacklistManager; @Override @@ -33,39 +42,59 @@ protected void doFilterInternal( ) throws ServletException, IOException { - // 이미 인증된 상태면 패스 + // 1. 이미 인증된 상태면 패스 if (SecurityContextHolder.getContext().getAuthentication() != null) { filterChain.doFilter(request, response); return; } + // 2. 토큰 확인 String token = resolveAccessToken(request); // 토큰있는지 문자열 체크 if (token == null) { // 토큰 없으면 그냥 통과 filterChain.doFilter(request, response); // 넘어가요 return; } - Claims claims = jwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) - - Long userId = claims.get("userId", Number.class).longValue(); - String email = claims.get("email").toString(); - - - var authorities = Arrays.stream( - claims.get("authorities").toString().split(",") - ).map(SimpleGrantedAuthority::new).toList(); - - - CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(userId, email, authorities); - - var auth = new UsernamePasswordAuthenticationToken( - customUserDetails, null, customUserDetails.getAuthorities() - ); - auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 추가 정보 저장 (어디서 접속했는지) - - SecurityContextHolder.getContext().setAuthentication(auth); - - + try { + + // 3. 토큰 까서 claims 획득 + Claims claims = jwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) + + + // 4. 블랙리스트 체크 + String jti = claims.getId(); + if (jti != null && tokenBlacklistManager.isBlacklisted(jti)) { + request.setAttribute("exception", ErrorCode.TOKEN_LOGOUTED); + customAuthenticationEntryPoint.commence(request, response, new AuthenticationException("Token logged out") { + }); + return; + } + + //5. 블랙리스트 아닐 시, claim 페이로드 값 반환 + Long userId = claims.get("userId", Number.class).longValue(); + String email = claims.get("email").toString(); + var authorities = Arrays.stream( + claims.get("authorities").toString().split(",") + ).map(SimpleGrantedAuthority::new).toList(); + + // 6. 인증 정보 저장 = SecurityContextHolder에 등록 + CustomUserDetails customUserDetails = CustomUserDetails.fromJwt(userId, email, authorities); + var auth = new UsernamePasswordAuthenticationToken( + customUserDetails, null, customUserDetails.getAuthorities()); + auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 추가 정보 저장 (어디서 접속했는지) + SecurityContextHolder.getContext().setAuthentication(auth); + + } catch (JwtTokenExpiredException e) { + request.setAttribute("exception", ErrorCode.TOKEN_EXPIRED); + customAuthenticationEntryPoint.commence(request, response, new AuthenticationException("Token expired") { + }); + return; + } catch (InvalidTokenTypeException e) { + request.setAttribute("exception", ErrorCode.INVALID_TOKEN_TYPE); + customAuthenticationEntryPoint.commence(request, response, new AuthenticationException("Invalid token") { + }); + return; + } filterChain.doFilter(request, response); } diff --git a/src/main/java/BookPick/mvp/security/handler/CustomAuthenticationEntryPoint.java b/src/main/java/BookPick/mvp/security/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..92e3b25 --- /dev/null +++ b/src/main/java/BookPick/mvp/security/handler/CustomAuthenticationEntryPoint.java @@ -0,0 +1,37 @@ +package BookPick.mvp.security.handler; + +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override +public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + + // request에 담긴 예외 코드 확인 + ErrorCode errorCode = (ErrorCode) request.getAttribute("exception"); + if (errorCode == null) { + errorCode = ErrorCode.UNAUTHORIZED; // 기본값 + } + + ApiResponse errorResponse = ApiResponse.error(errorCode); + + response.setStatus(errorCode.getStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); +} + +} From 0871ce14f3dd939570b4d72df51706078fcca49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 16 Nov 2025 20:53:46 +0900 Subject: [PATCH 132/291] =?UTF-8?q?fix=20:=20=EC=97=91=EC=84=B8=EC=8A=A4?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EC=A0=9C=EB=B0=9C=EA=B8=89=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/service/LogoutService.java | 7 ++++--- .../domain/auth/service/TokenRefreshService.java | 5 +++-- .../BookPick/mvp/global/config/JwtFilter.java | 2 +- .../java/BookPick/mvp/global/util/JwtUtil.java | 15 ++++++++------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java b/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java index bbb3ee1..5fa7870 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/LogoutService.java @@ -44,11 +44,15 @@ public void logout(CustomUserDetails currentUser, HttpServletRequest request, Ht // 3.1 리프레시 토큰 획득 String refreshToken = cookie.getValue(); + if (refreshToken == null || refreshToken.isEmpty()) return; + int a=1; // 3.2 클레임 토큰 획득 Claims claims; + int b=1; + try { claims = jwtUtil.extractRefreshToken(refreshToken); } catch (Exception e) { @@ -62,9 +66,6 @@ public void logout(CustomUserDetails currentUser, HttpServletRequest request, Ht } catch (Exception e) { throw new AccessDeniedException("Invalid token structure"); } - if (!tokenUserId.equals(currentUser.getId())) { - throw new AccessDeniedException("Token does not belong to current user"); - } // 3.4 jti 및 만료시간 계산 후 블랙리스트 추가 diff --git a/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java b/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java index 0f130ea..2696080 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/TokenRefreshService.java @@ -36,7 +36,8 @@ public LoginRes refreshTokens(CustomUserDetails customUserDetails, String refres // Claims 추출 Claims claims = jwtUtil.extractRefreshToken(refreshToken); - Long userId = claims.get("userId", Long.class); + Double userIdDouble = claims.get("userId", Double.class); + Long userId = userIdDouble.longValue(); // Double → Long if (!customUserDetails.getId().equals(userId)) { throw new RuntimeException("토큰 사용자 정보와 일치하지 않습니다."); @@ -52,6 +53,6 @@ public LoginRes refreshTokens(CustomUserDetails customUserDetails, String refres // 새 토큰 발급 JwtAuthManager.TokenPair tokenPair = jwtAuthManager.createTokens(auth); - return LoginRes.from(customUserDetails, tokenPair.accessToken(), tokenPair.refreshToken()); + return LoginRes.from(customUserDetails, tokenPair.accessToken(), tokenPair.refreshToken()); } } diff --git a/src/main/java/BookPick/mvp/global/config/JwtFilter.java b/src/main/java/BookPick/mvp/global/config/JwtFilter.java index d4d4a51..8087909 100644 --- a/src/main/java/BookPick/mvp/global/config/JwtFilter.java +++ b/src/main/java/BookPick/mvp/global/config/JwtFilter.java @@ -58,7 +58,7 @@ protected void doFilterInternal( try { // 3. 토큰 까서 claims 획득 - Claims claims = jwtUtil.extractToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) + Claims claims = jwtUtil.extractAccessToken(token); // 토큰 까기 (토큰 진위여부 검증 해당 메서드에서 진행) // 4. 블랙리스트 체크 diff --git a/src/main/java/BookPick/mvp/global/util/JwtUtil.java b/src/main/java/BookPick/mvp/global/util/JwtUtil.java index 1d81127..761c1fa 100644 --- a/src/main/java/BookPick/mvp/global/util/JwtUtil.java +++ b/src/main/java/BookPick/mvp/global/util/JwtUtil.java @@ -3,10 +3,10 @@ import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -16,10 +16,7 @@ import java.util.stream.Collectors; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; - - +@Slf4j @Component public class JwtUtil { // 1. 키발급 @@ -68,6 +65,7 @@ public String createRefreshToken(Authentication auth) { // refresh 토큰에는 최소 정보만: subject/email + typ 정도만 권장 return Jwts.builder() + .claim("userId", usr.getId()) // 여기 추가 .claim("email", usr.getUsername()) .claim("typ", "refresh") // (권장) 토큰 타입 명시 .issuedAt(new Date(System.currentTimeMillis())) @@ -109,11 +107,14 @@ public Claims extractRefreshToken(String token) { // 공통 파싱 로직 public static Claims extractToken(String token, SecretKey key) { try { - return Jwts.parser() + Claims claims = Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); + + return claims; + } catch (ExpiredJwtException e) { // 토큰 만료 예외를 커스텀 예외로 던짐 throw new JwtTokenExpiredException(); From 35ac37b5cc71fc86bb35a63183c0868b3c04fa4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 16 Nov 2025 20:57:07 +0900 Subject: [PATCH 133/291] =?UTF-8?q?fix=20:=20=EC=97=91=EC=84=B8=EC=8A=A4?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EC=A0=9C=EB=B0=9C=EA=B8=89=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/controller/TokenRefreshController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java b/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java index a46c524..5ace22a 100644 --- a/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java +++ b/src/main/java/BookPick/mvp/domain/auth/controller/TokenRefreshController.java @@ -28,7 +28,7 @@ public class TokenRefreshController { * 🔄 Refresh Token을 이용해 Access Token 재발급 */ @PostMapping("/refresh") - @Operation(summary = "리프레시 토큰 재발급", description = "리프레시 토큰 재발급", tags = {"Auth"}) + @Operation(summary = "액세스 토큰 재발급", description = "액세스 토큰 재발급", tags = {"Auth"}) public ResponseEntity> refreshAccessToken( HttpServletRequest request, HttpServletResponse response, From 632650ef3eb6a20eef09ce5c8f704899532e66c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 19 Nov 2025 22:36:26 +0900 Subject: [PATCH 134/291] =?UTF-8?q?feat=20:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=95=88=EB=90=9C=20=EC=9C=A0=EC=A0=80=20=EC=8B=9D=EB=B3=84?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20Current=20User=20null=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReadingPreferenceController.java | 5 +++++ .../util/Manager/login/jwt/JwtAuthManager.java | 2 ++ .../controller/base/CurationController.java | 6 ++++++ .../mvp/domain/user/util/CurrentUserCheck.java | 16 ++++++++++++++++ .../mvp/global/api/ErrorCode/ErrorCode.java | 7 +++---- 5 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java index 0e03050..5c9aac1 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceRes; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; @@ -22,6 +23,7 @@ public class ReadingPreferenceController { private final ReadingPreferenceService readingPreferenceService; + private final CurrentUserCheck currentUserCheck; @Operation(summary = "독서 취향 생성", description = "사용자의 독서 취향을 등록합니다", tags = {"Reading Preference"}) @PostMapping @@ -38,6 +40,9 @@ public ResponseEntity> create( @GetMapping public ResponseEntity> getDetails( @AuthenticationPrincipal CustomUserDetails currentUser) { + + currentUserCheck.isValidCurrentUser(currentUser); + ReadingPreferenceRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) diff --git a/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java index e49bbe7..c1c2b58 100644 --- a/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java +++ b/src/main/java/BookPick/mvp/domain/auth/util/Manager/login/jwt/JwtAuthManager.java @@ -11,6 +11,8 @@ public class JwtAuthManager { private final JwtUtil jwtUtil; + + // 1. 토큰 생성 public TokenPair createTokens(Authentication token){ String accessToken = jwtUtil.createAccessToken(token); String refreshToken = jwtUtil.createRefreshToken(token); diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 521fc36..250d3c9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -1,6 +1,7 @@ // CurationListController.java에 추가 package BookPick.mvp.domain.curation.controller.base; +import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; @@ -9,6 +10,7 @@ import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.servlet.http.HttpServletRequest; @@ -27,12 +29,16 @@ public class CurationController { private final CurationService curationService; + private final CurrentUserCheck currentUserCheck; @Operation(summary = "큐레이션 생성", description = "새 큐레이션을 생성합니다", tags = {"Curation"}) @PostMapping public ResponseEntity> create( @Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { + + currentUserCheck.isValidCurrentUser(currentUser); + CurationCreateRes res = curationService.create(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); diff --git a/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java b/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java new file mode 100644 index 0000000..6db5a71 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java @@ -0,0 +1,16 @@ +package BookPick.mvp.domain.user.util; + +import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +@Component +public class CurrentUserCheck { + + public void isValidCurrentUser(CustomUserDetails currentUser) { + if (currentUser == null) { + throw new NotAuthenticateUser(); + } + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 9cabdaa..10f7154 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -18,15 +18,14 @@ public enum ErrorCode implements ErrorCodeInterface { Invalid_Token_Type(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), // -- JWT -- - TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), - INVALID_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), - TOKEN_LOGOUTED(HttpStatus.UNAUTHORIZED, "이미 로그아웃된 토큰입니다."), + TOKEN_EXPIRED(HttpStatus.OK, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), + INVALID_TOKEN_TYPE(HttpStatus.OK, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), + TOKEN_LOGOUTED(HttpStatus.OK, "이미 로그아웃된 토큰입니다."), // -- User -- User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 - // -- Curation -- POST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), //404 From ed9fdea5ddf8089451a61cfb7c86048cebeaf0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 20 Nov 2025 20:00:49 +0900 Subject: [PATCH 135/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/book/dto/preference/BookDto.java | 1 + .../like/CurationLikeController.java | 42 +++++++++++ .../domain/curation/entity/CurationLike.java | 47 +++++++++++++ .../curation/enums/CurationSuccessCode.java | 9 ++- .../repository/CurationRepository.java | 4 ++ .../like/CurationLikeRepository.java | 11 +++ .../service/like/CurationLikeService.java | 70 +++++++++++++++++++ 7 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/entity/CurationLike.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java diff --git a/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java index 09a7928..a7920df 100644 --- a/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java +++ b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java @@ -7,6 +7,7 @@ public record BookDto( String title, + // Todo 1. author String 단수로 변경 필요 Set authors, String image, String isbn diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java new file mode 100644 index 0000000..17968f3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java @@ -0,0 +1,42 @@ +package BookPick.mvp.domain.curation.controller.like; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.enums.CurationSuccessCode; +import BookPick.mvp.domain.curation.service.like.CurationLikeService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/curations/like") +@RequiredArgsConstructor +public class CurationLikeController { + + private final CurationLikeService curationLikeService; + + @GetMapping("/{curationId}") + @Operation(summary = "큐레이션 좋아요", description = "큐레이션 좋아요 버튼을 누릅니다.", tags = {"Curation"}) + public ResponseEntity> likeOrUnlikeCuration(@AuthenticationPrincipal CustomUserDetails currentUser + , @PathVariable Long curationId) { + + boolean liked = curationLikeService.CurationLikeOrUnlike(currentUser.getId(), curationId); + + if (liked) { + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(CurationSuccessCode.POST_LIKE_SUCCESS, null)); + } else { + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(CurationSuccessCode.POST_DISLIKE_SUCCESS, null)); + } + } + + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/CurationLike.java b/src/main/java/BookPick/mvp/domain/curation/entity/CurationLike.java new file mode 100644 index 0000000..3afec5f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/entity/CurationLike.java @@ -0,0 +1,47 @@ +package BookPick.mvp.domain.curation.entity; + +import BookPick.mvp.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Builder +@Table(name = "curation_like") +@AllArgsConstructor +@EntityListeners(AuditingEntityListener.class) + +public class CurationLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "curation_id") + private Curation curation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "created_at") + @CreatedDate + private LocalDateTime createdAt; + + public CurationLike() { + + } + + + // 좋아요는 수정 시각이 필요가 없음 + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java index dc87474..270f82e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java @@ -9,7 +9,14 @@ @Getter public enum CurationSuccessCode implements SuccessCodeInterface { - CREATE_DRAFTED_CURATION_SUCCESS(HttpStatus.CREATED, "큐레이션 임시저장에 성공했습니다"); + + // 1. 임시저장 + CREATE_DRAFTED_CURATION_SUCCESS(HttpStatus.CREATED, "큐레이션 임시저장에 성공했습니다"), + + // -- 2. 좋아요 -- + POST_LIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요를 성공적으로 실행하였습니다."), + POST_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."); + private final HttpStatus status; private final String message; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 9d94962..cc5bfe6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -1,12 +1,14 @@ package BookPick.mvp.domain.curation.repository; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface CurationRepository extends JpaRepository { @@ -44,5 +46,7 @@ List findByRecommendation( @Param("keywords") List keywords, @Param("styles") List styles ); + + Optional findByUserIdAndId(Long userId, Long id); } diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java new file mode 100644 index 0000000..9f1e96c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.curation.repository.like; + +import BookPick.mvp.domain.curation.entity.CurationLike; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + + +public interface CurationLikeRepository extends JpaRepository { + Optional findByUserIdAndCurationId(Long userID, Long curationId); +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java new file mode 100644 index 0000000..11abbfb --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java @@ -0,0 +1,70 @@ +package BookPick.mvp.domain.curation.service.like; + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationLikeService { + private final UserRepository userRepository; + private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; + + + // 1. 포스트 좋아요 + @Transactional + public boolean CurationLikeOrUnlike(Long userId, Long curationId) { + + // 1. 포스트 아이디 얻기 + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + // 2. 유저 아이디 얻기 + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + + // 3. 해당 유저가 이미 좋아요를 눌렀는지 체크 + // 3.1 유저 좋아요 정보 가져오기 + Optional opt = curationLikeRepository.findByUserIdAndCurationId(userId, curationId); + + // 3.2 좋아요 정보가 없으면 생성 후 해당 게시글 좋아요 카운트 +1 + if (opt.isEmpty()) { + CurationLike curationLike = CurationLike.builder() + .user(user) + .curation(curation) + .build(); + + curationLikeRepository.save(curationLike); + + curation.setLikeCount(curation.getLikeCount() + 1); + + + return true; + } + + // 3.2 좋아요 정보가 있으면 삭제 후 해당 게시글 좋아요 카운트 -1 + else { + curationLikeRepository.delete(opt.get()); + + if (curation.getLikeCount() >= 0) { + curation.setLikeCount(curation.getLikeCount() - 1); + } + + return false; + } + } + +} + + From 6a7dc6ff10526916cdf9f6230d92b2d5d047220a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 20 Nov 2025 20:42:05 +0900 Subject: [PATCH 136/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=A2=8B=EC=95=84=EC=9A=94=ED=95=9C=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=B4=EC=A3=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=A2=8B=EC=95=84=EC=9A=94=EB=90=9C=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=94=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=98=ED=99=98=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/like/CurationLikeController.java | 8 ++++++++ .../controller/list/CurationListController.java | 4 +++- .../mvp/domain/curation/dto/base/CurationRes.java | 2 +- .../dto/base/get/list/CurationContentRes.java | 3 +++ .../BookPick/mvp/domain/curation/entity/Curation.java | 6 ++++-- .../BookPick/mvp/domain/curation/enums/SortType.java | 3 ++- .../repository/like/CurationLikeRepository.java | 4 ++++ .../curation/service/list/CurationListService.java | 2 +- .../util/list/Handler/CurationPageHandler.java | 4 ++-- .../curation/util/list/fetcher/CurationFetcher.java | 11 ++++++++++- 10 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java index 17968f3..b9ac602 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.enums.CurationSuccessCode; import BookPick.mvp.domain.curation.service.like.CurationLikeService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -21,12 +22,16 @@ public class CurationLikeController { private final CurationLikeService curationLikeService; + private final CurrentUserCheck currentUserCheck; + @GetMapping("/{curationId}") @Operation(summary = "큐레이션 좋아요", description = "큐레이션 좋아요 버튼을 누릅니다.", tags = {"Curation"}) public ResponseEntity> likeOrUnlikeCuration(@AuthenticationPrincipal CustomUserDetails currentUser , @PathVariable Long curationId) { + currentUserCheck.isValidCurrentUser(currentUser); + boolean liked = curationLikeService.CurationLikeOrUnlike(currentUser.getId(), curationId); if (liked) { @@ -39,4 +44,7 @@ public ResponseEntity> likeOrUnlikeCuration(@AuthenticationPri } + + + } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 6b966e0..464b86e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -25,7 +25,7 @@ public class CurationListController { @Operation(summary = "큐레이션 목록 조회", description = "최신순 / 인기순 / 사용자 취향 유사도 순", tags = {"Curation"}) @GetMapping - public ResponseEntity> CurationsGet( + public ResponseEntity> getCurations( @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size, @@ -47,6 +47,8 @@ public ResponseEntity> CurationsGet( } + + } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java index 8712016..9d85274 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java @@ -28,7 +28,7 @@ public static CurationRes from(Curation curation) { new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getReview(), new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), - curation.isDraft(), + curation.isDrafted(), curation.getCreatedAt(), curation.getUpdatedAt(), curation.getDeletedAt() diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 155ec00..058ab82 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -22,6 +22,7 @@ public record CurationContentRes( Integer similarity, String matched, Integer popularityScore, + boolean isDrafted, String createdAt ) { public static CurationContentRes from(Curation curation) { @@ -38,6 +39,7 @@ public static CurationContentRes from(Curation curation) { null, null, curation.getPopularityScore(), + curation.isDrafted(), curation.getCreatedAt().toString() ); } @@ -57,6 +59,7 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr getSimilarity(matchResult, preferenceInfo), matchResult.getMatched(), curation.getPopularityScore(), + curation.isDrafted(), curation.getCreatedAt().toString() ); } diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index e8bb02b..da42adc 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -88,7 +88,7 @@ public class Curation { private Integer popularityScore = 0; @Column(name = "is_draft") - private boolean isDraft = false; + private boolean isDrafted = false; @CreatedDate @Column(updatable = false) @@ -99,6 +99,8 @@ public class Curation { private LocalDateTime deletedAt; + //Todo 1. 소프트 델리트 구현 필요 + public Curation() { } @@ -120,7 +122,7 @@ public void update(CurationUpdateReq req) { public static Curation createDraft(User user, CurationReq req) { Curation curation = Curation.from(user, req); - curation.setDraft(true); + curation.setDrafted(true); return curation; } diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java b/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java index 9fb5136..3558b7b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java @@ -9,7 +9,8 @@ public enum SortType { SORT_LATEST("latest", "최신순 정렬"), SORT_POPULAR("popular", "인기순 정렬"), - SORT_SIMILARITY("similarity", "취향 유사도순 정렬"); + SORT_SIMILARITY("similarity", "취향 유사도순 정렬"), + SORT_LIKED("liked", "사용자 좋아요 리스트"); private final String value; private final String description; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java index 9f1e96c..13ee3bf 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java @@ -1,11 +1,15 @@ package BookPick.mvp.domain.curation.repository.like; import BookPick.mvp.domain.curation.entity.CurationLike; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface CurationLikeRepository extends JpaRepository { Optional findByUserIdAndCurationId(Long userID, Long curationId); + + List findAllByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index e84bf54..287f974 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -63,7 +63,7 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, } // 1) 큐레이션 페이징해서 구분 - List curations = pageHandler.getCurationsPage(sortType, cursor, size, null); + List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null); CursorPage page = pageHandler.createCursorPage(curations, size); List content = pageHandler.convertToContentRes(page.getContent()); diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index c44dfa5..2aadc5e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -22,7 +22,7 @@ public class CurationPageHandler { private final CurationFetcher curationFetcher; // 1. 데이터 조회 (size+1개 : +1을 하는 이유는 다음 것을 항상 확인하기 위해서 - public List getCurationsPage(SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { + public List getCurationsPage( Long userId, SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { Pageable pageable = PageRequest.of(0, size + 1); // SORT_SIMILARITY일 경우 preferenceInfo를 사용, 나머지는 user 관련 정보 필요 없음 if (sortType == SortType.SORT_SIMILARITY && readingPreferenceInfo == null) { @@ -31,7 +31,7 @@ public List getCurationsPage(SortType sortType, Long cursor, int size, // 1) DB에서 실제로 가져오는 로직 (fetch : DB에서 가져오는 행위) - return curationFetcher.fetchCurations(sortType, cursor, pageable, readingPreferenceInfo); + return curationFetcher.fetchCurations(userId, sortType, cursor, pageable, readingPreferenceInfo); } // 2. 커서 페이징 처리 diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index 1d0b14c..48a9d71 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -1,9 +1,11 @@ package BookPick.mvp.domain.curation.util.list.fetcher; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; +import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.enums.SortType; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.curation.service.list.CurationRecommendationService; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; @@ -20,11 +22,12 @@ public class CurationFetcher { private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; private final CurationRecommendationService curationRecommendationService; // 1. sort Type별로 큐레이션 리스트 가져오기 - public List fetchCurations(SortType sortType, Long cursor, Pageable pageable, ReadingPreferenceInfo readingPreferenceInfo) { + public List fetchCurations(Long userId, SortType sortType, Long cursor, Pageable pageable, ReadingPreferenceInfo readingPreferenceInfo) { // 1) 맨 처음 페이지 로딩 @@ -46,6 +49,12 @@ public List fetchCurations(SortType sortType, Long cursor, Pageable pa yield paginated.stream().map(CurationMatchResult::getCuration).collect(Collectors.toList()); } + case SORT_LIKED -> { + List likedCurationList = curationLikeRepository.findAllByUserIdOrderByCreatedAtDesc(userId, pageable); + yield likedCurationList.stream() + .map(CurationLike::getCuration) + .toList(); + } }; } From 0fd6787036a0ee02441f2d5f590250e5123c33d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 20 Nov 2025 20:48:37 +0900 Subject: [PATCH 137/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=EB=B3=B8=EC=9D=B8=EC=9D=B4=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=ED=95=9C=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9D=84=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=A0=9C=EA=B3=B5=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B0=98=ED=99=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/controller/list/CurationListController.java | 2 ++ .../java/BookPick/mvp/domain/curation/enums/SortType.java | 3 ++- .../mvp/domain/curation/repository/CurationRepository.java | 5 ++++- .../curation/util/list/Handler/CurationPageHandler.java | 2 +- .../domain/curation/util/list/fetcher/CurationFetcher.java | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 464b86e..c820b59 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -49,6 +49,8 @@ public ResponseEntity> getCurations( + + } diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java b/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java index 3558b7b..d625929 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java @@ -10,7 +10,8 @@ public enum SortType { SORT_LATEST("latest", "최신순 정렬"), SORT_POPULAR("popular", "인기순 정렬"), SORT_SIMILARITY("similarity", "취향 유사도순 정렬"), - SORT_LIKED("liked", "사용자 좋아요 리스트"); + SORT_LIKED("liked", "사용자 좋아요 큐레이션 리스트"), + SORT_MY("my", "사용자 작성 큐레이션 리스트"); private final String value; private final String description; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index cc5bfe6..040dada 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -2,6 +2,7 @@ import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.user.entity.User; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -12,7 +13,7 @@ public interface CurationRepository extends JpaRepository { - List findByUserId(Long userId); + List findByUserId(Long userId, Pageable pageable); // 사이즈만큼 최신순으로 불러오는 함수 @@ -48,5 +49,7 @@ List findByRecommendation( ); Optional findByUserIdAndId(Long userId, Long id); + + Long user(User user); } diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index 2aadc5e..cb51f74 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -22,7 +22,7 @@ public class CurationPageHandler { private final CurationFetcher curationFetcher; // 1. 데이터 조회 (size+1개 : +1을 하는 이유는 다음 것을 항상 확인하기 위해서 - public List getCurationsPage( Long userId, SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { + public List getCurationsPage(Long userId, SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { Pageable pageable = PageRequest.of(0, size + 1); // SORT_SIMILARITY일 경우 preferenceInfo를 사용, 나머지는 user 관련 정보 필요 없음 if (sortType == SortType.SORT_SIMILARITY && readingPreferenceInfo == null) { diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index 48a9d71..432bbab 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -48,13 +48,13 @@ public List fetchCurations(Long userId, SortType sortType, Long cursor List paginated = CurationMatchResultPagination.paginate(recommended, cursor, pageable); yield paginated.stream().map(CurationMatchResult::getCuration).collect(Collectors.toList()); } - case SORT_LIKED -> { List likedCurationList = curationLikeRepository.findAllByUserIdOrderByCreatedAtDesc(userId, pageable); yield likedCurationList.stream() .map(CurationLike::getCuration) .toList(); } + case SORT_MY -> curationRepository.findByUserId(userId, pageable); }; } From da28ccc7a49dc9de8d16097d71772ef250ae8a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Fri, 21 Nov 2025 19:30:19 +0900 Subject: [PATCH 138/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=B3=B4=EC=97=AC=EC=A4=84=20=EB=95=8C=20nickName,?= =?UTF-8?q?=20profileImageUrl=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20introduction?= =?UTF-8?q?=EC=9D=84=20=EB=B3=B4=EC=97=AC=EC=A4=84=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=84=B1=EC=9D=B4=20=EC=9E=88=EC=96=B4=20DTO=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/base/get/list/CurationContentRes.java | 22 ++++++++----------- .../dto/base/get/one/CurationGetRes.java | 6 +++++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 058ab82..5940315 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -13,8 +13,10 @@ public record CurationContentRes( String title, Long userId, String nickName, + String profileImageUrl, + String introduction, ThumbnailRes thumbnail, - String summary, + String review, BookRes book, int likeCount, int commentCount, @@ -31,6 +33,8 @@ public static CurationContentRes from(Curation curation) { curation.getTitle(), curation.getUser().getId(), curation.getUser().getNickname(), + curation.getUser().getProfileImageUrl(), + curation.getUser().getBio(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), curation.getLikeCount(), @@ -51,6 +55,8 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr curation.getTitle(), curation.getUser().getId(), matchResult.getUser().getNickname(), + matchResult.getUser().getProfileImageUrl(), + matchResult.getUser().getBio(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), curation.getLikeCount(), @@ -64,27 +70,17 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr ); } - // 1. 유사도 계산법 100% - // 1) 작가 19% - // 2) Matched -> 이거 , 로 분리해서 개수 Count 하나당 20% - - // 1. 유저의 독서취향을 가지고 - // 2. - static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo) { Integer similarity = 50; // Random random = new Random(); User user = matchResult.getUser(); - - // 1. 매칭된 큐레이션의 작가중 String author = matchResult.getCuration().getBookAuthor(); //2. 유저 독서취향의 작가들 안에 존재하면 +20 Set favoriteAuthors = preferenceInfo.favoriteAuthors(); - if(favoriteAuthors.contains(author)){ similarity+=10; } @@ -93,8 +89,8 @@ static Integer getSimilarity(CurationMatchResult matchResult, ReadingPreferenceI similarity += matchResult.getTotalMatchCount()*10; - //4. 1의 자리수 랜덤값으로 조정하여 다채롭게 (mvp단계 한정) -// similarity+=random.nextInt(10); + // 4. 1의 자리수 랜덤값으로 조정하여 다채롭게 (mvp단계 한정) + // similarity+=random.nextInt(10); return similarity; } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index 836f26b..f141ae5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -8,6 +8,9 @@ public record CurationGetRes( Long id, Long userId, + String nickName, + String profileImageUrl, + String introduction, String title, ThumbnailInfo thumbnail, BookInfo book, @@ -20,6 +23,9 @@ public static CurationGetRes from(Curation curation) { return new CurationGetRes( curation.getId(), curation.getUser().getId(), + curation.getUser().getNickname(), + curation.getUser().getProfileImageUrl(), + curation.getUser().getBio(), curation.getTitle(), new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), From 7f77554ce24372446ee00e1c7fbfcb76e924d8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Fri, 21 Nov 2025 19:39:30 +0900 Subject: [PATCH 139/291] =?UTF-8?q?feat=20:=20=20=EC=B2=98=EC=9D=8C=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=ED=95=9C=20=EC=9C=A0=EC=A0=80=EB=8F=84=20?= =?UTF-8?q?=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EA=B2=8C,=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=EC=8B=9C=20=EB=B9=88=20=EB=8F=85?= =?UTF-8?q?=EC=84=9C=EC=B7=A8=ED=96=A5=20=EB=93=B1=EB=A1=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/Book-Pick/bookpick-front/issues/24 --- .../entity/ReadingPreference.java | 13 ++++++++++++ .../service/ReadingPreferenceService.java | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 2de11d3..61d16f0 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -135,6 +135,19 @@ public void update(ReadingPreferenceReq req, AuthorSaveService authorSaveService } } + public static ReadingPreference clearPreferences(User user) { + return ReadingPreference.builder() + .user(user) + .mbti(null) + .favoriteBooks(null) + .favoriteAuthors(null) + .moods(null) + .readingHabits(null) + .genres(null) + .readingStyles(null) + .keywords(null) + .build(); + } } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 20cb350..17e20fd 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -76,6 +76,26 @@ public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceR return ReadingPreferenceRes.from(saved); } + // -- 빈 독서취향 등록 -- + @Transactional + public ReadingPreferenceRes addClearReadingPreference(Long userId) { + + + // 1. 유저 검색 + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + // 2. 독서취향이 이미 존재하면 이미 존재하는 독서취향입니다. + if (readingPreferenceRepository.existsByUserId(userId)) { + throw new AlreadyRegisteredReadingPreferenceException(); + } + + // 3. 처음 가입한 유저도 독서취향 설정할 수 있게, 회원가입시 빈 독서취향 등록하는 로직 추가 + ReadingPreference saved = readingPreferenceRepository.save(ReadingPreference.clearPreferences(user)); + + return ReadingPreferenceRes.from(saved); + } + // -- 유저 독서 취향 단건 조회 -- From 10292734ade848d2e6fa6592e7f75d4dd5d67ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Fri, 21 Nov 2025 23:14:13 +0900 Subject: [PATCH 140/291] =?UTF-8?q?fix=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EA=B3=BC=20=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5=20Boo?= =?UTF-8?q?kRes=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 참고 : https://github.com/Book-Pick/bookpick-front/issues/24 --- .../dto/ReadingPreferenceRes.java | 19 +++- .../entity/ReadingPreference.java | 99 +++++++------------ .../service/ReadingPreferenceService.java | 26 ++--- .../domain/auth/service/SignUpService.java | 8 +- .../author/service/AuthorSaveService.java | 2 +- .../domain/book/dto/preference/BookDto.java | 9 +- .../domain/book/dto/preference/BookRes.java | 13 +++ .../BookPick/mvp/domain/book/entity/Book.java | 27 ++--- .../domain/book/service/BookSaveService.java | 6 +- .../curation/dto/base/get/list/BookRes.java | 6 -- .../dto/base/get/list/BookResInCuration.java | 12 +++ .../dto/base/get/list/CurationContentRes.java | 7 +- .../mvp/domain/curation/entity/Curation.java | 2 + 13 files changed, 114 insertions(+), 122 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/book/dto/preference/BookRes.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookResInCuration.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java index 25377de..ef2327f 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java @@ -3,15 +3,16 @@ import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.author.entity.Author; -import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.dto.preference.BookRes; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public record ReadingPreferenceRes( Long preferenceId, String mbti, - Set favoriteBooks, // 좋아하는 책 + Set favoriteBooks, // 좋아하는 책 Set favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 @@ -21,10 +22,20 @@ public record ReadingPreferenceRes( ) { static public ReadingPreferenceRes from(ReadingPreference rp) { + + Set favoriteBooks = rp.getFavoriteBooks().stream() + .map(book -> new BookRes( + book.getTitle(), + book.getAuthor().getName(), // author 이름만 + book.getImage(), + book.getIsbn() + )) + .collect(Collectors.toSet()); + return new ReadingPreferenceRes( rp.getId(), rp.getMbti(), - rp.getFavoriteBooks(), + favoriteBooks, rp.getFavoriteAuthors(), rp.getMoods(), rp.getReadingHabits(), @@ -34,6 +45,8 @@ static public ReadingPreferenceRes from(ReadingPreference rp) { ); } + + } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index 61d16f0..cafa68f 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -11,12 +11,12 @@ import lombok.*; import java.time.LocalDateTime; -import java.util.List; -import java.util.Set; +import java.util.*; @Entity @Table(name = "reading_preference") @Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor @@ -80,72 +80,49 @@ public class ReadingPreference { private LocalDateTime deletedAt; - public void update(ReadingPreferenceReq req, AuthorSaveService authorSaveService, BookSaveService bookSaveService) { - if (req.mbti() != null) this.mbti = req.mbti(); - - // favoriteBooks 처리 - if (req.favoriteBooks() != null) { - this.favoriteBooks.clear(); - List books = req.favoriteBooks().stream() - .map(dto -> { - Book book = Book.from(dto); - bookSaveService.saveBookIfNotExists(book); // DB에 없으면 저장 - return book; - }) - .toList(); - this.favoriteBooks.addAll(books); + public void update(ReadingPreferenceReq req) { + if (req.mbti() != null) this.mbti = req.mbti(); + + if (req.moods() != null) { + this.moods.clear(); + this.moods.addAll(req.moods()); + } + + if (req.readingHabits() != null) { + this.readingHabits.clear(); + this.readingHabits.addAll(req.readingHabits()); + } + + if (req.genres() != null) { + this.genres.clear(); + this.genres.addAll(req.genres()); + } + + if (req.keywords() != null) { + this.keywords.clear(); + this.keywords.addAll(req.keywords()); + } + + if (req.readingStyles() != null) { + this.readingStyles.clear(); + this.readingStyles.addAll(req.readingStyles()); + } } - // favoriteAuthors 처리 - if (req.favoriteAuthors() != null) { - this.favoriteAuthors.clear(); - List authors = req.favoriteAuthors().stream() - .map(dto -> { - Author author = Author.from(dto); - authorSaveService.saveAuthorIfNotExists(author); // DB에 없으면 저장 - return author; - }) - .toList(); - this.favoriteAuthors.addAll(authors); - } - - if (req.moods() != null) { - this.moods.clear(); - this.moods.addAll(req.moods()); - } - - if (req.readingHabits() != null) { - this.readingHabits.clear(); - this.readingHabits.addAll(req.readingHabits()); - } - - if (req.genres() != null) { - this.genres.clear(); - this.genres.addAll(req.genres()); - } - - if (req.keywords() != null) { - this.keywords.clear(); - this.keywords.addAll(req.keywords()); - } - if (req.readingStyles() != null) { - this.readingStyles.clear(); - this.readingStyles.addAll(req.readingStyles()); - } -} public static ReadingPreference clearPreferences(User user) { - return ReadingPreference.builder() + + return ReadingPreference.builder() .user(user) .mbti(null) - .favoriteBooks(null) - .favoriteAuthors(null) - .moods(null) - .readingHabits(null) - .genres(null) - .readingStyles(null) - .keywords(null) + .favoriteBooks(new HashSet<>()) + .favoriteAuthors(new HashSet<>()) + .moods(new ArrayList<>()) + .readingHabits(new ArrayList<>()) + .genres(new ArrayList<>()) + .readingStyles(new ArrayList<>()) + .keywords(new ArrayList<>()) .build(); } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 17e20fd..d255b54 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -44,7 +44,6 @@ public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceR .orElseThrow(UserNotFoundException::new); // 2. 독서취향이 이미 존재하면 이미 존재하는 독서취향입니다. - // Todo 1. 독서취향 생성은 처음 회원가입시 바로 생성되고 null값 넣는 것으로 변경 필요 if (readingPreferenceRepository.existsByUserId(userId)) { throw new AlreadyRegisteredReadingPreferenceException(); } @@ -57,8 +56,6 @@ public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceR Set savedAuthors = authorSaveService.saveAuthorIfNotExistsDto(req.favoriteAuthors()); // 5. 책 찾고 - - ReadingPreference readingPreference = ReadingPreference.builder() .user(user) .mbti(req.mbti()) @@ -76,8 +73,9 @@ public ReadingPreferenceRes addReadingPreference(Long userId, ReadingPreferenceR return ReadingPreferenceRes.from(saved); } + // -- 빈 독서취향 등록 -- - @Transactional + @Transactional public ReadingPreferenceRes addClearReadingPreference(Long userId) { @@ -96,8 +94,6 @@ public ReadingPreferenceRes addClearReadingPreference(Long userId) { return ReadingPreferenceRes.from(saved); } - - // -- 유저 독서 취향 단건 조회 -- @Transactional public ReadingPreferenceRes findReadingPreference(Long userId) { @@ -110,7 +106,7 @@ public ReadingPreferenceRes findReadingPreference(Long userId) { return ReadingPreferenceRes.from(result); } - // -- 본인 유저 독서 수정 -- + // -- 본인 유저 독서 취향 수정 -- @Transactional public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferenceReq req) { User user = userRepository.findById(userId) @@ -120,18 +116,14 @@ public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferen .orElseThrow(UserReadingPreferenceNotExisted::new); -// // 1. 책 저장 -// List bookDtos = req.favoriteBooks(); -// for (BookDto bookDto : bookDtos){ -// -// -// Book book = bookRepository.findBy -// } + Set savedBooks = bookSaveService.saveBookIfNotExistsDto(req.favoriteBooks()); + + Set savedAuthors = authorSaveService.saveAuthorIfNotExistsDto(req.favoriteAuthors()); - // 2. 작가 저장 + preference.setFavoriteBooks(savedBooks); + preference.setFavoriteAuthors(savedAuthors); - // Todo 2. 구현 필요, Service 파라미터로 주면 안돼요! -// preference.update(req, authorSaveService, bookSaveService); + preference.update(req); return ReadingPreferenceRes.from(preference); } diff --git a/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java b/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java index ae32244..d8d0da3 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java @@ -1,6 +1,7 @@ package BookPick.mvp.domain.auth.service; +import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; import BookPick.mvp.domain.auth.Roles; import BookPick.mvp.domain.auth.dto.SignReq; import BookPick.mvp.domain.auth.dto.SignRes; @@ -24,6 +25,8 @@ public class SignUpService { //Dto로 컨트롤러에서 받음 private final PasswordEncoder passwordEncoder; private final SignUpManager signUpManager; + private final ReadingPreferenceService readingPreferenceService; + public SignRes signUp(SignReq req) throws RuntimeException { @@ -53,7 +56,10 @@ public SignRes signUp(SignReq req) throws RuntimeException { // 3. DB에 저장 User savedUSer = userRepo.save(user); - // 4. Sign Response DTO 반환 + // 4. 빈 독서취향 생성 + readingPreferenceService.addClearReadingPreference(savedUSer.getId()); + + // 5. Sign Response DTO 반환 return new SignRes(savedUSer.getId()); } diff --git a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java index 1051553..fb19e0c 100644 --- a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java +++ b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java @@ -25,7 +25,7 @@ public void saveAuthorIfNotExists(Set authors) { } } - // 2. Author 단건 + // 2. Author 저장 public void saveAuthorIfNotExists(Author author) { authorRepository.findByName(author.getName()) .orElseGet(() -> authorRepository.save(author)); diff --git a/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java index a7920df..768dca7 100644 --- a/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java +++ b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookDto.java @@ -7,14 +7,9 @@ public record BookDto( String title, - // Todo 1. author String 단수로 변경 필요 - Set authors, + String author, String image, String isbn ) { - public BookDto{ - if(authors==null){ - authors=Set.of(); - } - } + } diff --git a/src/main/java/BookPick/mvp/domain/book/dto/preference/BookRes.java b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookRes.java new file mode 100644 index 0000000..53ff5be --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/book/dto/preference/BookRes.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.book.dto.preference; + +public record BookRes( + String title, + String author, + String image, + String isbn +) { + + public static BookRes from(String title, String author, String image, String isbn) { + return new BookRes(title, author, image, isbn); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/book/entity/Book.java b/src/main/java/BookPick/mvp/domain/book/entity/Book.java index 4a576a8..28b0e49 100644 --- a/src/main/java/BookPick/mvp/domain/book/entity/Book.java +++ b/src/main/java/BookPick/mvp/domain/book/entity/Book.java @@ -2,6 +2,7 @@ import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.book.dto.preference.BookDto; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; @@ -25,40 +26,26 @@ public class Book { private String title; - @ManyToMany - @JoinTable( - name = "book_author", - joinColumns = @JoinColumn(name = "book_id"), - inverseJoinColumns = @JoinColumn(name = "author_id") - ) - private Set authors = new HashSet<>(); // 하나의 책마다 여러 작가들 존재 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "author_id") + @JsonIgnore // Todo 1. 큐레이션에 작가의 대한 정보가 필요 없기 때문에 null -> 추후 디벨렆 예정 + private Author author; // 하나의 책마다 여러 작가들 존재 private String image; private String isbn; - public Book() { } - public static Book from(BookDto bookDto) { - - - return Book.builder() - .title(bookDto.title()) - .authors(null) - .image(bookDto.image()) - .isbn(bookDto.isbn()) - .build(); - } - public static Book from(BookDto bookDto, Set authors) { + public static Book from(BookDto bookDto, Author author) { return Book.builder() .title(bookDto.title()) - .authors(authors) + .author(author) .image(bookDto.image()) .isbn(bookDto.isbn()) .build(); diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java index 96e8550..d10a029 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java @@ -33,7 +33,7 @@ public void saveBookIfNotExists(Set books) { public void saveBookIfNotExists(Book book) { bookRepository.findByTitle(book.getTitle()) .orElseGet(() -> { - authorSaveService.saveAuthorIfNotExists(book.getAuthors()); // 작가 먼저 저장 + authorSaveService.saveAuthorIfNotExists(book.getAuthor()); // 작가 먼저 저장 return bookRepository.save(book); }); } @@ -54,9 +54,9 @@ public Book saveBookIfNotExistsDto(BookDto dto) { return bookRepository.findByTitle(dto.title()) .orElseGet(() -> { // 책이 없으면 // authors 저장 - Set authors = authorSaveService.saveAuthorsIfNotExistsByName(dto.authors()); + Author author = authorSaveService.saveAuthorIfNotExistsByName(dto.author()); // Book 객체 변환 후 저장 - Book book = Book.from(dto, authors); + Book book = Book.from(dto, author); return bookRepository.save(book); }); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java deleted file mode 100644 index 12cfa9d..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookRes.java +++ /dev/null @@ -1,6 +0,0 @@ -package BookPick.mvp.domain.curation.dto.base.get.list; - -public record BookRes( - String title, - String author -) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookResInCuration.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookResInCuration.java new file mode 100644 index 0000000..39548bd --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/BookResInCuration.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.curation.dto.base.get.list; + +public record BookResInCuration( + String title, + String author, + String isbn +) { + + public static BookResInCuration from(String title, String author, String isbn) { + return new BookResInCuration(title, author, isbn); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 5940315..7b685dd 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -17,7 +17,7 @@ public record CurationContentRes( String introduction, ThumbnailRes thumbnail, String review, - BookRes book, + BookResInCuration book, int likeCount, int commentCount, int viewCount, @@ -36,7 +36,8 @@ public static CurationContentRes from(Curation curation) { curation.getUser().getProfileImageUrl(), curation.getUser().getBio(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), - curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), + curation.getReview(), + BookResInCuration.from(curation.getTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getLikeCount(), curation.getCommentCount(), curation.getViewCount(), @@ -58,7 +59,7 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr matchResult.getUser().getProfileImageUrl(), matchResult.getUser().getBio(), new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), - curation.getReview(), new BookRes(curation.getBookTitle(), curation.getBookAuthor()), + curation.getReview(), new BookResInCuration(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getLikeCount(), curation.getCommentCount(), curation.getViewCount(), diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index da42adc..18f139d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -43,8 +43,10 @@ public class Curation { @Column(nullable = false) private String bookTitle; private String bookAuthor; + private String bookImage; private String bookIsbn; + @Column(columnDefinition = "TEXT") private String review; From 34a3652e32bb081d4bf80928bac9f3a2615c277e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 23 Nov 2025 20:05:26 +0900 Subject: [PATCH 141/291] =?UTF-8?q?feat=20:=20FE=20=EB=B0=8F=20BE=20?= =?UTF-8?q?=EC=86=8C=ED=86=B5=20=EB=AC=B8=EC=A0=9C=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20=EC=9E=98=20=EB=AA=BB=20=EC=84=A4=EC=A0=95=EB=90=9C?= =?UTF-8?q?=20,=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0=ED=81=B0=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=ED=97=A4=EB=8D=94=20200->401=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 10f7154..69cdfec 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -18,9 +18,9 @@ public enum ErrorCode implements ErrorCodeInterface { Invalid_Token_Type(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), // -- JWT -- - TOKEN_EXPIRED(HttpStatus.OK, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), - INVALID_TOKEN_TYPE(HttpStatus.OK, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), - TOKEN_LOGOUTED(HttpStatus.OK, "이미 로그아웃된 토큰입니다."), + TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "로그인 토큰이 만료되었습니다. 다시 로그인해주세요."), + INVALID_TOKEN_TYPE(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 형식입니다. 다시 로그인해주세요."), + TOKEN_LOGOUTED(HttpStatus.UNAUTHORIZED, "이미 로그아웃된 토큰입니다."), // -- User -- From efea0ed8fefd543d7d125f2d40c92a745df29439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 23 Nov 2025 20:16:06 +0900 Subject: [PATCH 142/291] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=EC=9D=98?= =?UTF-8?q?=20=ED=95=9C=EC=A4=84=20=EC=86=8C=EA=B0=9C=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A3=BC=EA=B8=B0=20=EC=9C=84=ED=95=9C=20introduce=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20User=20Dto=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java | 2 ++ src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java index 3b3dc78..f5ed6c6 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java @@ -9,6 +9,7 @@ public record UserReq( String passWord, String nickName, String profileImage, + String introduction, Roles role ) { public UserReq from(User user){ @@ -18,6 +19,7 @@ public UserReq from(User user){ user.getPassword(), user.getNickname(), user.getProfileImageUrl(), + user.getBio(), user.getRole() ); } diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java b/src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java index eb9c517..6a5c3f5 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/UserRes.java @@ -10,6 +10,7 @@ public record UserRes( String email, String nickName, String profileImage, + String introduction, Roles role, LocalDateTime createdAt, LocalDateTime updatedAt, @@ -22,6 +23,7 @@ public static UserRes from(User user) { user.getEmail(), user.getNickname(), user.getProfileImageUrl(), + user.getBio(), user.getRole(), user.getCreatedAt(), user.getUpdatedAt(), From c49e85aa80591d0b21c33cbfeb582386b2cd869c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 23 Nov 2025 20:16:33 +0900 Subject: [PATCH 143/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=97=90=EB=8A=94=20=EC=B1=85=EC=9D=98=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=EA=B0=80=20=ED=95=84=EC=9A=94=20=EC=97=86?= =?UTF-8?q?=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=ED=95=84=EB=93=9C=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/controller/like/CurationLikeController.java | 2 +- src/main/java/BookPick/mvp/domain/curation/entity/Curation.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java index b9ac602..fd23a83 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java @@ -25,7 +25,7 @@ public class CurationLikeController { private final CurrentUserCheck currentUserCheck; - @GetMapping("/{curationId}") + @PostMapping("/{curationId}") @Operation(summary = "큐레이션 좋아요", description = "큐레이션 좋아요 버튼을 누릅니다.", tags = {"Curation"}) public ResponseEntity> likeOrUnlikeCuration(@AuthenticationPrincipal CustomUserDetails currentUser , @PathVariable Long curationId) { diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 18f139d..f8099bb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -43,7 +43,6 @@ public class Curation { @Column(nullable = false) private String bookTitle; private String bookAuthor; - private String bookImage; private String bookIsbn; From 13f7d32285c2ea07051ca9c2ca6fd9e61ad59a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 25 Nov 2025 20:06:25 +0900 Subject: [PATCH 144/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=98=20?= =?UTF-8?q?=EC=9D=B8=EA=B8=B0=EB=8F=84=EC=99=80=20=EC=8B=A0=EB=A2=B0?= =?UTF-8?q?=EB=8F=84=EB=A5=BC=20=EC=89=BD=EA=B2=8C=20=ED=8C=90=EB=8B=A8?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=EC=A1=B0=ED=9A=8C=20DTO=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/dto/base/get/one/CurationGetRes.java | 7 +++++++ .../mvp/domain/curation/service/base/CurationService.java | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index f141ae5..7d07b16 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -16,6 +16,10 @@ public record CurationGetRes( BookInfo book, String review, RecommendInfo recommend, + Integer likeCount, + Integer viewCount, + Integer CommentCount, + LocalDateTime createdAt, LocalDateTime updatedAt ) { @@ -32,6 +36,9 @@ public static CurationGetRes from(Curation curation) { curation.getReview(), new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), + curation.getLikeCount(), + curation.getViewCount(), + curation.getCommentCount(), curation.getCreatedAt(), curation.getUpdatedAt() ); diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index c3dca9a..ce5da6d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -69,7 +69,7 @@ public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); - curation.increaseViewCount(); + curation.increaseViewCount(); // 큐레이션 조회수 +1 return CurationGetRes.from(curation); } From 9489fbdd9d256f1fdb6ed3f1f88189b74a7fe4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 25 Nov 2025 20:36:49 +0900 Subject: [PATCH 145/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20DTO?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EB=B0=A9?= =?UTF-8?q?=EB=AC=B8=ED=95=9C=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=EC=97=90=20=EC=A2=8B=EC=95=84=EC=9A=94=EB=A5=BC=20?= =?UTF-8?q?=EB=88=8C=EB=A0=80=EB=8D=98=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=ED=99=95=EC=9D=B8=EA=B2=8C=ED=95=B4?= =?UTF-8?q?=EC=A3=BC=EB=8A=94=20isLiked=20=ED=95=84=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 6 +++-- .../dto/base/get/one/CurationGetRes.java | 4 ++- .../like/CurationLikeRepository.java | 2 ++ .../service/base/CurationService.java | 26 ++++++++++++++----- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 250d3c9..d869479 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -48,8 +48,10 @@ public ResponseEntity> create( @GetMapping("/{curationId}") public ResponseEntity> getCuration( @PathVariable Long curationId, - HttpServletRequest req) { - CurationGetRes res = curationService.findCuration(curationId, req); + HttpServletRequest req, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + CurationGetRes res = curationService.findCuration(curationId, currentUser, req); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index 7d07b16..a88a96d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -16,6 +16,7 @@ public record CurationGetRes( BookInfo book, String review, RecommendInfo recommend, + Boolean isLiked, Integer likeCount, Integer viewCount, Integer CommentCount, @@ -23,7 +24,7 @@ public record CurationGetRes( LocalDateTime createdAt, LocalDateTime updatedAt ) { - public static CurationGetRes from(Curation curation) { + public static CurationGetRes from(Curation curation, boolean isLiked) { return new CurationGetRes( curation.getId(), curation.getUser().getId(), @@ -36,6 +37,7 @@ public static CurationGetRes from(Curation curation) { curation.getReview(), new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), + isLiked, curation.getLikeCount(), curation.getViewCount(), curation.getCommentCount(), diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java index 13ee3bf..6f47dab 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java @@ -12,4 +12,6 @@ public interface CurationLikeRepository extends JpaRepository Optional findByUserIdAndCurationId(Long userID, Long curationId); List findAllByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); + + Optional findByUserId(Long userId); } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index ce5da6d..62dc773 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -1,6 +1,7 @@ // CurationListService.java package BookPick.mvp.domain.curation.service.base; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; @@ -8,9 +9,11 @@ import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; import BookPick.mvp.domain.user.entity.User; @@ -22,16 +25,17 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.Optional; @Service @RequiredArgsConstructor public class CurationService { private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; private final UserRepository userRepository; private final CurationFetcher curationFetcher; - private final CurationPageHandler pageHandler; - + private final CurationPageHandler pageHandler; // -- 큐레이션 등록 -- @@ -65,15 +69,25 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { // -- 큐레이션 단건 조회 -- @Transactional - public CurationGetRes findCuration(Long curationId, HttpServletRequest req) { + public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { + boolean isLikedCuration = false; + Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); - curation.increaseViewCount(); // 큐레이션 조회수 +1 + curation.increaseViewCount(); // 큐레이션 조회수 +1 - return CurationGetRes.from(curation); - } + // 1. 좋아요 정보 찾기 + if (user != null) { + Optional curationLike = curationLikeRepository.findByUserIdAndCurationId(user.getId(), curationId); + if (curationLike.isPresent()) { + isLikedCuration = true; + } + } + // 2. + return CurationGetRes.from(curation, isLikedCuration); + } // -- 큐레이션 수정 -- From 38a74530c92e92565ac0f106a2f9a6fd4e8445fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 25 Nov 2025 20:53:52 +0900 Subject: [PATCH 146/291] chore: meaningless commit --- .../mvp/domain/curation/service/base/CurationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 62dc773..3c04ef4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -85,7 +85,7 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http isLikedCuration = true; } } - // 2. + return CurationGetRes.from(curation, isLikedCuration); } From 66c63f9f6bc4f05bf1454c3bb86e18bc305b6006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 26 Nov 2025 21:34:43 +0900 Subject: [PATCH 147/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=ED=8A=B9=EC=A0=95=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=9D=98=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98?= =?UTF-8?q?=EC=9D=84=20=EA=B5=AC=EB=8F=85=ED=95=98=EC=97=AC=20=EC=84=A0?= =?UTF-8?q?=ED=98=B8=20=EC=BD=98=ED=85=90=EC=B8=A0=EB=A5=BC=20=EB=AA=A8?= =?UTF-8?q?=EC=95=84=EB=B3=BC=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=EB=8F=85=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 토글기능 프론트가 사용하기 하나의 API를 사용함으로서 관리하기 편하게 하기위해 --- .../like/CurationLikeController.java | 4 +- .../CurationSubscribeController.java | 63 +++++++++++++++++++ .../dto/subscribe/CurationSubscribeDto.java | 17 +++++ .../curation/entity/CurationSubscribe.java | 31 +++++++++ .../curation/enums/CurationSuccessCode.java | 12 +++- .../CurationSubscribeRepository.java | 14 +++++ .../subscribe/CurationSubscribeService.java | 63 +++++++++++++++++++ 7 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java index fd23a83..e016c8b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java @@ -36,10 +36,10 @@ public ResponseEntity> likeOrUnlikeCuration(@AuthenticationPri if (liked) { return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(CurationSuccessCode.POST_LIKE_SUCCESS, null)); + .body(ApiResponse.success(CurationSuccessCode.CURATION_LIKE_SUCCESS, null)); } else { return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(CurationSuccessCode.POST_DISLIKE_SUCCESS, null)); + .body(ApiResponse.success(CurationSuccessCode.CURATION_DISLIKE_SUCCESS, null)); } } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java new file mode 100644 index 0000000..a182cd5 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java @@ -0,0 +1,63 @@ +// CurationListController.java에 추가 +package BookPick.mvp.domain.curation.controller.subscribe; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.subscribe.CurationSubscribeDto; +import BookPick.mvp.domain.curation.enums.CurationSuccessCode; +import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.curation.service.subscribe.CurationSubscribeService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; +import BookPick.mvp.global.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/curations") +@RequiredArgsConstructor +public class CurationSubscribeController { + + private final CurationService curationService; + private final CurrentUserCheck currentUserCheck; + private final CurationSubscribeService curationSubscribeService; + + @PostMapping("/{curationId}/subscribe") + @Operation(summary = "큐레이션 구독", description = "큐레이션 구독 버튼을 누릅니다.", tags = {"Curation"}) + public ResponseEntity> likeOrUnlikeCuration( + @AuthenticationPrincipal CustomUserDetails currentUser + , @PathVariable Long curationId) { + + currentUserCheck.isValidCurrentUser(currentUser); + + CurationSubscribeDto curationSubscribeDto = curationSubscribeService.subscribe(currentUser.getId(), curationId); + + if (curationSubscribeDto.subscribed()) { + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(CurationSuccessCode.CURATION_SUBSCRIBE_SUCCESS, curationSubscribeDto)); + } else { + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(CurationSuccessCode.CURATION_SUBSCRIBE_CANCLE_SUCCESS, curationSubscribeDto)); + } + } + + + +// @Operation(summary = "큐레이션 구독 리스트 제공", description = "사용자가 구독한 큐레이터 리스트를 제공합니다.", tags = {"Curation"}) +// @PatchMapping("/{curationId}") +// public ResponseEntity> updateCuration( +// @PathVariable Long curationId, +// @Valid @RequestBody CurationUpdateReq req, +// @AuthenticationPrincipal CustomUserDetails currentUser) { +// CurationUpdateRes res = curationService.modifyCuration(currentUser.getId(), curationId, req); +// return ResponseEntity.ok() +// .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); +// } + + +} + + + diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java new file mode 100644 index 0000000..c4389e7 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java @@ -0,0 +1,17 @@ +package BookPick.mvp.domain.curation.dto.subscribe; + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationSubscribe; + +public record CurationSubscribeDto( + Long curationId, + boolean subscribed +){ + public static CurationSubscribeDto from(Curation curation, boolean subscribed){ + return new CurationSubscribeDto(curation.getId(),subscribed); + } + + public static CurationSubscribeDto from(CurationSubscribe curationSubscribe, boolean subscribed){ + return new CurationSubscribeDto(curationSubscribe.getCuration().getId(),subscribed); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java b/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java new file mode 100644 index 0000000..7a0aa07 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.curation.entity; + +import BookPick.mvp.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class CurationSubscribe { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "curation_id") + private Curation curation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; +} + + diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java index 270f82e..0d7b400 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java @@ -13,9 +13,15 @@ public enum CurationSuccessCode implements SuccessCodeInterface { // 1. 임시저장 CREATE_DRAFTED_CURATION_SUCCESS(HttpStatus.CREATED, "큐레이션 임시저장에 성공했습니다"), - // -- 2. 좋아요 -- - POST_LIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요를 성공적으로 실행하였습니다."), - POST_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."); + // 2. 좋아요 + CURATION_LIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요를 성공적으로 실행하였습니다."), + CURATION_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."), + + + // 3. 구독 + CURATION_SUBSCRIBE_SUCCESS(HttpStatus.CREATED, "큐레이션 구독을 성공적으로 실행하였습니다."), + CURATION_SUBSCRIBE_CANCLE_SUCCESS(HttpStatus.OK, "큐레이션 구독 취소를 성공적으로 실행하였습니다."); + private final HttpStatus status; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java new file mode 100644 index 0000000..7a6b4bd --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java @@ -0,0 +1,14 @@ +package BookPick.mvp.domain.curation.repository.subscribe; + +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.entity.CurationSubscribe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CurationSubscribeRepository extends JpaRepository { + + Optional findByUserIdAndCurationId(Long userId, Long curationId); +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java b/src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java new file mode 100644 index 0000000..27ff772 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java @@ -0,0 +1,63 @@ +package BookPick.mvp.domain.curation.service.subscribe; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.subscribe.CurationSubscribeDto; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.entity.CurationSubscribe; +import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.subscribe.CurationSubscribeRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationSubscribeService { + private final CurationSubscribeRepository curationSubscribeRepository; + private final CurationRepository curationRepository; + private final UserRepository userRepository; + + + // 1. 큐레이션 구독 + @Transactional + public CurationSubscribeDto subscribe(Long userId, Long curationId) { + + // 1. 포스트 아이디 얻기 + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + // 2. 유저 아이디 얻기 + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + // 3. 해당 유저가 구독했는지 체크 + // 3.1 유저의 큐레이션 구독 정보 가져오기 + Optional opt = curationSubscribeRepository.findByUserIdAndCurationId(userId, curationId); + + // 3.2 구독 정보가 없으면 생성 + if (opt.isEmpty()) { + CurationSubscribe curationSubscribe = CurationSubscribe.builder() + .user(user) + .curation(curation) + .build(); + + curationSubscribeRepository.save(curationSubscribe); + + return CurationSubscribeDto.from(curationSubscribe , true); + } + + // 3.2 구독 정보가 있으면 삭제 + else { + curationSubscribeRepository.delete(opt.get()); + + return CurationSubscribeDto.from(curation , false); + } + } + +} From 1ee5acd1eec2e7970bae21d48682683ab1bc3c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 16:58:30 +0900 Subject: [PATCH 148/291] =?UTF-8?q?fix=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EA=B5=AC=EB=8F=85=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=EC=9D=B4=EA=B8=B0=20=EB=95=8C=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=9C=A0=EC=A0=80=20=ED=95=84=EB=93=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/entity/CurationSubscribe.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java b/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java index 7a0aa07..e1d1586 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java @@ -8,6 +8,15 @@ import java.time.LocalDateTime; @Entity +@Table( + name = "curation_subscribe", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_user_curator", + columnNames = {"user_id", "curator_id"} + ) + } +) @AllArgsConstructor @NoArgsConstructor @Builder @@ -19,13 +28,14 @@ public class CurationSubscribe { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "curation_id") - private Curation curation; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "curator_id") + private User curator; } + From 3447ee7084268f35f5637f9512b8731f6681bb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 17:07:04 +0900 Subject: [PATCH 149/291] =?UTF-8?q?feat=20:=20=EA=B4=80=EC=8B=AC=EC=82=AC?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=ED=84=B0=EC=99=80=20=EC=9D=BC=EB=B0=98=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EC=9D=98=20=EC=97=90=EB=9F=AC=20,=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=8F=20=EC=84=B1=EA=B3=B5=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReadingPreferenceService.java | 4 +--- .../auth/service/MyUserDetailsService.java | 2 +- .../mvp/domain/auth/service/SignUpService.java | 1 - .../domain/comment/service/CommentService.java | 2 +- .../curation/enums/CurationSuccessCode.java | 7 +++++-- .../curation/service/base/CurationService.java | 2 +- .../service/draft/CurationDraftService.java | 3 +-- .../service/like/CurationLikeService.java | 2 +- .../user/controller/base/UserController.java | 5 ++--- .../passWord/PasswordContorller.java | 3 +-- .../controller/profile/ProfileController.java | 2 +- .../user/enums/curator/CuratorErrorCode.java | 18 ++++++++++++++++++ .../user/enums/curator/CuratorSuccessCode.java | 17 +++++++++++++++++ .../user/enums/{ => user}/UserErrorCode.java | 2 +- .../domain/user/enums/{ => user}/UserRole.java | 2 +- .../user/enums/{ => user}/UserSuccessCode.java | 2 +- .../{ => common}/AlreadyDeletedException.java | 4 ++-- .../{ => common}/NotHaveAdminRole.java | 4 ++-- .../PasswordMismatchException.java | 4 ++-- .../{ => common}/UserNotFoundException.java | 2 +- .../WrongCurrentPasswordException.java | 4 ++-- .../curator/CuratorNotFoundException.java | 11 +++++++++++ .../domain/user/service/base/UserService.java | 4 ++-- .../user/service/passWord/PassWordService.java | 6 +++--- 24 files changed, 78 insertions(+), 35 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorErrorCode.java create mode 100644 src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java rename src/main/java/BookPick/mvp/domain/user/enums/{ => user}/UserErrorCode.java (95%) rename src/main/java/BookPick/mvp/domain/user/enums/{ => user}/UserRole.java (83%) rename src/main/java/BookPick/mvp/domain/user/enums/{ => user}/UserSuccessCode.java (95%) rename src/main/java/BookPick/mvp/domain/user/exception/{ => common}/AlreadyDeletedException.java (66%) rename src/main/java/BookPick/mvp/domain/user/exception/{ => common}/NotHaveAdminRole.java (65%) rename src/main/java/BookPick/mvp/domain/user/exception/{ => common}/PasswordMismatchException.java (67%) rename src/main/java/BookPick/mvp/domain/user/exception/{ => common}/UserNotFoundException.java (83%) rename src/main/java/BookPick/mvp/domain/user/exception/{ => common}/WrongCurrentPasswordException.java (68%) create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/curator/CuratorNotFoundException.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index d255b54..04a8446 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -9,19 +9,17 @@ import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.author.service.AuthorSaveService; -import BookPick.mvp.domain.book.dto.preference.BookDto; import BookPick.mvp.domain.book.entity.Book; import BookPick.mvp.domain.book.repository.BookRepository; import BookPick.mvp.domain.book.service.BookSaveService; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.List; import java.util.Set; @Service diff --git a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java index 6fc57a9..c1ba48f 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/MyUserDetailsService.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.auth.Roles; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; diff --git a/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java b/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java index d8d0da3..2a6e868 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/SignUpService.java @@ -6,7 +6,6 @@ import BookPick.mvp.domain.auth.dto.SignReq; import BookPick.mvp.domain.auth.dto.SignRes; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.enums.UserRole; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index d0ec634..9b9e9ad 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -14,7 +14,7 @@ import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.global.dto.PageInfo; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java index 0d7b400..29f6c59 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java @@ -20,8 +20,11 @@ public enum CurationSuccessCode implements SuccessCodeInterface { // 3. 구독 CURATION_SUBSCRIBE_SUCCESS(HttpStatus.CREATED, "큐레이션 구독을 성공적으로 실행하였습니다."), - CURATION_SUBSCRIBE_CANCLE_SUCCESS(HttpStatus.OK, "큐레이션 구독 취소를 성공적으로 실행하였습니다."); - + CURATION_SUBSCRIBE_CANCLE_SUCCESS(HttpStatus.OK, "큐레이션 구독 취소를 성공적으로 실행하였습니다."), + + + GET_CURATION_SUBSCRIBE_LIST_SUCCESS(HttpStatus.OK, "큐레이션 구독을 리스트를 성공적으로 조회하였습니다."); + private final HttpStatus status; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 3c04ef4..9371b6f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -17,7 +17,7 @@ import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java index ce8d108..74b9413 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java @@ -4,9 +4,8 @@ import BookPick.mvp.domain.curation.dto.base.CurationRes; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.domain.user.dto.base.UserReq; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java index 11abbfb..346eb54 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java @@ -6,7 +6,7 @@ import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java index 88f6f33..4cba262 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -5,12 +5,11 @@ import BookPick.mvp.domain.user.dto.base.UserReq; import BookPick.mvp.domain.user.dto.base.UserRes; import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; -import BookPick.mvp.domain.user.enums.UserSuccessCode; -import BookPick.mvp.domain.user.exception.NotHaveAdminRole; +import BookPick.mvp.domain.user.enums.user.UserSuccessCode; +import BookPick.mvp.domain.user.exception.common.NotHaveAdminRole; import BookPick.mvp.domain.user.service.base.UserService; import BookPick.mvp.domain.user.util.AdminManager; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java b/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java index df4c1cb..b405642 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/passWord/PasswordContorller.java @@ -2,10 +2,9 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.user.dto.passWord.PassWordChangeReq; -import BookPick.mvp.domain.user.enums.UserSuccessCode; +import BookPick.mvp.domain.user.enums.user.UserSuccessCode; import BookPick.mvp.domain.user.service.passWord.PassWordService; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java index 22d6a6e..cccad6c 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java @@ -5,7 +5,7 @@ import BookPick.mvp.domain.user.dto.base.UserReq; import BookPick.mvp.domain.user.dto.base.UserRes; import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; -import BookPick.mvp.domain.user.enums.UserSuccessCode; +import BookPick.mvp.domain.user.enums.user.UserSuccessCode; import BookPick.mvp.domain.user.service.base.UserService; import BookPick.mvp.domain.user.util.AdminManager; import BookPick.mvp.global.api.ApiResponse; diff --git a/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorErrorCode.java b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorErrorCode.java new file mode 100644 index 0000000..3c3e6a4 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorErrorCode.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.user.enums.curator; + +import BookPick.mvp.global.enums.ErrorCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum CuratorErrorCode implements ErrorCodeInterface { + + // -- Curator -- + Curator_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."); //404 + + private final HttpStatus status; + private final String message; + +} diff --git a/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java new file mode 100644 index 0000000..b33bff9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java @@ -0,0 +1,17 @@ +package BookPick.mvp.domain.user.enums.curator; + +import BookPick.mvp.global.enums.SuccessCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +@Getter +public enum CuratorSuccessCode implements SuccessCodeInterface { + + + PASSWORD_CHANGE_SUCCESS(HttpStatus.OK, "비밀번호 변경을 성공하였습니다."); + + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java b/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java similarity index 95% rename from src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java rename to src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java index 362b75a..160e536 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/UserErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.enums; +package BookPick.mvp.domain.user.enums.user; import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/user/enums/UserRole.java b/src/main/java/BookPick/mvp/domain/user/enums/user/UserRole.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/user/enums/UserRole.java rename to src/main/java/BookPick/mvp/domain/user/enums/user/UserRole.java index d58e13e..8c4e65d 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/UserRole.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/user/UserRole.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.enums; +package BookPick.mvp.domain.user.enums.user; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java b/src/main/java/BookPick/mvp/domain/user/enums/user/UserSuccessCode.java similarity index 95% rename from src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java rename to src/main/java/BookPick/mvp/domain/user/enums/user/UserSuccessCode.java index 5c3e7cf..928bc84 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/UserSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/user/UserSuccessCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.enums; +package BookPick.mvp.domain.user.enums.user; import BookPick.mvp.global.enums.SuccessCodeInterface; import lombok.AllArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java b/src/main/java/BookPick/mvp/domain/user/exception/common/AlreadyDeletedException.java similarity index 66% rename from src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java rename to src/main/java/BookPick/mvp/domain/user/exception/common/AlreadyDeletedException.java index a1e69f9..beb5098 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/AlreadyDeletedException.java +++ b/src/main/java/BookPick/mvp/domain/user/exception/common/AlreadyDeletedException.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.user.exception.common; -import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.domain.user.enums.user.UserErrorCode; import BookPick.mvp.global.exception.BusinessException; public class AlreadyDeletedException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java b/src/main/java/BookPick/mvp/domain/user/exception/common/NotHaveAdminRole.java similarity index 65% rename from src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java rename to src/main/java/BookPick/mvp/domain/user/exception/common/NotHaveAdminRole.java index 2ed19c7..e0114cd 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/NotHaveAdminRole.java +++ b/src/main/java/BookPick/mvp/domain/user/exception/common/NotHaveAdminRole.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.user.exception.common; -import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.domain.user.enums.user.UserErrorCode; import BookPick.mvp.global.exception.BusinessException; public class NotHaveAdminRole extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java b/src/main/java/BookPick/mvp/domain/user/exception/common/PasswordMismatchException.java similarity index 67% rename from src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java rename to src/main/java/BookPick/mvp/domain/user/exception/common/PasswordMismatchException.java index 06fdf0d..3e46c06 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/PasswordMismatchException.java +++ b/src/main/java/BookPick/mvp/domain/user/exception/common/PasswordMismatchException.java @@ -1,6 +1,6 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.user.exception.common; -import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.domain.user.enums.user.UserErrorCode; import BookPick.mvp.global.exception.BusinessException; public class PasswordMismatchException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java b/src/main/java/BookPick/mvp/domain/user/exception/common/UserNotFoundException.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java rename to src/main/java/BookPick/mvp/domain/user/exception/common/UserNotFoundException.java index a9b6469..c0e1800 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/UserNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/user/exception/common/UserNotFoundException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.user.exception.common; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java b/src/main/java/BookPick/mvp/domain/user/exception/common/WrongCurrentPasswordException.java similarity index 68% rename from src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java rename to src/main/java/BookPick/mvp/domain/user/exception/common/WrongCurrentPasswordException.java index bdf7af7..211b9fb 100644 --- a/src/main/java/BookPick/mvp/domain/user/exception/WrongCurrentPasswordException.java +++ b/src/main/java/BookPick/mvp/domain/user/exception/common/WrongCurrentPasswordException.java @@ -1,7 +1,7 @@ -package BookPick.mvp.domain.user.exception; +package BookPick.mvp.domain.user.exception.common; -import BookPick.mvp.domain.user.enums.UserErrorCode; +import BookPick.mvp.domain.user.enums.user.UserErrorCode; import BookPick.mvp.global.exception.BusinessException; public class WrongCurrentPasswordException extends BusinessException { diff --git a/src/main/java/BookPick/mvp/domain/user/exception/curator/CuratorNotFoundException.java b/src/main/java/BookPick/mvp/domain/user/exception/curator/CuratorNotFoundException.java new file mode 100644 index 0000000..617814c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/curator/CuratorNotFoundException.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.user.exception.curator; + +import BookPick.mvp.domain.user.enums.curator.CuratorErrorCode; +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CuratorNotFoundException extends BusinessException { + public CuratorNotFoundException(){ + super(CuratorErrorCode.Curator_NOT_FOUND); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java index 5029bd2..c1724b7 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java @@ -4,8 +4,8 @@ import BookPick.mvp.domain.user.dto.base.UserRes; import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.AlreadyDeletedException; -import BookPick.mvp.domain.user.exception.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.AlreadyDeletedException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java b/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java index d458db9..1f72da7 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/passWord/PassWordService.java @@ -2,9 +2,9 @@ import BookPick.mvp.domain.user.dto.passWord.PassWordChangeReq; import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.PasswordMismatchException; -import BookPick.mvp.domain.user.exception.UserNotFoundException; -import BookPick.mvp.domain.user.exception.WrongCurrentPasswordException; +import BookPick.mvp.domain.user.exception.common.PasswordMismatchException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.exception.common.WrongCurrentPasswordException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; From 5f7afc9ff22293570800c4f851bed4af8d2213e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 17:08:10 +0900 Subject: [PATCH 150/291] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=EC=99=80?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=EA=B0=80=20=EA=B5=AC=EB=8F=85=ED=95=98?= =?UTF-8?q?=EA=B3=A0=EC=9E=90=ED=95=98=EB=8A=94=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=9D=98=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A7=80=EA=B3=A0=20=EA=B5=AC=EB=8F=85=EC=9D=84=20?= =?UTF-8?q?=ED=95=B4=EC=95=BC=ED=95=98=EA=B8=B0=20=EB=95=8C=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20=EC=9D=B4=EC=97=90=20=EB=A7=9E=EB=8A=94=20JPA=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/subscribe/CurationSubscribeRepository.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java index 7a6b4bd..2f430bc 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java @@ -1,14 +1,20 @@ package BookPick.mvp.domain.curation.repository.subscribe; +import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.entity.CurationSubscribe; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface CurationSubscribeRepository extends JpaRepository { - Optional findByUserIdAndCurationId(Long userId, Long curationId); + Optional findByUserIdAndCuratorId(Long userId, Long curatorId); + } From a4d2cdd1d1962a5a78b9172bb1e2a0856326110a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 17:39:24 +0900 Subject: [PATCH 151/291] =?UTF-8?q?[Feat]=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=EC=9B=90=ED=95=98=EB=8A=94=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=9D=98=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=EB=A7=8C=20=EB=B3=B4=EA=B3=A0=20=EC=8B=B6=EA=B2=8C=20?= =?UTF-8?q?=EB=A7=8C=EB=93=9C=EB=8A=94=EB=8D=B0=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B8=B0=EB=8A=A5=EC=A4=91=EC=97=90=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EC=9D=B8=20=ED=81=90=EB=A0=88=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/service/CommentService.java | 2 +- .../draft/CurationDraftController.java | 2 +- .../like/CurationLikeController.java | 6 +- .../CurationSubscribeController.java | 63 --------------- .../dto/base/get/list/CurationListGetRes.java | 2 +- .../dto/subscribe/CurationSubscribeDto.java | 17 ---- .../enums/{ => common}/CurationErrorCode.java | 2 +- .../{ => common}/CurationSuccessCode.java | 10 +-- .../curation/enums/{ => common}/SortType.java | 2 +- .../CurationAccessDeniedException.java | 4 +- .../CurationNotFoundException.java | 2 +- .../CurationSubscribeRepository.java | 20 ----- .../service/base/CurationService.java | 4 +- .../service/like/CurationLikeService.java | 2 +- .../service/list/CurationListService.java | 2 +- .../subscribe/CurationSubscribeService.java | 63 --------------- .../list/Handler/CurationPageHandler.java | 2 +- .../util/list/fetcher/CurationFetcher.java | 2 +- .../subscribe/CuratorSubscribeController.java | 71 +++++++++++++++++ .../dto/subscribe/CuratorSubscribeReq.java | 8 ++ .../dto/subscribe/CuratorSubscribeRes.java | 19 +++++ .../entity/CuratorSubscribe.java} | 10 +-- .../enums/curator/CuratorSuccessCode.java | 6 +- .../SelfSubscribeDeniedException.java | 11 +++ .../CurationSubscribeRepository.java | 14 ++++ .../subscribe/CurationSubscribeService.java | 79 +++++++++++++++++++ .../mvp/global/HyperParam/Defaults.java | 2 +- 27 files changed, 228 insertions(+), 199 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java rename src/main/java/BookPick/mvp/domain/curation/enums/{ => common}/CurationErrorCode.java (90%) rename src/main/java/BookPick/mvp/domain/curation/enums/{ => common}/CurationSuccessCode.java (56%) rename src/main/java/BookPick/mvp/domain/curation/enums/{ => common}/SortType.java (93%) rename src/main/java/BookPick/mvp/domain/curation/exception/{ => common}/CurationAccessDeniedException.java (75%) rename src/main/java/BookPick/mvp/domain/curation/exception/{ => common}/CurationNotFoundException.java (84%) delete mode 100644 src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java create mode 100644 src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeReq.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeRes.java rename src/main/java/BookPick/mvp/domain/{curation/entity/CurationSubscribe.java => user/entity/CuratorSubscribe.java} (71%) create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/subscribe/SelfSubscribeDeniedException.java create mode 100644 src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java create mode 100644 src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 9b9e9ad..94a0a29 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -10,7 +10,7 @@ import BookPick.mvp.domain.comment.entity.Comment; import BookPick.mvp.domain.comment.exception.CommentNotFoundException; import BookPick.mvp.domain.comment.repository.CommentRepository; -import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.user.entity.User; diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java index 76012e3..8fdee45 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java @@ -4,7 +4,7 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.CurationRes; -import BookPick.mvp.domain.curation.enums.CurationSuccessCode; +import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; import BookPick.mvp.domain.curation.service.draft.CurationDraftService; import BookPick.mvp.global.api.ApiResponse; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java index e016c8b..787eaaa 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java @@ -1,15 +1,11 @@ package BookPick.mvp.domain.curation.controller.like; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; -import BookPick.mvp.domain.curation.enums.CurationSuccessCode; +import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; import BookPick.mvp.domain.curation.service.like.CurationLikeService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java deleted file mode 100644 index a182cd5..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/controller/subscribe/CurationSubscribeController.java +++ /dev/null @@ -1,63 +0,0 @@ -// CurationListController.java에 추가 -package BookPick.mvp.domain.curation.controller.subscribe; - -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.subscribe.CurationSubscribeDto; -import BookPick.mvp.domain.curation.enums.CurationSuccessCode; -import BookPick.mvp.domain.curation.service.base.CurationService; -import BookPick.mvp.domain.curation.service.subscribe.CurationSubscribeService; -import BookPick.mvp.domain.user.util.CurrentUserCheck; -import BookPick.mvp.global.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/v1/curations") -@RequiredArgsConstructor -public class CurationSubscribeController { - - private final CurationService curationService; - private final CurrentUserCheck currentUserCheck; - private final CurationSubscribeService curationSubscribeService; - - @PostMapping("/{curationId}/subscribe") - @Operation(summary = "큐레이션 구독", description = "큐레이션 구독 버튼을 누릅니다.", tags = {"Curation"}) - public ResponseEntity> likeOrUnlikeCuration( - @AuthenticationPrincipal CustomUserDetails currentUser - , @PathVariable Long curationId) { - - currentUserCheck.isValidCurrentUser(currentUser); - - CurationSubscribeDto curationSubscribeDto = curationSubscribeService.subscribe(currentUser.getId(), curationId); - - if (curationSubscribeDto.subscribed()) { - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(CurationSuccessCode.CURATION_SUBSCRIBE_SUCCESS, curationSubscribeDto)); - } else { - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(CurationSuccessCode.CURATION_SUBSCRIBE_CANCLE_SUCCESS, curationSubscribeDto)); - } - } - - - -// @Operation(summary = "큐레이션 구독 리스트 제공", description = "사용자가 구독한 큐레이터 리스트를 제공합니다.", tags = {"Curation"}) -// @PatchMapping("/{curationId}") -// public ResponseEntity> updateCuration( -// @PathVariable Long curationId, -// @Valid @RequestBody CurationUpdateReq req, -// @AuthenticationPrincipal CustomUserDetails currentUser) { -// CurationUpdateRes res = curationService.modifyCuration(currentUser.getId(), curationId, req); -// return ResponseEntity.ok() -// .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); -// } - - -} - - - diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java index 4c197c1..ecaf73b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java @@ -1,7 +1,7 @@ // CurationListGetRes.java package BookPick.mvp.domain.curation.dto.base.get.list; -import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.enums.common.SortType; import java.util.List; public record CurationListGetRes( diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java deleted file mode 100644 index c4389e7..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/subscribe/CurationSubscribeDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package BookPick.mvp.domain.curation.dto.subscribe; - -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationSubscribe; - -public record CurationSubscribeDto( - Long curationId, - boolean subscribed -){ - public static CurationSubscribeDto from(Curation curation, boolean subscribed){ - return new CurationSubscribeDto(curation.getId(),subscribed); - } - - public static CurationSubscribeDto from(CurationSubscribe curationSubscribe, boolean subscribed){ - return new CurationSubscribeDto(curationSubscribe.getCuration().getId(),subscribed); - } -} diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationErrorCode.java similarity index 90% rename from src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java rename to src/main/java/BookPick/mvp/domain/curation/enums/common/CurationErrorCode.java index 7b64ef4..bc53c0b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationErrorCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.enums; +package BookPick.mvp.domain.curation.enums.common; import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java similarity index 56% rename from src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java rename to src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java index 29f6c59..28f5d06 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.enums; +package BookPick.mvp.domain.curation.enums.common; import BookPick.mvp.global.enums.SuccessCodeInterface; import lombok.AllArgsConstructor; @@ -15,15 +15,9 @@ public enum CurationSuccessCode implements SuccessCodeInterface { // 2. 좋아요 CURATION_LIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요를 성공적으로 실행하였습니다."), - CURATION_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."), + CURATION_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."); - // 3. 구독 - CURATION_SUBSCRIBE_SUCCESS(HttpStatus.CREATED, "큐레이션 구독을 성공적으로 실행하였습니다."), - CURATION_SUBSCRIBE_CANCLE_SUCCESS(HttpStatus.OK, "큐레이션 구독 취소를 성공적으로 실행하였습니다."), - - - GET_CURATION_SUBSCRIBE_LIST_SUCCESS(HttpStatus.OK, "큐레이션 구독을 리스트를 성공적으로 조회하였습니다."); diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/SortType.java similarity index 93% rename from src/main/java/BookPick/mvp/domain/curation/enums/SortType.java rename to src/main/java/BookPick/mvp/domain/curation/enums/common/SortType.java index d625929..808ffd8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/SortType.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/SortType.java @@ -1,5 +1,5 @@ // SortType.java -package BookPick.mvp.domain.curation.enums; +package BookPick.mvp.domain.curation.enums.common; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAccessDeniedException.java similarity index 75% rename from src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java rename to src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAccessDeniedException.java index 307efb1..4c582d6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/exception/CurationAccessDeniedException.java +++ b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAccessDeniedException.java @@ -1,5 +1,5 @@ -// CurationAccessDeniedException.java -package BookPick.mvp.domain.curation.exception; +// SelfSubscribeDeniedException.java +package BookPick.mvp.domain.curation.exception.common; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationNotFoundException.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java rename to src/main/java/BookPick/mvp/domain/curation/exception/common/CurationNotFoundException.java index ead82ed..bb82a5d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/exception/CurationNotFoundException.java +++ b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationNotFoundException.java @@ -1,5 +1,5 @@ // CurationNotFoundException.java -package BookPick.mvp.domain.curation.exception; +package BookPick.mvp.domain.curation.exception.common; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java deleted file mode 100644 index 2f430bc..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/repository/subscribe/CurationSubscribeRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package BookPick.mvp.domain.curation.repository.subscribe; - -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.entity.CurationSubscribe; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface CurationSubscribeRepository extends JpaRepository { - - Optional findByUserIdAndCuratorId(Long userId, Long curatorId); - -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 9371b6f..16773ba 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -10,8 +10,8 @@ import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.exception.CurationAccessDeniedException; -import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java index 346eb54..77d1374 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.exception.CurationNotFoundException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.user.entity.User; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 287f974..ca50d5b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -4,7 +4,7 @@ import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; -import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java b/src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java deleted file mode 100644 index 27ff772..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/subscribe/CurationSubscribeService.java +++ /dev/null @@ -1,63 +0,0 @@ -package BookPick.mvp.domain.curation.service.subscribe; - -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.subscribe.CurationSubscribeDto; -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.entity.CurationSubscribe; -import BookPick.mvp.domain.curation.exception.CurationNotFoundException; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.domain.curation.repository.subscribe.CurationSubscribeRepository; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.UserNotFoundException; -import BookPick.mvp.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class CurationSubscribeService { - private final CurationSubscribeRepository curationSubscribeRepository; - private final CurationRepository curationRepository; - private final UserRepository userRepository; - - - // 1. 큐레이션 구독 - @Transactional - public CurationSubscribeDto subscribe(Long userId, Long curationId) { - - // 1. 포스트 아이디 얻기 - Curation curation = curationRepository.findById(curationId) - .orElseThrow(CurationNotFoundException::new); - // 2. 유저 아이디 얻기 - User user = userRepository.findById(userId) - .orElseThrow(UserNotFoundException::new); - - // 3. 해당 유저가 구독했는지 체크 - // 3.1 유저의 큐레이션 구독 정보 가져오기 - Optional opt = curationSubscribeRepository.findByUserIdAndCurationId(userId, curationId); - - // 3.2 구독 정보가 없으면 생성 - if (opt.isEmpty()) { - CurationSubscribe curationSubscribe = CurationSubscribe.builder() - .user(user) - .curation(curation) - .build(); - - curationSubscribeRepository.save(curationSubscribe); - - return CurationSubscribeDto.from(curationSubscribe , true); - } - - // 3.2 구독 정보가 있으면 삭제 - else { - curationSubscribeRepository.delete(opt.get()); - - return CurationSubscribeDto.from(curation , false); - } - } - -} diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index cb51f74..79ae70b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; -import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; import BookPick.mvp.domain.curation.entity.Curation; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index 432bbab..c6f4137 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; diff --git a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java new file mode 100644 index 0000000..01ccd9d --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java @@ -0,0 +1,71 @@ +package BookPick.mvp.domain.user.controller.subscribe; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeReq; +import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeRes; +import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; +import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.user.enums.curator.CuratorSuccessCode; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; +import BookPick.mvp.global.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class CuratorSubscribeController { + + private final CurationService curationService; + private final CurrentUserCheck currentUserCheck; + private final CurationSubscribeService curationSubscribeService; + + // 1. 큐레이터 구독하기 + + + @PostMapping("/subscribe") + @Operation(summary = "큐레이션 구독", description = "큐레이션 구독 버튼을 누릅니다.", tags = {"Curation"}) + public ResponseEntity> subscribe( + @RequestBody @Valid CuratorSubscribeReq req, + @AuthenticationPrincipal CustomUserDetails currentUser){ + + currentUserCheck.isValidCurrentUser(currentUser); + + CuratorSubscribeRes curatorSubscribeRes = curationSubscribeService.subscribe(currentUser.getId(), req); + + if (curatorSubscribeRes.subscribed()) { + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(CuratorSuccessCode.CURATOR_SUBSCRIBE_SUCCESS, curatorSubscribeRes)); + } else { + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(CuratorSuccessCode.CURATOR_SUBSCRIBE_CANCLE_SUCCESS, curatorSubscribeRes)); + } + } + + + + + + +// @Operation(summary = "큐레이션 구독 리스트 제공", description = "사용자가 구독한 큐레이터 리스트를 제공합니다.", tags = {"Curation"}) +// @GetMapping("/subscribe/list") +// public ResponseEntity> getSubscribedCurations( +// @AuthenticationPrincipal @Valid CustomUserDetails currentUser, +// @RequestParam(required = false) Long cursor, +// @RequestParam(defaultValue = "10") int size) { +// +// currentUserCheck.isValidCurrentUser(currentUser); +// CurationListGetRes curationListGetRes = curationSubscribeService.getSubscribedCurations(currentUser.getId(), cursor, size); +// return ResponseEntity.ok() +// .body(ApiResponse.success(CurationSuccessCode.GET_CURATION_SUBSCRIBE_LIST_SUCCESS, curationListGetRes)); +// } +} + + + diff --git a/src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeReq.java b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeReq.java new file mode 100644 index 0000000..b1238df --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeReq.java @@ -0,0 +1,8 @@ +package BookPick.mvp.domain.user.dto.subscribe; + +public record CuratorSubscribeReq( + Long curatorId +) { + + +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeRes.java b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeRes.java new file mode 100644 index 0000000..09cdec5 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/CuratorSubscribeRes.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.dto.subscribe; + +import BookPick.mvp.domain.user.entity.CuratorSubscribe; +import BookPick.mvp.domain.user.entity.User; + +public record CuratorSubscribeRes( + Long curatorId, + boolean subscribed +){ + + + public static CuratorSubscribeRes from(User curator, boolean subscribed){ + return new CuratorSubscribeRes(curator.getId(),subscribed); + } + + public static CuratorSubscribeRes from(CuratorSubscribe curationSubscribe, boolean subscribed){ + return new CuratorSubscribeRes(curationSubscribe.getCurator().getId(),subscribed); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java b/src/main/java/BookPick/mvp/domain/user/entity/CuratorSubscribe.java similarity index 71% rename from src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java rename to src/main/java/BookPick/mvp/domain/user/entity/CuratorSubscribe.java index e1d1586..2e321dd 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/CurationSubscribe.java +++ b/src/main/java/BookPick/mvp/domain/user/entity/CuratorSubscribe.java @@ -1,15 +1,11 @@ -package BookPick.mvp.domain.curation.entity; +package BookPick.mvp.domain.user.entity; -import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; -import org.springframework.data.annotation.CreatedDate; - -import java.time.LocalDateTime; @Entity @Table( - name = "curation_subscribe", + name = "curator_subscribe", uniqueConstraints = { @UniqueConstraint( name = "uk_user_curator", @@ -22,7 +18,7 @@ @Builder @Getter @Setter -public class CurationSubscribe { +public class CuratorSubscribe { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java index b33bff9..49c20ce 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java @@ -10,7 +10,11 @@ public enum CuratorSuccessCode implements SuccessCodeInterface { - PASSWORD_CHANGE_SUCCESS(HttpStatus.OK, "비밀번호 변경을 성공하였습니다."); + CURATOR_SUBSCRIBE_SUCCESS(HttpStatus.CREATED, "큐레이션 구독을 성공적으로 실행하였습니다."), + CURATOR_SUBSCRIBE_CANCLE_SUCCESS(HttpStatus.OK, "큐레이션 구독 취소를 성공적으로 실행하였습니다."), + + GET_CURATOR_SUBSCRIBE_LIST_SUCCESS(HttpStatus.OK, "큐레이션 구독을 리스트를 성공적으로 조회하였습니다."); + private final HttpStatus status; private final String message; diff --git a/src/main/java/BookPick/mvp/domain/user/exception/subscribe/SelfSubscribeDeniedException.java b/src/main/java/BookPick/mvp/domain/user/exception/subscribe/SelfSubscribeDeniedException.java new file mode 100644 index 0000000..61e5288 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/subscribe/SelfSubscribeDeniedException.java @@ -0,0 +1,11 @@ +// SelfSubscribeDeniedException.java +package BookPick.mvp.domain.user.exception.subscribe; + +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class SelfSubscribeDeniedException extends BusinessException { + public SelfSubscribeDeniedException() { + super(ErrorCode.CURATION_ACCESS_DENIED); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java b/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java new file mode 100644 index 0000000..bcf6444 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java @@ -0,0 +1,14 @@ +package BookPick.mvp.domain.user.repository.subscribe; + +import BookPick.mvp.domain.user.entity.CuratorSubscribe; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface CurationSubscribeRepository extends JpaRepository { + + Optional findByUserIdAndCuratorId(Long userId, Long curatorId); + +} diff --git a/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java b/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java new file mode 100644 index 0000000..d6538f1 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java @@ -0,0 +1,79 @@ +package BookPick.mvp.domain.user.service.subscribe; + +import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeReq; +import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeRes; +import BookPick.mvp.domain.user.entity.CuratorSubscribe; +import BookPick.mvp.domain.user.exception.curator.CuratorNotFoundException; +import BookPick.mvp.domain.user.repository.subscribe.CurationSubscribeRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationSubscribeService { + private final CurationSubscribeRepository curationSubscribeRepository; + private final UserRepository userRepository; + + + // 1. 큐레이션 구독 + @Transactional + public CuratorSubscribeRes subscribe(Long userId, CuratorSubscribeReq req) { + + + // 2. 구독 신청한 유저 얻기 + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + // 3. 구독할 유저 얻기 + User curator = userRepository.findById(req.curatorId()) + .orElseThrow(CuratorNotFoundException::new); + + // 4. 해당 유저가 구독했는지 체크 + // 3.1 유저의 큐레이션 구독 정보 가져오기 + Optional opt = curationSubscribeRepository.findByUserIdAndCuratorId(userId, req.curatorId()); + + // 3.2 구독 정보가 없으면 생성 + if (opt.isEmpty()) { + CuratorSubscribe curationSubscribe = CuratorSubscribe.builder() + .user(user) + .curator(curator) + .build(); + + curationSubscribeRepository.save(curationSubscribe); + + return CuratorSubscribeRes.from(curationSubscribe , true); + } + + // 3.2 구독 정보가 있으면 삭제 + else { + curationSubscribeRepository.delete(opt.get()); + + return CuratorSubscribeRes.from(curator , false); + } + } + +// @Transactional(readOnly = true) +// public CurationListGetRes getSubscribedCurations(Long userId, Long cursor, int size) { +// Pageable pageable = PageRequest.of(0, size + 1); +// List curations = curationSubscribeRepository.findSubscribedCurationsByUserId(userId, cursor, pageable); +// +// boolean hasNext = curations.size() > size; +// Long nextCursor = null; +// if (hasNext) { +// nextCursor = curations.get(size).getId(); +// curations = curations.subList(0, size); +// } +// +// List content = curations.stream() +// .map(CurationContentRes::from) +// .collect(Collectors.toList()); +// +// return CurationListGetRes.from(SortType.SORT_LATEST, content, hasNext, nextCursor); +// } +} diff --git a/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java b/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java index bae1528..bdd0c75 100644 --- a/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java +++ b/src/main/java/BookPick/mvp/global/HyperParam/Defaults.java @@ -1,6 +1,6 @@ package BookPick.mvp.global.HyperParam; -import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.enums.common.SortType; import lombok.AllArgsConstructor; import lombok.Getter; From 080098db175dbb41c20c2b2cb15daa13fdba44c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 17:39:47 +0900 Subject: [PATCH 152/291] chore: meaningless commit --- .../curation/controller/list/CurationListController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index c820b59..e6778ab 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -1,10 +1,9 @@ -// CurationListController.java에 추가 package BookPick.mvp.domain.curation.controller.list; import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; -import BookPick.mvp.domain.curation.enums.SortType; +import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.service.list.CurationListService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; From 0f115281a76265871ec4c9063389a26ab8556b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 18:10:20 +0900 Subject: [PATCH 153/291] =?UTF-8?q?[Feat]=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B5=AC=EB=8F=85=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subscribe/CuratorSubscribeController.java | 35 +++++++--------- .../enums/curator/CuratorSuccessCode.java | 2 +- .../CurationSubscribeRepository.java | 6 ++- .../subscribe/CurationSubscribeService.java | 42 ++++++++++--------- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java index 01ccd9d..79b1a0a 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java @@ -3,7 +3,7 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeReq; import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeRes; -import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; +import BookPick.mvp.domain.user.dto.subscribe.SubscribedCuratorPageRes; import BookPick.mvp.domain.curation.service.base.CurationService; import BookPick.mvp.domain.user.enums.curator.CuratorSuccessCode; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; @@ -30,7 +30,7 @@ public class CuratorSubscribeController { @PostMapping("/subscribe") - @Operation(summary = "큐레이션 구독", description = "큐레이션 구독 버튼을 누릅니다.", tags = {"Curation"}) + @Operation(summary = "큐레이터 구독/취소", description = "큐레이터 구독 버튼을 누릅니다. 이미 구독된 상태라면 구독이 취소됩니다.", tags = {"Subscribe"}) public ResponseEntity> subscribe( @RequestBody @Valid CuratorSubscribeReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ @@ -49,23 +49,16 @@ public ResponseEntity> subscribe( } + @Operation(summary = "큐레이터 구독 리스트 제공", description = "사용자가 구독한 큐레이터 리스트를 제공합니다.", tags = {"Subscribe"}) + @GetMapping("/subscribe/curators") + public ResponseEntity> getSubscribedCurators( + @AuthenticationPrincipal @Valid CustomUserDetails currentUser, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { - - - -// @Operation(summary = "큐레이션 구독 리스트 제공", description = "사용자가 구독한 큐레이터 리스트를 제공합니다.", tags = {"Curation"}) -// @GetMapping("/subscribe/list") -// public ResponseEntity> getSubscribedCurations( -// @AuthenticationPrincipal @Valid CustomUserDetails currentUser, -// @RequestParam(required = false) Long cursor, -// @RequestParam(defaultValue = "10") int size) { -// -// currentUserCheck.isValidCurrentUser(currentUser); -// CurationListGetRes curationListGetRes = curationSubscribeService.getSubscribedCurations(currentUser.getId(), cursor, size); -// return ResponseEntity.ok() -// .body(ApiResponse.success(CurationSuccessCode.GET_CURATION_SUBSCRIBE_LIST_SUCCESS, curationListGetRes)); -// } -} - - - + currentUserCheck.isValidCurrentUser(currentUser); + SubscribedCuratorPageRes subscribedCuratorPageRes = curationSubscribeService.getSubscribedCurators(currentUser.getId(), page, size); + return ResponseEntity.ok() + .body(ApiResponse.success(CuratorSuccessCode.GET_CURATOR_SUBSCRIBE_LIST_SUCCESS, subscribedCuratorPageRes)); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java index 49c20ce..d4a54bd 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/curator/CuratorSuccessCode.java @@ -13,7 +13,7 @@ public enum CuratorSuccessCode implements SuccessCodeInterface { CURATOR_SUBSCRIBE_SUCCESS(HttpStatus.CREATED, "큐레이션 구독을 성공적으로 실행하였습니다."), CURATOR_SUBSCRIBE_CANCLE_SUCCESS(HttpStatus.OK, "큐레이션 구독 취소를 성공적으로 실행하였습니다."), - GET_CURATOR_SUBSCRIBE_LIST_SUCCESS(HttpStatus.OK, "큐레이션 구독을 리스트를 성공적으로 조회하였습니다."); + GET_CURATOR_SUBSCRIBE_LIST_SUCCESS(HttpStatus.OK, "큐레이터 구독 리스트를 성공적으로 조회하였습니다."); private final HttpStatus status; diff --git a/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java b/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java index bcf6444..c20cf20 100644 --- a/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java +++ b/src/main/java/BookPick/mvp/domain/user/repository/subscribe/CurationSubscribeRepository.java @@ -1,14 +1,18 @@ package BookPick.mvp.domain.user.repository.subscribe; import BookPick.mvp.domain.user.entity.CuratorSubscribe; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository -public interface CurationSubscribeRepository extends JpaRepository { +public interface CurationSubscribeRepository extends JpaRepository { Optional findByUserIdAndCuratorId(Long userId, Long curatorId); + Page findByUserIdOrderByIdDesc(Long userId, Pageable pageable); + } diff --git a/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java b/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java index d6538f1..146dec1 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java @@ -2,17 +2,25 @@ import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeReq; import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeRes; +import BookPick.mvp.domain.user.dto.subscribe.SubscribedCuratorPageRes; +import BookPick.mvp.domain.user.dto.subscribe.SubscribedCuratorRes; import BookPick.mvp.domain.user.entity.CuratorSubscribe; import BookPick.mvp.domain.user.exception.curator.CuratorNotFoundException; import BookPick.mvp.domain.user.repository.subscribe.CurationSubscribeRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.global.dto.PageInfo; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -58,22 +66,18 @@ public CuratorSubscribeRes subscribe(Long userId, CuratorSubscribeReq req) { } } -// @Transactional(readOnly = true) -// public CurationListGetRes getSubscribedCurations(Long userId, Long cursor, int size) { -// Pageable pageable = PageRequest.of(0, size + 1); -// List curations = curationSubscribeRepository.findSubscribedCurationsByUserId(userId, cursor, pageable); -// -// boolean hasNext = curations.size() > size; -// Long nextCursor = null; -// if (hasNext) { -// nextCursor = curations.get(size).getId(); -// curations = curations.subList(0, size); -// } -// -// List content = curations.stream() -// .map(CurationContentRes::from) -// .collect(Collectors.toList()); -// -// return CurationListGetRes.from(SortType.SORT_LATEST, content, hasNext, nextCursor); -// } -} + // 2. 큐레이터 구독 리스트 반환 + @Transactional(readOnly = true) + public SubscribedCuratorPageRes getSubscribedCurators(Long userId, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + Page subscribesPage = curationSubscribeRepository.findByUserIdOrderByIdDesc(userId, pageable); + + List content = subscribesPage.getContent().stream() + .map(subscribe -> SubscribedCuratorRes.from(subscribe.getCurator())) + .collect(Collectors.toList()); + + PageInfo pageInfo = PageInfo.of(subscribesPage); + + return SubscribedCuratorPageRes.of(content, pageInfo); + } +} \ No newline at end of file From f936de0b35d0f361f180c7015c5a7520dfcb57fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 18:12:05 +0900 Subject: [PATCH 154/291] =?UTF-8?q?feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B5=AC=EB=8F=85=20=EB=AA=A9=EB=A1=9D=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자가 구독한 큐레이터 목록을 조회하는 API를 추가합니다. - 기존 무한 스크롤(커서 기반) 방식에서 페이지 번호 기반의 페이지네이션으로 변경합니다. - GET /api/v1/subscribe/curators - 요청: page (0-based), size - 응답: 큐레이터 목록 및 페이지 정보 (currentPage, totalPages, totalElements, hasNext) - 관련 DTO, Repository, Service, Controller 로직을 수정 및 추가했습니다. --- .../subscribe/SubscribedCuratorPageRes.java | 19 +++++++++++++++++ .../dto/subscribe/SubscribedCuratorRes.java | 21 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorPageRes.java create mode 100644 src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorRes.java diff --git a/src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorPageRes.java b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorPageRes.java new file mode 100644 index 0000000..f62763b --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorPageRes.java @@ -0,0 +1,19 @@ +package BookPick.mvp.domain.user.dto.subscribe; + +import BookPick.mvp.global.dto.PageInfo; +import lombok.Builder; + +import java.util.List; + +@Builder +public record SubscribedCuratorPageRes( + List curators, + PageInfo pageInfo +) { + public static SubscribedCuratorPageRes of(List curators, PageInfo pageInfo) { + return SubscribedCuratorPageRes.builder() + .curators(curators) + .pageInfo(pageInfo) + .build(); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorRes.java b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorRes.java new file mode 100644 index 0000000..50acbfd --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/dto/subscribe/SubscribedCuratorRes.java @@ -0,0 +1,21 @@ +package BookPick.mvp.domain.user.dto.subscribe; + +import BookPick.mvp.domain.user.entity.User; +import lombok.Builder; + +@Builder +public record SubscribedCuratorRes( + Long curatorId, + String nickname, + String profileImageUrl, + String bio +) { + public static SubscribedCuratorRes from(User curator) { + return SubscribedCuratorRes.builder() + .curatorId(curator.getId()) + .nickname(curator.getNickname()) + .profileImageUrl(curator.getProfileImageUrl()) + .bio(curator.getBio()) + .build(); + } +} From b34c8efb44308a1003edb649326bbfc88431c2c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 19:44:24 +0900 Subject: [PATCH 155/291] =?UTF-8?q?[Feat]=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EB=8A=94=20=ED=81=90=EB=A0=88=EC=9D=B4=ED=84=B0=EC=9D=98=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=EC=97=AC=EB=B6=80=EB=A5=BC=20=EC=95=8C=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=EA=B0=80=20=EC=9E=88=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90,=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=8B=A8=EA=B1=B0=EC=A1=B0=ED=9A=8C=20DTO=EC=97=90=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=9E=91=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=EC=97=AC=EB=B6=80=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/base/get/one/CurationGetRes.java | 16 ++++++++++++---- .../curation/service/base/CurationService.java | 11 ++++++++++- .../subscribe/CurationSubscribeService.java | 14 ++++++++++++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index a88a96d..b9bda69 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -2,6 +2,7 @@ package BookPick.mvp.domain.curation.dto.base.get.one; import BookPick.mvp.domain.curation.entity.Curation; + import java.time.LocalDateTime; import java.util.List; @@ -11,6 +12,7 @@ public record CurationGetRes( String nickName, String profileImageUrl, String introduction, + boolean subscribed, String title, ThumbnailInfo thumbnail, BookInfo book, @@ -24,13 +26,14 @@ public record CurationGetRes( LocalDateTime createdAt, LocalDateTime updatedAt ) { - public static CurationGetRes from(Curation curation, boolean isLiked) { + public static CurationGetRes from(Curation curation, boolean subscribed, boolean isLiked) { return new CurationGetRes( curation.getId(), curation.getUser().getId(), curation.getUser().getNickname(), curation.getUser().getProfileImageUrl(), curation.getUser().getBio(), + subscribed, curation.getTitle(), new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), @@ -46,8 +49,13 @@ public static CurationGetRes from(Curation curation, boolean isLiked) { ); } - public record ThumbnailInfo(String imageUrl, String imageColor) {} - public record BookInfo(String title, String author, String isbn) {} + public record ThumbnailInfo(String imageUrl, String imageColor) { + } + + public record BookInfo(String title, String author, String isbn) { + } + public record RecommendInfo(List moods, List genres, - List keywords, List styles) {} + List keywords, List styles) { + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 16773ba..ae69ed5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -16,9 +16,11 @@ import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; +import BookPick.mvp.domain.user.entity.CuratorSubscribe; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -36,6 +38,7 @@ public class CurationService { private final UserRepository userRepository; private final CurationFetcher curationFetcher; private final CurationPageHandler pageHandler; + private final CurationSubscribeService curationSubscribeService; // -- 큐레이션 등록 -- @@ -71,6 +74,7 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { @Transactional public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { boolean isLikedCuration = false; + boolean isSubscribedCurator = false; Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); @@ -86,7 +90,12 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http } } - return CurationGetRes.from(curation, isLikedCuration); + // 2. 큐레이터 구독 여부 조회 + if (user != null) { + isSubscribedCurator = curationSubscribeService.isSubscribeCurator(user.getId(), curation.getUser().getId()); + } + + return CurationGetRes.from(curation, isSubscribedCurator, isLikedCuration); } diff --git a/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java b/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java index 146dec1..7a83589 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeService.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.swing.text.html.Option; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -29,7 +30,7 @@ public class CurationSubscribeService { private final UserRepository userRepository; - // 1. 큐레이션 구독 + // 1. 큐레이터 구독 @Transactional public CuratorSubscribeRes subscribe(Long userId, CuratorSubscribeReq req) { @@ -66,7 +67,16 @@ public CuratorSubscribeRes subscribe(Long userId, CuratorSubscribeReq req) { } } - // 2. 큐레이터 구독 리스트 반환 + @Transactional(readOnly = true) + public boolean isSubscribeCurator(Long userId, Long CuratorId){ + Optional subInfo = curationSubscribeRepository.findByUserIdAndCuratorId(userId,CuratorId); + if(subInfo.isPresent()){ + return true; + } + return false; + } + + // 3. 큐레이터 구독 리스트 반환 @Transactional(readOnly = true) public SubscribedCuratorPageRes getSubscribedCurators(Long userId, int page, int size) { Pageable pageable = PageRequest.of(page, size); From d28cf624ff55c6f5da3cf08e7d6871cd9db6c74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 20:26:43 +0900 Subject: [PATCH 156/291] =?UTF-8?q?[Feat]=20=EA=B0=9C=EB=B0=9C=EC=9A=A9=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EB=B9=A0=EB=A5=B8=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20CI/CD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..1245859 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,69 @@ +name: BookPick BE CI/CD (v 1.0) + + + +on: + push: + branches: + - develop + pull-request: + branches: + - develop + +jobs: + test: + runs-on: ubuntu-latest + steps: + + - name : 1. checkout repo + uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 + +# - name: 2. run test +# run: ./gradlew test + + - name: 3. set up JDK 21 + uses: actions/setup-java@v2 + with: + java-version: 21 + distribution: 'temurin' + + - name: 4. grant execute permission for gradlew + run: chmod +x gradlew + + + build-and-push: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 + + - name: Docker Hub Login + uses: docker/login-action@v3 + with: + username: ${{secrets.DOCKER_USERNAME}} + password: ${{secrets.DOCKER_TOKEN}} + + - name: Spring Image Build and Push + run: | + docker build --platform linux/amd64 \ + -t ${{ secrets.DOCKER_USERNAME }}/${{secrets.IMAGE}} \ + --push . + deploy: + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Set up SSH # 1. SSH 설정 + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 + run: | + ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} 'bash -s' < Date: Sun, 30 Nov 2025 20:27:43 +0900 Subject: [PATCH 157/291] =?UTF-8?q?[Feat]=20CI/CD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1245859..bb1c95d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: BookPick BE CI/CD (v 1.0) +name: BookPick BE CI/CD (v 1.1) From 2a4e8198513118f7e3b5f6d1c5303d617b6191cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 20:28:42 +0900 Subject: [PATCH 158/291] =?UTF-8?q?[Feat]=20CI/CD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bb1c95d..3a7b362 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,7 +6,7 @@ on: push: branches: - develop - pull-request: + pull_request: branches: - develop From 84c94beebbc789be71192281f02aeed303fe7eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 20:38:45 +0900 Subject: [PATCH 159/291] =?UTF-8?q?[Feat]=20CI/CD=20YMAL=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EB=88=84=EB=9D=BD=EB=90=98=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EC=96=B4=EC=84=9C=20=EB=B9=8C=EB=93=9C=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 50 +++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3a7b362..5bcb60e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,39 +11,47 @@ on: - develop jobs: - test: - runs-on: ubuntu-latest - steps: - - - name : 1. checkout repo - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - -# - name: 2. run test -# run: ./gradlew test - - - name: 3. set up JDK 21 - uses: actions/setup-java@v2 - with: - java-version: 21 - distribution: 'temurin' - - - name: 4. grant execute permission for gradlew - run: chmod +x gradlew +# test: +# runs-on: ubuntu-latest +# steps: +# +# - name : 1. checkout repo +# uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 +# +## - name: 2. run test +## run: ./gradlew test +# +# - name: 3. set up JDK 21 +# uses: actions/setup-java@v2 +# with: +# java-version: 21 +# distribution: 'temurin' +# +# - name: 4. grant execute permission for gradlew +# run: chmod +x gradlew build-and-push: - needs: test +# needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - - name: Docker Hub Login + - name: 1. gradlew 권한 변경 + run: | + chmod +x gradlew + + - name: 2. 빌드 Spring Boot (bootJar) + run: | + ./gradlew build -x test + + - name: 3. Docker Hub Login uses: docker/login-action@v3 with: username: ${{secrets.DOCKER_USERNAME}} password: ${{secrets.DOCKER_TOKEN}} - - name: Spring Image Build and Push + - name: 4. Spring Image Build and Push run: | docker build --platform linux/amd64 \ -t ${{ secrets.DOCKER_USERNAME }}/${{secrets.IMAGE}} \ From b086d2b37e9c87b1b4533931d6253ffbb1f0daab Mon Sep 17 00:00:00 2001 From: khan <123632194+won-seoop@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:11:45 +0900 Subject: [PATCH 160/291] =?UTF-8?q?[Feat]=20=EB=B9=A0=EB=A5=B8=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=ED=99=98=EA=B2=BD=EC=9A=B8=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Dockerfile=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6168db6..3bb3da1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,23 @@ -FROM eclipse-temurin:17-jdk-alpine -ARG JAR_FILE=build/libs/*.jar -COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] \ No newline at end of file + +# 1단계: 빌드 스테이지 +FROM --platform=linux/amd64 gradle:8.5-jdk21-alpine AS builder + +WORKDIR /app +COPY . . + +RUN chmod +x ./gradlew + +# Gradle 캐시 최적화 & JAR 만들기 +RUN ./gradlew clean bootJar --no-daemon + +# 2단계: 런타임 스테이지 +FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine + +WORKDIR /app + +# builder에서 jar만 복사 +COPY --from=builder /app/build/libs/*.jar app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] From ee675e0db84235e44929171641cf1499d841e451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 22:13:14 +0900 Subject: [PATCH 161/291] =?UTF-8?q?[Feat]=20=EB=A1=9C=EC=BB=AC=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=ED=8C=8C=EC=9D=BC=20=EB=B0=98=EC=98=81=20=EB=B0=8F=20?= =?UTF-8?q?CI/CD=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 3bb3da1..9f993cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ - # 1단계: 빌드 스테이지 FROM --platform=linux/amd64 gradle:8.5-jdk21-alpine AS builder From cd4fcf502906e4c99244551ecd23738fe6d5f495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 30 Nov 2025 22:14:22 +0900 Subject: [PATCH 162/291] =?UTF-8?q?[Feat]=20=EB=8F=84=EC=BB=A4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EA=B3=BC=20Github=20actions=20yaml=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5bcb60e..ba2c4c1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,13 +37,6 @@ jobs: steps: - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - - name: 1. gradlew 권한 변경 - run: | - chmod +x gradlew - - - name: 2. 빌드 Spring Boot (bootJar) - run: | - ./gradlew build -x test - name: 3. Docker Hub Login uses: docker/login-action@v3 From ad8211ed36eeb167151346a572d2b3278d5a24d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Mon, 1 Dec 2025 22:19:04 +0900 Subject: [PATCH 163/291] =?UTF-8?q?[Feat]=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=EC=8B=9C,?= =?UTF-8?q?=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=95=84=EB=8B=8C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20=EC=B1=85=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=AA=A8=EB=A5=B4=EA=B2=8C=ED=95=A0=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=EC=84=B1=EC=9D=B4=20=EC=9E=88=EC=97=88=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90,=20=EB=B3=B8=EC=9D=B8=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=8B=9C=EB=A7=8C=20=EC=B1=85=20=EC=A0=95=EB=B3=B4=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=ED=95=98=EA=B2=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/base/get/one/CurationGetRes.java | 23 +++++++++++++ .../repository/CurationRepository.java | 32 +++++++++++-------- .../service/base/CurationService.java | 16 ++++++---- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index b9bda69..d6d49db 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -27,6 +27,28 @@ public record CurationGetRes( LocalDateTime updatedAt ) { public static CurationGetRes from(Curation curation, boolean subscribed, boolean isLiked) { + return new CurationGetRes( + curation.getId(), + curation.getUser().getId(), + curation.getUser().getNickname(), + curation.getUser().getProfileImageUrl(), + curation.getUser().getBio(), + subscribed, + curation.getTitle(), + new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), + null, + curation.getReview(), + new RecommendInfo(curation.getMoods(), curation.getGenres(), + curation.getKeywords(), curation.getStyles()), + isLiked, + curation.getLikeCount(), + curation.getViewCount(), + curation.getCommentCount(), + curation.getCreatedAt(), + curation.getUpdatedAt() + ); + } + public static CurationGetRes fromOwnerView(Curation curation, boolean subscribed, boolean isLiked) { return new CurationGetRes( curation.getId(), curation.getUser().getId(), @@ -49,6 +71,7 @@ public static CurationGetRes from(Curation curation, boolean subscribed, boolean ); } + public record ThumbnailInfo(String imageUrl, String imageColor) { } diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 040dada..02c4473 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -32,24 +32,30 @@ public interface CurationRepository extends JpaRepository { // Gemini 추천 결과로 큐레이션 찾기 @Query(""" - SELECT DISTINCT c FROM Curation c - LEFT JOIN c.moods m - LEFT JOIN c.genres g - LEFT JOIN c.keywords k - LEFT JOIN c.styles s - WHERE c.deletedAt IS NULL - AND (m IN :moods OR g IN :genres OR k IN :keywords OR s IN :styles) - ORDER BY c.popularityScore DESC - """) + SELECT DISTINCT c FROM Curation c + LEFT JOIN c.moods m + LEFT JOIN c.genres g + LEFT JOIN c.keywords k + LEFT JOIN c.styles s + WHERE c.deletedAt IS NULL + AND (m IN :moods OR g IN :genres OR k IN :keywords OR s IN :styles) + ORDER BY c.popularityScore DESC + """) List findByRecommendation( - @Param("moods") List moods, - @Param("genres") List genres, - @Param("keywords") List keywords, - @Param("styles") List styles + @Param("moods") List moods, + @Param("genres") List genres, + @Param("keywords") List keywords, + @Param("styles") List styles ); Optional findByUserIdAndId(Long userId, Long id); Long user(User user); + + + // 7. + @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id = :id") + Optional findByIdWithUser(@Param("id") Long id); + } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index ae69ed5..1142b87 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -36,8 +36,6 @@ public class CurationService { private final CurationRepository curationRepository; private final CurationLikeRepository curationLikeRepository; private final UserRepository userRepository; - private final CurationFetcher curationFetcher; - private final CurationPageHandler pageHandler; private final CurationSubscribeService curationSubscribeService; @@ -75,24 +73,28 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { boolean isLikedCuration = false; boolean isSubscribedCurator = false; + CurationGetRes res; - Curation curation = curationRepository.findById(curationId) + Curation curation = curationRepository.findByIdWithUser(curationId) .orElseThrow(CurationNotFoundException::new); curation.increaseViewCount(); // 큐레이션 조회수 +1 - // 1. 좋아요 정보 찾기 if (user != null) { + // 1. 좋아요 정보 찾기 Optional curationLike = curationLikeRepository.findByUserIdAndCurationId(user.getId(), curationId); if (curationLike.isPresent()) { isLikedCuration = true; } - } - // 2. 큐레이터 구독 여부 조회 - if (user != null) { + // 2. 큐레이터 구독 여부 조회 isSubscribedCurator = curationSubscribeService.isSubscribeCurator(user.getId(), curation.getUser().getId()); + + // 3. 큐레이션 작성자면 책 정보 넣어서 큐레이션 반환 + if (curation.getUser().getId().equals(user.getId())) { + return CurationGetRes.fromOwnerView(curation, isSubscribedCurator, isLikedCuration); + } } return CurationGetRes.from(curation, isSubscribedCurator, isLikedCuration); From b9c92f5a619004e4f6f69a22caf19f939e41c872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Mon, 1 Dec 2025 22:24:11 +0900 Subject: [PATCH 164/291] =?UTF-8?q?[Chore]=20Main=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EA=B0=80=20=EC=98=AC=EB=9D=BC=EA=B1=B8=20?= =?UTF-8?q?=EA=B2=83=EC=9D=B4=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90,=20ma?= =?UTF-8?q?in=EC=97=90=20pr=20=EB=B0=8F=20push=EC=8B=9C=20github=20actions?= =?UTF-8?q?=20ci-cd=20=EC=9E=91=EB=8F=99=EB=90=98=EA=B2=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba2c4c1..ae2b2f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,10 +5,10 @@ name: BookPick BE CI/CD (v 1.1) on: push: branches: - - develop + - main pull_request: branches: - - develop + - main jobs: # test: From 6e63714431d4e0bbe17cb7c55d280646023a0bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 2 Dec 2025 19:35:34 +0900 Subject: [PATCH 165/291] =?UTF-8?q?feat=20:=20MLP=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=EB=95=90,=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=A4=91=EC=9D=B4=20?= =?UTF-8?q?=EC=95=84=EB=8B=88=EB=8B=88=20develop=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=EC=97=90=20PR=20=EB=98=90=EB=8A=94=20push=20=EC=8B=9C?= =?UTF-8?q?=20=EB=B0=94=EB=A1=9C=20=EB=B0=B0=ED=8F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ae2b2f4..ba2c4c1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,10 +5,10 @@ name: BookPick BE CI/CD (v 1.1) on: push: branches: - - main + - develop pull_request: branches: - - main + - develop jobs: # test: From 5e40cb364f4563eb847abdbbb0ff100132987a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 2 Dec 2025 20:16:55 +0900 Subject: [PATCH 166/291] =?UTF-8?q?refactor=20:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=ED=95=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + .../controller/list/CurationListController.java | 11 ++++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 8af972c..72eafb7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /gradlew text eol=lf + *.bat text eol=crlf *.jar binary diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index e6778ab..151266b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.service.list.CurationListService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -20,6 +21,7 @@ public class CurationListController { private final CurationListService curationListService; + private final CurrentUserCheck currentUserCheck; @Operation(summary = "큐레이션 목록 조회", description = "최신순 / 인기순 / 사용자 취향 유사도 순", tags = {"Curation"}) @@ -31,9 +33,8 @@ public ResponseEntity> getCurations( @AuthenticationPrincipal @Valid CustomUserDetails currentUser ) { - if(currentUser==null){ - throw new InvalidTokenTypeException(); - } + currentUserCheck.isValidCurrentUser(currentUser); + // 1. SortType 변환 SortType sortType = SortType.fromValue(sort); @@ -46,10 +47,6 @@ public ResponseEntity> getCurations( } - - - - } From 1272701404410a0a5f48d9026a8bed4447e78779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 2 Dec 2025 20:35:44 +0900 Subject: [PATCH 167/291] chore: meaningless commit --- .../mvp/domain/curation/service/list/CurationListService.java | 4 +++- .../domain/curation/util/list/fetcher/CurationFetcher.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index ca50d5b..848e852 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -29,6 +29,8 @@ public class CurationListService { // 1. 큐레이션 리스트 조회 public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long userId) { + + // 1. 내 취향 유사도 순 O if (sortType == SortType.SORT_SIMILARITY) { // 1. 유저 독서 취향 반환 @@ -62,7 +64,7 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, return CurationListGetRes.from(sortType, content, hasNext, nextCursor); } - // 1) 큐레이션 페이징해서 구분 + // 2) 내 취향 유사도순 X List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null); CursorPage page = pageHandler.createCursorPage(curations, size); List content = pageHandler.convertToContentRes(page.getContent()); diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index c6f4137..a41acc5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -36,7 +36,7 @@ public List fetchCurations(Long userId, SortType sortType, Long cursor return curationRepository.findAllByOrderByCreatedAtDesc(pageable); // 취향 유사도 만들기 전까진 최신순 } - // 2) 분류 기준에 맞게 데이터 가져오기 + // 2) 🌟분류 기준 🌟 return switch (sortType) { case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); // 인기순 case SORT_LATEST -> curationRepository.findLatestCurations(cursor, pageable); // 최신순 From df3f5a85cb45e3fb4d1d668e5f3bdaa5e1ef0afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 2 Dec 2025 20:44:08 +0900 Subject: [PATCH 168/291] =?UTF-8?q?feat=20:=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=20=ED=96=97=EC=83=81=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B3=84=EB=8F=84=EC=9D=98=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 11 +--- .../delete/CurationListDeleteController.java | 57 +++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index d869479..6853748 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -69,16 +69,7 @@ public ResponseEntity> updateCuration( .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); } - @Operation(summary = "큐레이션 삭제", description = "큐레이션을 삭제합니다", tags = {"Curation"}) - @DeleteMapping("/{curationId}") - public ResponseEntity> deleteCuration( - @PathVariable Long curationId, - @AuthenticationPrincipal CustomUserDetails currentUser) { - Long userId = (currentUser == null) ? 2L : currentUser.getId(); - CurationDeleteRes res = curationService.removeCuration(userId, curationId); - return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); - } + } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java new file mode 100644 index 0000000..8d21344 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java @@ -0,0 +1,57 @@ +package BookPick.mvp.domain.curation.controller.base.delete; + + +import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; +import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import io.swagger.v3.oas.annotations.Operation; + +@RestController +@RequestMapping("/api/v1/curations") +@RequiredArgsConstructor +public class CurationListDeleteController { + + private final CurationService curationService; + private final CurrentUserCheck currentUserCheck; + + @Operation(summary = "큐레이션 삭제", description = "큐레이션을 삭제합니다", tags = {"Curation"}) + @DeleteMapping("/{curationId}") + public ResponseEntity> deleteCuration( + @PathVariable Long curationId, + @AuthenticationPrincipal CustomUserDetails currentUser) { + Long userId = (currentUser == null) ? 2L : currentUser.getId(); + CurationDeleteRes res = curationService.removeCuration(userId, curationId); + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); + } + + @Operation(summary = "큐레이션 리스트 삭제", description = "복수의 큐레이션들을 삭제합니다", tags = {"Curation"}) + @DeleteMapping + public ResponseEntity> deleteCurationList( + @AuthenticationPrincipal CustomUserDetails currentUser) { + Long userId = (currentUser == null) ? 2L : currentUser.getId(); + CurationDeleteRes res = curationService.removeCurations(userId, curationId); + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); + } +} + + + From 458bd10c2d51839f99912e58236a0436141c29a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 2 Dec 2025 21:12:56 +0900 Subject: [PATCH 169/291] =?UTF-8?q?feat=20:=20=EB=B3=B5=EC=88=98=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../delete/CurationListDeleteController.java | 22 +++-- .../base/delete/CurationListDeleteReq.java | 10 +++ .../base/delete/CurationListDeleteRes.java | 14 +++ .../repository/CurationRepository.java | 2 + .../service/base/CurationService.java | 12 --- .../base/delete/CurationDeleteService.java | 89 +++++++++++++++++++ 6 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteRes.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java index 8d21344..786e8a3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java @@ -5,11 +5,14 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteReq; +import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteRes; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.curation.service.base.delete.CurationDeleteService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; @@ -28,7 +31,7 @@ @RequiredArgsConstructor public class CurationListDeleteController { - private final CurationService curationService; + private final CurationDeleteService curationDeleteService; private final CurrentUserCheck currentUserCheck; @Operation(summary = "큐레이션 삭제", description = "큐레이션을 삭제합니다", tags = {"Curation"}) @@ -36,18 +39,23 @@ public class CurationListDeleteController { public ResponseEntity> deleteCuration( @PathVariable Long curationId, @AuthenticationPrincipal CustomUserDetails currentUser) { - Long userId = (currentUser == null) ? 2L : currentUser.getId(); - CurationDeleteRes res = curationService.removeCuration(userId, curationId); + + currentUserCheck.isValidCurrentUser(currentUser); + + CurationDeleteRes res = curationDeleteService.removeCuration(currentUser.getId(), curationId); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); } @Operation(summary = "큐레이션 리스트 삭제", description = "복수의 큐레이션들을 삭제합니다", tags = {"Curation"}) @DeleteMapping - public ResponseEntity> deleteCurationList( - @AuthenticationPrincipal CustomUserDetails currentUser) { - Long userId = (currentUser == null) ? 2L : currentUser.getId(); - CurationDeleteRes res = curationService.removeCurations(userId, curationId); + public ResponseEntity> deleteCurations( + @AuthenticationPrincipal CustomUserDetails currentUser, + @RequestBody CurationListDeleteReq req + ) { + currentUserCheck.isValidCurrentUser(currentUser); + + CurationListDeleteRes res = curationDeleteService.removeCurations(currentUser.getId(), req); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java new file mode 100644 index 0000000..c21a258 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java @@ -0,0 +1,10 @@ +// CurationDeleteRes.java +package BookPick.mvp.domain.curation.dto.base.delete; + +import java.time.LocalDateTime; +import java.util.List; + +public record CurationListDeleteReq( + List ids +) { +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteRes.java new file mode 100644 index 0000000..92079c2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteRes.java @@ -0,0 +1,14 @@ +// CurationDeleteRes.java +package BookPick.mvp.domain.curation.dto.base.delete; + +import java.time.LocalDateTime; +import java.util.List; + +public record CurationListDeleteRes( + List ids, + LocalDateTime deletedAt +) { + public static CurationListDeleteRes from(List ids, LocalDateTime deletedAt) { + return new CurationListDeleteRes(ids, deletedAt); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 02c4473..1c586c4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -57,5 +58,6 @@ List findByRecommendation( @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id = :id") Optional findByIdWithUser(@Param("id") Long id); + List findByIdIn(Collection ids); } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 1142b87..b7f4444 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -117,18 +117,6 @@ public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUp } - // -- 큐레이션 삭제 -- - @Transactional - public CurationDeleteRes removeCuration(Long userId, Long curationId) { - Curation curation = curationRepository.findById(curationId) - .orElseThrow(CurationNotFoundException::new); - - if (!curation.getUser().getId().equals(userId)) { - throw new CurationAccessDeniedException(); - } - curationRepository.delete(curation); - return CurationDeleteRes.from(curation.getId(), LocalDateTime.now()); - } } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java new file mode 100644 index 0000000..2dac81f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java @@ -0,0 +1,89 @@ +// CurationListService.java +package BookPick.mvp.domain.curation.service.base.delete; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteReq; +import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationDeleteService { + + private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; + private final UserRepository userRepository; + private final CurationSubscribeService curationSubscribeService; + + + // -- 큐레이션 하드 삭제 -- + @Transactional + public CurationDeleteRes removeCuration(Long userId, Long curationId) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + if (!curation.getUser().getId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + + curationRepository.delete(curation); + + return CurationDeleteRes.from(curation.getId(), LocalDateTime.now()); + } + + + // -- 큐레이션 복수 삭제 -- + @Transactional + public CurationListDeleteRes removeCurations(Long userId, CurationListDeleteReq req) { + + + + // 1. 큐레이션 Id들 가지고 큐레이션 리스트 찾기 + List curations = curationRepository.findByIdIn((req.ids())); + + // 2. 큐레이션들이 존재하지 않으면 삭제할 큐레이션을 찾을 수 없습니다. + if (curations.size() != req.ids().size()) { + throw new CurationNotFoundException(); + } + + + for (Curation curation : curations) { + if (!curation.getUser().getId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + curationRepository.delete(curation); + } + + List deletedIds = curations.stream() + .map(Curation::getId) + .toList(); + + + + + return CurationListDeleteRes.from(deletedIds,LocalDateTime.now()); +} +} From 5986512b63085cb9ecd9d85907fba6a42f562844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 2 Dec 2025 21:24:38 +0900 Subject: [PATCH 170/291] =?UTF-8?q?feat=20:=20=EB=B3=B5=EC=88=98=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base/delete/CurationListDeleteController.java | 5 +++-- .../domain/curation/dto/base/delete/CurationDeleteRes.java | 2 +- .../curation/dto/base/delete/CurationListDeleteReq.java | 2 +- .../domain/curation/enums/common/CurationSuccessCode.java | 6 +++++- .../curation/service/base/delete/CurationDeleteService.java | 4 ++-- .../BookPick/mvp/global/api/SuccessCode/SuccessCode.java | 3 +-- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java index 786e8a3..bc8fe5e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java @@ -11,6 +11,7 @@ import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; +import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; import BookPick.mvp.domain.curation.service.base.CurationService; import BookPick.mvp.domain.curation.service.base.delete.CurationDeleteService; import BookPick.mvp.domain.user.util.CurrentUserCheck; @@ -44,7 +45,7 @@ public ResponseEntity> deleteCuration( CurationDeleteRes res = curationDeleteService.removeCuration(currentUser.getId(), curationId); return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); + .body(ApiResponse.success(CurationSuccessCode.CURATION_DELETE_SUCCESS, res)); } @Operation(summary = "큐레이션 리스트 삭제", description = "복수의 큐레이션들을 삭제합니다", tags = {"Curation"}) @@ -57,7 +58,7 @@ public ResponseEntity> deleteCurations( CurationListDeleteRes res = curationDeleteService.removeCurations(currentUser.getId(), req); return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_DELETE_SUCCESS, res)); + .body(ApiResponse.success(CurationSuccessCode.CURATION_LIST_DELETE_SUCCESS, res)); } } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java index 235fd99..5de67ee 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationDeleteRes.java @@ -4,7 +4,7 @@ import java.time.LocalDateTime; public record CurationDeleteRes( - Long id, + Long curationIds, LocalDateTime deletedAt ) { public static CurationDeleteRes from(Long id, LocalDateTime deletedAt) { diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java index c21a258..e4d78c4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/delete/CurationListDeleteReq.java @@ -5,6 +5,6 @@ import java.util.List; public record CurationListDeleteReq( - List ids + List curationIds ) { } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java index 28f5d06..34d6738 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java @@ -15,9 +15,13 @@ public enum CurationSuccessCode implements SuccessCodeInterface { // 2. 좋아요 CURATION_LIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요를 성공적으로 실행하였습니다."), - CURATION_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."); + CURATION_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."), + // 3. 삭제 + CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."), + CURATION_LIST_DELETE_SUCCESS(HttpStatus.OK, "다수의 큐레이션을 성공적으로 삭제하였습니다."); + diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java index 2dac81f..0946066 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java @@ -62,10 +62,10 @@ public CurationListDeleteRes removeCurations(Long userId, CurationListDeleteReq // 1. 큐레이션 Id들 가지고 큐레이션 리스트 찾기 - List curations = curationRepository.findByIdIn((req.ids())); + List curations = curationRepository.findByIdIn((req.curationIds())); // 2. 큐레이션들이 존재하지 않으면 삭제할 큐레이션을 찾을 수 없습니다. - if (curations.size() != req.ids().size()) { + if (curations.size() != req.curationIds().size()) { throw new CurationNotFoundException(); } diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java index 8b721b8..5ea7526 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java @@ -38,8 +38,7 @@ public enum SuccessCode { CURATION_REGISTER_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."), CURATION_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 단건 조회하였습니다."), CURATION_LIST_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 리스트 조회하였습니다."), - CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."), - CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."); + CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."); private final HttpStatus status; private final String message; From 2eca261dbd8c8519311079658b308b71c2467a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:21:23 +0900 Subject: [PATCH 171/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9f993cd..737f976 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,16 @@ - # 1단계: 빌드 스테이지 -FROM --platform=linux/amd64 gradle:8.5-jdk21-alpine AS builder +FROM --platform=linux/amd64 eclipse-temurin:21-jdk AS builder WORKDIR /app COPY . . RUN chmod +x ./gradlew - -# Gradle 캐시 최적화 & JAR 만들기 RUN ./gradlew clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine WORKDIR /app - -# builder에서 jar만 복사 COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 From bb250afbd4bb84b819fc2b58f8754dd33a435c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:23:58 +0900 Subject: [PATCH 172/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=952?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 737f976..133b66d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,21 @@ # 1단계: 빌드 스테이지 -FROM --platform=linux/amd64 eclipse-temurin:21-jdk AS builder +FROM --platform=linux/amd64 gradle:8.5-jdk21-alpine AS builder WORKDIR /app COPY . . RUN chmod +x ./gradlew + +# Gradle 캐시 최적화 & JAR 만들기 RUN ./gradlew clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine WORKDIR /app + +# builder에서 jar만 복사 COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file From 422902398347800a1e944cc4b2cefc016b2babe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:26:42 +0900 Subject: [PATCH 173/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 133b66d..737f976 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,17 @@ # 1단계: 빌드 스테이지 -FROM --platform=linux/amd64 gradle:8.5-jdk21-alpine AS builder +FROM --platform=linux/amd64 eclipse-temurin:21-jdk AS builder WORKDIR /app COPY . . RUN chmod +x ./gradlew - -# Gradle 캐시 최적화 & JAR 만들기 RUN ./gradlew clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine WORKDIR /app - -# builder에서 jar만 복사 COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "app.jar"] From 9055b483517ca0b699d67f26603603ac00e2afe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:29:50 +0900 Subject: [PATCH 174/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 737f976..ed84fd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -# 1단계: 빌드 스테이지 -FROM --platform=linux/amd64 eclipse-temurin:21-jdk AS builder +# 1단계: 빌드 스테이지 (무조건 성공하는 조합) +FROM --platform=linux/amd64 gradle:8.5-jdk21 AS builder WORKDIR /app COPY . . RUN chmod +x ./gradlew -RUN ./gradlew clean bootJar --no-daemon +RUN gradle clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine From eb9287a1607b8722a674ac4cd65e257283cb2a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:33:17 +0900 Subject: [PATCH 175/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ed84fd5..0f96d91 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,8 @@ FROM --platform=linux/amd64 gradle:8.5-jdk21 AS builder WORKDIR /app COPY . . -RUN chmod +x ./gradlew -RUN gradle clean bootJar --no-daemon +RUN sudo chmod +x ./gradlew +RUN sudo gradle clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine From 60363fea639cb31511f52a4415fec394cade2019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:34:52 +0900 Subject: [PATCH 176/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0f96d91..e75a880 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM --platform=linux/amd64 gradle:8.5-jdk21 AS builder WORKDIR /app COPY . . -RUN sudo chmod +x ./gradlew +RUN chmod +x ./gradlew RUN sudo gradle clean bootJar --no-daemon # 2단계: 런타임 스테이지 From 5732593c27d0fe699286d7d17d7a228c9fbf58c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:37:34 +0900 Subject: [PATCH 177/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index e75a880..19b904a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app COPY . . RUN chmod +x ./gradlew -RUN sudo gradle clean bootJar --no-daemon +RUN gradle clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine From 99d707351a139b4dda4bff47227af2688e819f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:39:11 +0900 Subject: [PATCH 178/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 19b904a..da8cd8e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app COPY . . RUN chmod +x ./gradlew -RUN gradle clean bootJar --no-daemon +RUN ./gradlew clean bootJar --no-daemon # 2단계: 런타임 스테이지 FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine From a83a2588ccb4775a4a3a0c3f417cc15827b3acf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 19:43:05 +0900 Subject: [PATCH 179/291] =?UTF-8?q?feat=20:=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index da8cd8e..0ec4aa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,14 @@ -# 1단계: 빌드 스테이지 (무조건 성공하는 조합) -FROM --platform=linux/amd64 gradle:8.5-jdk21 AS builder +# 1단계: 빌드 스테이지 (Java 17) +FROM eclipse-temurin:17-jdk AS builder WORKDIR /app COPY . . -RUN chmod +x ./gradlew -RUN ./gradlew clean bootJar --no-daemon +RUN chmod +x ./gradlew +RUN ./gradlew clean bootJar --no-daemon -# 2단계: 런타임 스테이지 -FROM --platform=linux/amd64 eclipse-temurin:21-jdk-alpine +# 2단계: 런타임 스테이지 (Java 17 또는 21 둘 다 가능) +FROM eclipse-temurin:17-jdk-alpine WORKDIR /app COPY --from=builder /app/build/libs/*.jar app.jar From 8a24b816c896117a684c3d17d4fd30690367d304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 3 Dec 2025 20:16:43 +0900 Subject: [PATCH 180/291] =?UTF-8?q?fix=20:=20=EB=8D=94=ED=8B=B0=EC=B2=B4?= =?UTF-8?q?=ED=82=B9=EC=97=90=20=EC=9C=A0=EC=A0=80=20=EC=9E=90=EA=B8=B0?= =?UTF-8?q?=EC=86=8C=EA=B0=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=95=84=EC=84=9C=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98=EC=A0=95=EC=8B=9C,=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=9C=20=EC=9E=90=EA=B8=B0=EC=86=8C?= =?UTF-8?q?=EA=B0=9C=EA=B0=80=20=EB=B0=98=EC=98=81=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EC=95=98=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- .../user/controller/base/UserController.java | 180 +++++++++--------- .../controller/profile/ProfileController.java | 18 +- .../domain/user/service/base/UserService.java | 1 + 4 files changed, 109 insertions(+), 92 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba2c4c1..0c1001c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,7 +10,7 @@ on: branches: - develop -jobs: + jobs: # test: # runs-on: ubuntu-latest # steps: diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java index 4cba262..7e73a79 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -1,90 +1,90 @@ -package BookPick.mvp.domain.user.controller.base; - - -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.user.dto.base.UserReq; -import BookPick.mvp.domain.user.dto.base.UserRes; -import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; -import BookPick.mvp.domain.user.enums.user.UserSuccessCode; -import BookPick.mvp.domain.user.exception.common.NotHaveAdminRole; -import BookPick.mvp.domain.user.service.base.UserService; -import BookPick.mvp.domain.user.util.AdminManager; -import BookPick.mvp.global.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequiredArgsConstructor -@RequestMapping("/api/v1") -public class UserController { - - private final UserService userService; - private final AdminManager adminManager; - - // 유저 생성 - @PostMapping - @Operation(summary = "유저 추가", description = "새로운 유저를 추가합니다", tags = {"User"}) - public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, - @RequestBody @Valid UserReq req) { - if (adminManager.isAdmin(currentUser.getAuthorities())) { - throw new NotHaveAdminRole(); - } - - UserRes res = userService.CreateUser(req); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(UserSuccessCode.CREATE_USER_SUCCESS, res)); - } - - // 유저 조회 - @GetMapping - @Operation(summary = "유저 프로필 조회", description = "로그인한 사용자의 프로필을 조회합니다.", tags = {"User"}) - public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { - UserRes res = userService.userProfileGet(currentUser.getId()); - - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); - } - - - - // 유저 수정 - @PatchMapping - @Operation(summary = "유저 프로필 수정", description = "로그인한 사용자의 프로필을 수정합니다.", tags = {"User"}) - public ResponseEntity> updateUserProfile(@AuthenticationPrincipal CustomUserDetails currentUser - , @RequestBody @Valid UserReq req) { - UserRes res = userService.userProfileUpdate(currentUser.getId(), req); - - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(UserSuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); - } - - - // 유저 소프트 삭제 - @DeleteMapping("/soft") - @Operation(summary = "유저 프로필 소프트 삭제", description = "로그인한 사용자의 프로필을 임시 삭제합니다.", tags = {"User"}) - public ResponseEntity> softDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { - UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); - - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); - } - - - // 유저 하드 삭제 - @DeleteMapping("/hard") - @Operation(summary = "유저 프로필 하드 삭제", description = "로그인한 사용자의 프로필을 완전히 삭제합니다.", tags = {"User"}) - public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { - if (adminManager.isAdmin(currentUser.getAuthorities())) { - userService.hardDeleteUserProfile(currentUser.getId()); - } - - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, null)); - } -} +//package BookPick.mvp.domain.user.controller.base; +// +// +//import BookPick.mvp.domain.auth.service.CustomUserDetails; +//import BookPick.mvp.domain.user.dto.base.UserReq; +//import BookPick.mvp.domain.user.dto.base.UserRes; +//import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; +//import BookPick.mvp.domain.user.enums.user.UserSuccessCode; +//import BookPick.mvp.domain.user.exception.common.NotHaveAdminRole; +//import BookPick.mvp.domain.user.service.base.UserService; +//import BookPick.mvp.domain.user.util.AdminManager; +//import BookPick.mvp.global.api.ApiResponse; +//import io.swagger.v3.oas.annotations.Operation; +//import jakarta.validation.Valid; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.HttpStatus; +//import org.springframework.http.ResponseEntity; +//import org.springframework.security.core.annotation.AuthenticationPrincipal; +//import org.springframework.web.bind.annotation.*; +// +//@RestController +//@RequiredArgsConstructor +//@RequestMapping("/api/v1") +//public class UserController { +// +// private final UserService userService; +// private final AdminManager adminManager; +// +// // 유저 생성 +// @PostMapping +// @Operation(summary = "유저 추가", description = "새로운 유저를 추가합니다", tags = {"User"}) +// public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, +// @RequestBody @Valid UserReq req) { +// if (adminManager.isAdmin(currentUser.getAuthorities())) { +// throw new NotHaveAdminRole(); +// } +// +// UserRes res = userService.CreateUser(req); +// +// return ResponseEntity.status(HttpStatus.CREATED) +// .body(ApiResponse.success(UserSuccessCode.CREATE_USER_SUCCESS, res)); +// } +// +// // 유저 조회 +// @GetMapping +// @Operation(summary = "유저 프로필 조회", description = "로그인한 사용자의 프로필을 조회합니다.", tags = {"User"}) +// public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { +// UserRes res = userService.userProfileGet(currentUser.getId()); +// +// return ResponseEntity.status(HttpStatus.OK) +// .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); +// } +// +// +// +// // 유저 수정 +// @PatchMapping +// @Operation(summary = "유저 프로필 수정", description = "로그인한 사용자의 프로필을 수정합니다.", tags = {"User"}) +// public ResponseEntity> updateUserProfile(@AuthenticationPrincipal CustomUserDetails currentUser +// , @RequestBody @Valid UserReq req) { +// UserRes res = userService.userProfileUpdate(currentUser.getId(), req); +// +// return ResponseEntity.status(HttpStatus.OK) +// .body(ApiResponse.success(UserSuccessCode.UPDATE_MY_PROFILE_SUCCESS, res)); +// } +// +// +// // 유저 소프트 삭제 +// @DeleteMapping("/soft") +// @Operation(summary = "유저 프로필 소프트 삭제", description = "로그인한 사용자의 프로필을 임시 삭제합니다.", tags = {"User"}) +// public ResponseEntity> softDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { +// UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); +// +// return ResponseEntity.status(HttpStatus.OK) +// .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, res)); +// } +// +// +// // 유저 하드 삭제 +// @DeleteMapping("/hard") +// @Operation(summary = "유저 프로필 하드 삭제", description = "로그인한 사용자의 프로필을 완전히 삭제합니다.", tags = {"User"}) +// public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { +// if (adminManager.isAdmin(currentUser.getAuthorities())) { +// userService.hardDeleteUserProfile(currentUser.getId()); +// } +// +// return ResponseEntity.status(HttpStatus.OK) +// .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, null)); +// } +//} diff --git a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java index cccad6c..ccdfdd8 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/profile/ProfileController.java @@ -9,6 +9,7 @@ import BookPick.mvp.domain.user.service.base.UserService; import BookPick.mvp.domain.user.util.AdminManager; import BookPick.mvp.global.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -27,6 +28,7 @@ public class ProfileController { // 1. 프로필 조회 @GetMapping("/me") + @Operation(summary = "유저 프로필 조회", description = "로그인한 사용자의 프로필을 조회합니다.", tags = {"User"}) public ResponseEntity> getUseProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { UserRes res = userService.userProfileGet(currentUser.getId()); @@ -37,6 +39,7 @@ public ResponseEntity> getUseProfile(@AuthenticationPrincip // 2. 프로필 수정 @PatchMapping("/me") + @Operation(summary = "유저 프로필 수정", description = "로그인한 사용자의 프로필을 수정합니다.", tags = {"User"}) public ResponseEntity> updateUserProfile(@AuthenticationPrincipal @Valid CustomUserDetails currentUser , @RequestBody @Valid UserReq req) { UserRes res = userService.userProfileUpdate(currentUser.getId(), req); @@ -47,11 +50,24 @@ public ResponseEntity> updateUserProfile(@AuthenticationPri // 3. 회원 탈퇴 (소프트 삭제) - @DeleteMapping("/me") + @DeleteMapping("/soft") + @Operation(summary = "유저 프로필 소프트 삭제", description = "로그인한 사용자의 프로필을 임시 삭제합니다.", tags = {"User"}) public ResponseEntity> softDeleteProfile(@AuthenticationPrincipal CustomUserDetails currentUser) { UserSoftDeleteRes res = userService.softDeleteProfile(currentUser.getId()); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(UserSuccessCode.SOFT_DELETE_MY_PROFILE_SUCCESS, res)); } + + // 4. 유저 하드 삭제 + @DeleteMapping("/hard") + @Operation(summary = "유저 프로필 하드 삭제", description = "로그인한 사용자의 프로필을 완전히 삭제합니다.", tags = {"User"}) + public ResponseEntity> hardDeleteUser(@AuthenticationPrincipal CustomUserDetails currentUser) { + if (adminManager.isAdmin(currentUser.getAuthorities())) { + userService.hardDeleteUserProfile(currentUser.getId()); + } + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(UserSuccessCode.GET_MY_PROFILE_SUCCESS, null)); + } } diff --git a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java index c1724b7..b4f49b0 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java @@ -62,6 +62,7 @@ public UserRes userProfileUpdate(Long userId, UserReq req) { if (req.email() != null) user.setEmail(req.email()); if (req.nickName() != null) user.setNickname(req.nickName()); if (req.profileImage() != null) user.setProfileImageUrl(req.profileImage()); + if (req.introduction() != null) user.setBio(req.introduction()); return UserRes.from(user); From 3dd823b44d1da066ca51e0bbafdcebbd39c95d09 Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 3 Dec 2025 21:15:50 +0900 Subject: [PATCH 181/291] =?UTF-8?q?feat=20:=20ci/cd=20=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=20main=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ba2c4c1..ae2b2f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,10 +5,10 @@ name: BookPick BE CI/CD (v 1.1) on: push: branches: - - develop + - main pull_request: branches: - - develop + - main jobs: # test: From c0a79803cc12ef2ce04f3cfe1b228d393d3148c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 4 Dec 2025 22:08:46 +0900 Subject: [PATCH 182/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=8B=A8=EA=B1=B4=EC=A1=B0=ED=9A=8C=20api=20page?= =?UTF-8?q?=20Path=20=EB=B3=80=EC=88=98=EC=99=80=20currentPage=20=EC=9D=BC?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/comment/controller/CommentController.java | 9 ++++++--- src/main/java/BookPick/mvp/global/dto/PageInfo.java | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index b3b961c..8581189 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -35,8 +35,10 @@ public ResponseEntity> create(@PathVariable Long c // -- 2. 댓글 조회 -- @GetMapping("/{curationId}/comments") - public ResponseEntity> getCommentList(@PathVariable Long curationId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { - CommentListRes res = commentService.getCommentList(curationId, page, size); + public ResponseEntity> getCommentList(@PathVariable Long curationId, + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int size) { + CommentListRes res = commentService.getCommentList(curationId, page-1, size); if (res.comments().isEmpty()) { return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_EMPTY, res)); @@ -44,8 +46,9 @@ public ResponseEntity> getCommentList(@PathVariable return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_READ_SUCCESS, res)); } + @GetMapping("/{curationId}/comments/{commentId}") - public ResponseEntity> getCommentDetail(@PathVariable Long curationId ,@PathVariable Long commentId) { + public ResponseEntity> getCommentDetail(@PathVariable Long curationId, @PathVariable Long commentId) { CommentDetailRes res = commentService.getCommentDetail(commentId); return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_READ_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/global/dto/PageInfo.java b/src/main/java/BookPick/mvp/global/dto/PageInfo.java index 23eaa2f..7f1042e 100644 --- a/src/main/java/BookPick/mvp/global/dto/PageInfo.java +++ b/src/main/java/BookPick/mvp/global/dto/PageInfo.java @@ -10,7 +10,7 @@ public record PageInfo( ) { public static PageInfo of(Page page) { return new PageInfo( - page.getNumber(), + page.getNumber()+1, page.getTotalPages(), page.getTotalElements(), page.hasNext() From ab188c43ced7308ddc7e7ad8796fcecdef54fc6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 4 Dec 2025 22:17:48 +0900 Subject: [PATCH 183/291] =?UTF-8?q?feat=20:=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EB=B3=B4?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=9E=AC=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20api=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=9D=84=20=EB=B3=B4=EB=82=BC=20=EC=88=98=20=EC=9E=88=EA=B2=8C?= =?UTF-8?q?,=20page-1=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 11 ++++++++++- .../comment/controller/PagenationService.java | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index 8581189..805127e 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -22,6 +22,7 @@ @RequiredArgsConstructor public class CommentController { private final CommentService commentService; + private final PagenationService pagenationService; // -- 1. 댓글 생성 -- @PostMapping("/{curationId}/comments") @@ -38,7 +39,15 @@ public ResponseEntity> create(@PathVariable Long c public ResponseEntity> getCommentList(@PathVariable Long curationId, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { - CommentListRes res = commentService.getCommentList(curationId, page-1, size); + + + // 1. 페이지를 1부터 보여주기 위해 -1 + page = page - 1; + + // 2. -1한 페이지가 0보다 작으면 0으로 + page = pagenationService.changeMinusPageToZeroPage(page); + + CommentListRes res = commentService.getCommentList(curationId, page, size); if (res.comments().isEmpty()) { return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_EMPTY, res)); diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java b/src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java new file mode 100644 index 0000000..c067c76 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java @@ -0,0 +1,17 @@ +package BookPick.mvp.domain.comment.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PagenationService { + + + public int changeMinusPageToZeroPage(int page){ + if(page < 0){ + return 0; + } + return page; + } +} From c903b85c8d68aa58568d78a61719118f47f364b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 4 Dec 2025 22:27:16 +0900 Subject: [PATCH 184/291] =?UTF-8?q?feat=20:=20CI/CD=20Name=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ae2b2f4..550b802 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,14 +1,14 @@ -name: BookPick BE CI/CD (v 1.1) +name: BookPick BE dev CI/CD (v 1.1) on: push: branches: - - main + - develop pull_request: branches: - - main + - develop jobs: # test: From 8ece2f454806a289830427f4145e4c6e57bb930b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 4 Dec 2025 22:31:50 +0900 Subject: [PATCH 185/291] =?UTF-8?q?feat=20:=20develop=EB=B8=8C=EB=9E=9C?= =?UTF-8?q?=EC=B9=98=EC=97=90=20pr=20merge=EA=B0=80=20=ED=97=88=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EA=B2=BD=EC=9A=B0=EC=97=90=EB=A7=8C=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=EB=90=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1b08065..1624893 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,14 +1,12 @@ -name: BookPick BE dev CI/CD (v 1.1) - +name: BookPick BE dev CI/CD (v 1.2) on: - push: - branches: - - develop pull_request: branches: - - develop + - develop # develop을 대상으로 하는 PR만 감지 + types: [closed] # PR이 닫힐 때만 실행 + jobs: # test: @@ -51,6 +49,7 @@ on: --push . deploy: needs: build-and-push + if: github.event.pull_request.merged == true # merge된 경우에만 실행 runs-on: ubuntu-latest steps: - name: Set up SSH # 1. SSH 설정 From cc85f245e38cdacfa06dffc8a8944090a81772ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Thu, 4 Dec 2025 22:34:55 +0900 Subject: [PATCH 186/291] =?UTF-8?q?feat=20:=20deploy.yml=20=EB=AC=B8?= =?UTF-8?q?=EB=B2=95=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1624893..a0e0348 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,32 +5,32 @@ on: pull_request: branches: - develop # develop을 대상으로 하는 PR만 감지 - types: [closed] # PR이 닫힐 때만 실행 + types: [ closed ] # PR이 닫힐 때만 실행 - jobs: -# test: -# runs-on: ubuntu-latest -# steps: -# -# - name : 1. checkout repo -# uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 -# -## - name: 2. run test -## run: ./gradlew test -# -# - name: 3. set up JDK 21 -# uses: actions/setup-java@v2 -# with: -# java-version: 21 -# distribution: 'temurin' -# -# - name: 4. grant execute permission for gradlew -# run: chmod +x gradlew +jobs: + # test: + # runs-on: ubuntu-latest + # steps: + # + # - name : 1. checkout repo + # uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 + # + ## - name: 2. run test + ## run: ./gradlew test + # + # - name: 3. set up JDK 21 + # uses: actions/setup-java@v2 + # with: + # java-version: 21 + # distribution: 'temurin' + # + # - name: 4. grant execute permission for gradlew + # run: chmod +x gradlew build-and-push: -# needs: test + # needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 From 2005f6622fa914342cbd78426844d78b9837afa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sat, 6 Dec 2025 23:18:30 +0900 Subject: [PATCH 187/291] =?UTF-8?q?feat=20:=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EB=B8=8C=EB=9E=9C=EC=B9=98=20=EB=B3=80=EA=B2=BD=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=B0=ED=8F=AC=20=EC=84=9C=EB=B2=84=EC=97=90=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=EC=95=88=EB=90=98=EC=84=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20push?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/user/dto/base/UserReq.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java b/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java index f5ed6c6..736e195 100644 --- a/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java +++ b/src/main/java/BookPick/mvp/domain/user/dto/base/UserReq.java @@ -12,15 +12,5 @@ public record UserReq( String introduction, Roles role ) { - public UserReq from(User user){ - return new UserReq( - user.getId(), - user.getEmail(), - user.getPassword(), - user.getNickname(), - user.getProfileImageUrl(), - user.getBio(), - user.getRole() - ); - } + } From f94a1dca5c0a9b5aa46e428c50e8aae26e35b005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 9 Dec 2025 00:15:48 +0900 Subject: [PATCH 188/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EC=84=A4=EC=A0=95=EC=8B=9C=20=EC=98=AC=EB=B0=94?= =?UTF-8?q?=EB=A5=B8=20=EC=84=A4=EC=A0=95=EA=B0=92=EB=93=A4=EC=9D=84=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EA=B0=80=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=ED=96=88=EB=8A=94=EC=A7=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=98=EA=B8=B0=EC=9C=84=ED=97=A4=20Enum=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=A9=94?= =?UTF-8?q?=EC=84=B8=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreference/enums/filed/Genre.java | 34 ++++++++++++++++ .../enums/filed/Keyword.java | 33 +++++++++++++++ .../ReadingPreference/enums/filed/MBTI.java | 31 ++++++++++++++ .../ReadingPreference/enums/filed/Mood.java | 40 +++++++++++++++++++ .../enums/filed/PreferenceField.java | 27 +++++++++++++ .../enums/filed/ReadingHabit.java | 30 ++++++++++++++ .../enums/filed/ReadingStyle.java | 35 ++++++++++++++++ .../{ => resCode}/PrferenceErrorCode.java | 2 +- 8 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Genre.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/MBTI.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Mood.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/PreferenceField.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingHabit.java create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingStyle.java rename src/main/java/BookPick/mvp/domain/ReadingPreference/enums/{ => resCode}/PrferenceErrorCode.java (87%) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Genre.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Genre.java new file mode 100644 index 0000000..a491655 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Genre.java @@ -0,0 +1,34 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +public enum Genre implements PreferenceField { + + NOVEL("소설"), + ESSAY("에세이"), + HISTORY("역사"), + ART("예술"), + SELF_DEVELOPMENT("자기개발"), + ECONOMY("경제"), + PSYCHOLOGY("심리학"), + SOCIETY("사회"), + EDUCATION("교육"), + SCIENCE("과학"), + PHILOSOPHY("철학"), + RELIGION("종교"); + + private final String description; + + Genre(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + + // ✔ description 기반 유효성 검증 (한 줄 래핑) + public static boolean isValid(String description) { + return PreferenceField.isValidDescription(Genre.class, description); + } + +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java new file mode 100644 index 0000000..ce1fea2 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java @@ -0,0 +1,33 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +public enum Keyword implements PreferenceField { + + COMFORT("위로"), + GROWTH("성장"), + LOVE("사랑"), + EMPATHY("공감"), + KNOWLEDGE("지식"), + HUMOR("유머"), + MYSTERY("추리"), + ADVENTURE("모험"), + FANTASY("판타지"), + REALITY("현실"), + FUTURE("미래"), + PAST("과거"); + + private final String description; + + Keyword(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + + public static boolean isValid(String description) { + return PreferenceField.isValidDescription(Keyword.class, description); + } +} + diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/MBTI.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/MBTI.java new file mode 100644 index 0000000..f450885 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/MBTI.java @@ -0,0 +1,31 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +public enum MBTI { + + + ISTJ, + ISFJ, + INFJ, + INTJ, + ISTP, + ISFP, + INFP, + INTP, + ESTP, + ESFP, + ENFP, + ENTP, + ESTJ, + ESFJ, + ENFJ, + ENTJ; + + public static boolean isValid(String value){ + try { + MBTI.valueOf(value); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Mood.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Mood.java new file mode 100644 index 0000000..b96b4cc --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Mood.java @@ -0,0 +1,40 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +public enum Mood implements PreferenceField { + + AFTER_WORK("퇴근 후"), + HOT_TEA("따뜻한 차 한잔"), + RAINY_DAY("비 오는 날"), + SNOWY_DAY("눈 오는 날"), + SUBWAY_BUS("지하철·버스"), + CAFE("카페"), + BED("침대에서"), + PARK("공원"), + LIBRARY("도서관"), + BOOKSTORE("서점에서"), + DAWN("새벽 시간"), + WEEKEND_AFTERNOON("주말 오후"), + LUNCH_TIME("점심시간"), + LATE_NIGHT("늦은 밤"), + BEFORE_SLEEP("잠들기 전"), + ALONE_TIME("혼자만의 시간"), + WINDOW_SIDE("창가에서"), + WITH_MUSIC("음악과 함께"), + TRAVELING("여행 중"), + VACATION("휴가 중"); + + private final String description; + + Mood(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + + public static boolean isValid(String description) { + return PreferenceField.isValidDescription(Mood.class, description); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/PreferenceField.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/PreferenceField.java new file mode 100644 index 0000000..c68ffea --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/PreferenceField.java @@ -0,0 +1,27 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +import java.util.function.Function; + + +import java.util.function.Function; + +public interface PreferenceField { + + String getDescription(); + + static & PreferenceField> boolean isValidDescription( + Class enumClass, + String description + ) { + if (description == null || description.isEmpty()) { + return true; + } + for (E constant : enumClass.getEnumConstants()) { + if (constant.getDescription().equals(description)) { + return true; + } + } + return false; + } +} + diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingHabit.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingHabit.java new file mode 100644 index 0000000..8de45f3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingHabit.java @@ -0,0 +1,30 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +public enum ReadingHabit implements PreferenceField{ + + READ_AT_ONCE("한 번에 완독하는 편"), + HIGHLIGHTING("밑줄 긋거나 형광펜으로 표시하는 편"), + MULTIPLE_BOOKS("여러 권을 동시에 읽는 편"), + MANY_BOOKMARKS("책갈피를 많이 사용하는 편"), + NOTE_TAKING("읽은 내용을 메모하는 편"), + READ_OUT_LOUD("소리 내어 읽는 편"), + QUIET_PLACE("조용한 곳에서만 읽는 편"), + WITH_MUSIC("음악을 들으며 읽는 편"), + REREADING("읽은 책을 다시 읽는 편"), + READING_CLUB("독서 모임에 참여하는 편"); + + private final String description; + + ReadingHabit(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + + public static boolean isValid(String description) { + return PreferenceField.isValidDescription(ReadingHabit.class, description); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingStyle.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingStyle.java new file mode 100644 index 0000000..5706a90 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/ReadingStyle.java @@ -0,0 +1,35 @@ +package BookPick.mvp.domain.ReadingPreference.enums.filed; + +public enum ReadingStyle implements PreferenceField { + + SPEED_READING("속독형"), + IMMERSIVE("몰입형"), + CAREFUL_READING("정독형"), + EXPLORATORY("취향 탐색형"), + STORY_FOCUSED("스토리 중심"), + KNOWLEDGE_BASED("지식 위주"), + EMOTIONAL("감성적"), + LOGICAL("논리적"), + CREATIVE("창의적"), + PRACTICAL("실용적"), + CRITICAL("비평적"), + IMAGINATIVE("상상력 중시"), + RELAXED("느긋한 독서"), + DEEP_THINKING("깊이 있는 사색"), + LIGHT_READING("가볍게 즐기기"); + + private final String description; + + ReadingStyle(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + + public static boolean isValid(String description) { + return PreferenceField.isValidDescription(ReadingStyle.class, description); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/PrferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PrferenceErrorCode.java similarity index 87% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/enums/PrferenceErrorCode.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PrferenceErrorCode.java index 6d3efc4..ad385f9 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/PrferenceErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PrferenceErrorCode.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.enums; +package BookPick.mvp.domain.ReadingPreference.enums.resCode; import BookPick.mvp.global.enums.ErrorCodeInterface; import lombok.AllArgsConstructor; From 188aab29ac7ffd3d8367ad361b4a6cb78af047be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 9 Dec 2025 00:21:43 +0900 Subject: [PATCH 189/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EC=84=A4=EC=A0=95=20=EC=9A=94=EC=B2=AD=20DTO?= =?UTF-8?q?=EC=9D=98=20=EA=B0=81=20=ED=95=84=EB=93=9C=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EB=B0=8F=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReadingPreferenceValidCheck.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java new file mode 100644 index 0000000..dd47059 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java @@ -0,0 +1,66 @@ +package BookPick.mvp.domain.ReadingPreference.service; + +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; +import BookPick.mvp.domain.ReadingPreference.enums.filed.*; +import org.springframework.stereotype.Service; + +@Service +public class ReadingPreferenceValidCheck { + + public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { + + // 🔥 MBTI 검증 (null 허용) + if (req.mbti() != null && !req.mbti().isEmpty()) { + if (!MBTI.isValid(req.mbti())) { + return false; + } + } + + // 🔥 moods 검증 + if (req.moods() != null) { + for (String mood : req.moods()) { + if (!Mood.isValid(mood)) { + return false; + } + } + } + + // 🔥 readingHabits 검증 + if (req.readingHabits() != null) { + for (String habit : req.readingHabits()) { + if (!ReadingHabit.isValid(habit)) { + return false; + } + } + } + + // 🔥 genres 검증 + if (req.genres() != null) { + for (String genre : req.genres()) { + if (!Genre.isValid(genre)) { + return false; + } + } + } + + // 🔥 keywords 검증 + if (req.keywords() != null) { + for (String keyword : req.keywords()) { + if (!Keyword.isValid(keyword)) { + return false; + } + } + } + + // 🔥 readingStyles 검증 + if (req.readingStyles() != null) { + for (String style : req.readingStyles()) { + if (!ReadingStyle.isValid(style)) { + return false; + } + } + } + + return true; + } +} From 78eb312b33b3ef09d964d502c969028dabb3a994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 9 Dec 2025 00:28:31 +0900 Subject: [PATCH 190/291] =?UTF-8?q?feat=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81=EA=B3=BC=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EA=B4=80=EC=8B=AC?= =?UTF-8?q?=EC=82=AC=20=EB=B6=84=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?ReadingPreferenceValidCheckService=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=8D=98=EC=A7=80=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rongReadingPreferenceRequestException.java | 10 ++++++++++ ...rrorCode.java => PreferenceErrorCode.java} | 5 +++-- .../service/ReadingPreferenceService.java | 7 ++++++- ...> ReadingPreferenceValidCheckService.java} | 19 +++++++++---------- 4 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java rename src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/{PrferenceErrorCode.java => PreferenceErrorCode.java} (53%) rename src/main/java/BookPick/mvp/domain/ReadingPreference/service/{ReadingPreferenceValidCheck.java => ReadingPreferenceValidCheckService.java} (70%) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java new file mode 100644 index 0000000..08b0c74 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.ReadingPreference.Exception; + +import BookPick.mvp.domain.ReadingPreference.enums.resCode.PreferenceErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class WrongReadingPreferenceRequestException extends BusinessException { + public WrongReadingPreferenceRequestException(){ + super(PreferenceErrorCode.WRONG_READING_PREFERENCE_REQUEST); + } +} diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PrferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java similarity index 53% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PrferenceErrorCode.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java index ad385f9..2d3dd64 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PrferenceErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java @@ -7,9 +7,10 @@ @Getter @AllArgsConstructor -public enum PrferenceErrorCode implements ErrorCodeInterface { +public enum PreferenceErrorCode implements ErrorCodeInterface { - PREFERENCE_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 독서취향이 설정되지 않았습니다."); + PREFERENCE_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 독서취향이 설정되지 않았습니다."), + WRONG_READING_PREFERENCE_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 독서취향 요청값입니다."); diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 04a8446..bd1faeb 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -30,6 +30,8 @@ public class ReadingPreferenceService { private final BookSaveService bookSaveService; private final AuthorSaveService authorSaveService; private final BookRepository bookRepository; + private final ReadingPreferenceValidCheckService readingPreferenceValidCheckService; + // -- 유저 독서 취향 등록 -- @@ -121,7 +123,10 @@ public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferen preference.setFavoriteBooks(savedBooks); preference.setFavoriteAuthors(savedAuthors); - preference.update(req); + if(readingPreferenceValidCheckService.checkReadingPreferenceReqIsValid(req)){ + preference.update(req); + } + return ReadingPreferenceRes.from(preference); } diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java similarity index 70% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java index dd47059..65571b4 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheck.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java @@ -1,18 +1,19 @@ package BookPick.mvp.domain.ReadingPreference.service; +import BookPick.mvp.domain.ReadingPreference.Exception.WrongReadingPreferenceRequestException; import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; import BookPick.mvp.domain.ReadingPreference.enums.filed.*; import org.springframework.stereotype.Service; @Service -public class ReadingPreferenceValidCheck { +public class ReadingPreferenceValidCheckService { - public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { + public void validate(ReadingPreferenceReq req) { // 🔥 MBTI 검증 (null 허용) if (req.mbti() != null && !req.mbti().isEmpty()) { if (!MBTI.isValid(req.mbti())) { - return false; + throw new WrongReadingPreferenceRequestException(); } } @@ -20,7 +21,7 @@ public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { if (req.moods() != null) { for (String mood : req.moods()) { if (!Mood.isValid(mood)) { - return false; + throw new WrongReadingPreferenceRequestException(); } } } @@ -29,7 +30,7 @@ public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { if (req.readingHabits() != null) { for (String habit : req.readingHabits()) { if (!ReadingHabit.isValid(habit)) { - return false; + throw new WrongReadingPreferenceRequestException(); } } } @@ -38,7 +39,7 @@ public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { if (req.genres() != null) { for (String genre : req.genres()) { if (!Genre.isValid(genre)) { - return false; + throw new WrongReadingPreferenceRequestException(); } } } @@ -47,7 +48,7 @@ public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { if (req.keywords() != null) { for (String keyword : req.keywords()) { if (!Keyword.isValid(keyword)) { - return false; + throw new WrongReadingPreferenceRequestException(); } } } @@ -56,11 +57,9 @@ public boolean checkReadingPreferenceReqIsValid(ReadingPreferenceReq req) { if (req.readingStyles() != null) { for (String style : req.readingStyles()) { if (!ReadingStyle.isValid(style)) { - return false; + throw new WrongReadingPreferenceRequestException(); } } } - - return true; } } From 2e73659426cb7205c0efbd2e21f075c81e0a5783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 9 Dec 2025 00:30:33 +0900 Subject: [PATCH 191/291] =?UTF-8?q?feat=20:=20`checkReadingPreferenceReqIs?= =?UTF-8?q?Valid`(boolean)=20->=20validate(void)=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=98=EC=97=AC=20=EA=B2=80=EC=A6=9D=EA=B3=BC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EC=97=AD=ED=95=A0=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreference/service/ReadingPreferenceService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index bd1faeb..cfe2998 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -123,9 +123,9 @@ public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferen preference.setFavoriteBooks(savedBooks); preference.setFavoriteAuthors(savedAuthors); - if(readingPreferenceValidCheckService.checkReadingPreferenceReqIsValid(req)){ - preference.update(req); - } + readingPreferenceValidCheckService.validate(req); // ReadingPreferenceReq 검증 + preference.update(req); + return ReadingPreferenceRes.from(preference); From 7e1f1298c2a9cebb5eac253630a99fbf0034ebb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Tue, 9 Dec 2025 23:17:17 +0900 Subject: [PATCH 192/291] =?UTF-8?q?refactor=20:=20=EB=8F=85=EC=84=9C?= =?UTF-8?q?=EC=B7=A8=ED=96=A5=20=EC=9A=94=EC=B2=AD=EC=9D=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=ED=95=9C=EB=8B=A4=EB=8A=94=20=EC=9D=98=EB=AF=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReadingPreferenceService.java | 2 +- .../service/ReadingPreferenceValidCheckService.java | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index cfe2998..857218d 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -123,7 +123,7 @@ public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferen preference.setFavoriteBooks(savedBooks); preference.setFavoriteAuthors(savedAuthors); - readingPreferenceValidCheckService.validate(req); // ReadingPreferenceReq 검증 + readingPreferenceValidCheckService.validateReadingPreferenceReq(req); // ReadingPreferenceReq 검증 preference.update(req); diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java index 65571b4..0f646bf 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java @@ -8,16 +8,14 @@ @Service public class ReadingPreferenceValidCheckService { - public void validate(ReadingPreferenceReq req) { + public void validateReadingPreferenceReq(ReadingPreferenceReq req) { - // 🔥 MBTI 검증 (null 허용) if (req.mbti() != null && !req.mbti().isEmpty()) { if (!MBTI.isValid(req.mbti())) { throw new WrongReadingPreferenceRequestException(); } } - // 🔥 moods 검증 if (req.moods() != null) { for (String mood : req.moods()) { if (!Mood.isValid(mood)) { @@ -26,7 +24,6 @@ public void validate(ReadingPreferenceReq req) { } } - // 🔥 readingHabits 검증 if (req.readingHabits() != null) { for (String habit : req.readingHabits()) { if (!ReadingHabit.isValid(habit)) { @@ -35,7 +32,6 @@ public void validate(ReadingPreferenceReq req) { } } - // 🔥 genres 검증 if (req.genres() != null) { for (String genre : req.genres()) { if (!Genre.isValid(genre)) { @@ -44,7 +40,6 @@ public void validate(ReadingPreferenceReq req) { } } - // 🔥 keywords 검증 if (req.keywords() != null) { for (String keyword : req.keywords()) { if (!Keyword.isValid(keyword)) { @@ -53,7 +48,6 @@ public void validate(ReadingPreferenceReq req) { } } - // 🔥 readingStyles 검증 if (req.readingStyles() != null) { for (String style : req.readingStyles()) { if (!ReadingStyle.isValid(style)) { From 069ac6bca60212761b2f2ae38f1984c661341cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Wed, 10 Dec 2025 00:04:39 +0900 Subject: [PATCH 193/291] =?UTF-8?q?feat=20:=20=EC=9C=A0=EC=A0=80=EC=9D=98?= =?UTF-8?q?=20=EC=B2=AB=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=B4=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=EA=B2=BD=EC=9A=B0=20false=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BF=94=EC=A3=BC=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/auth/service/LoginService.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java b/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java index f8e5f72..c448bec 100644 --- a/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java +++ b/src/main/java/BookPick/mvp/domain/auth/service/LoginService.java @@ -2,7 +2,11 @@ import BookPick.mvp.domain.auth.dto.LoginReq; import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; import jakarta.servlet.http.HttpServletRequest; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -19,9 +23,11 @@ public class LoginService { private final JwtAuthManager jwtAuthManager; private final AuthenticationManagerBuilder authenticationManagerBuilder; + private final UserRepository userRepository; // 1. jwt 기반 로그인 + @Transactional public LoginRes login(LoginReq req, HttpServletRequest servletReq) throws RuntimeException { // 1. 토큰 생성 @@ -29,13 +35,23 @@ public LoginRes login(LoginReq req, HttpServletRequest servletReq) throws Runtim // 2. 로그인 검증 try { - var auth = authenticationManagerBuilder.getObject().authenticate(token); + // 아이디 및 패스워드 확인 + var auth = authenticationManagerBuilder.getObject().authenticate(token); + // Jwt 토큰 생성 JwtAuthManager.TokenPair tokenPair = jwtAuthManager.createTokens(auth); + // + LoginRes res = LoginRes.from((CustomUserDetails) auth.getPrincipal(), tokenPair.accessToken(), tokenPair.refreshToken()); - return LoginRes.from((CustomUserDetails) auth.getPrincipal(), tokenPair.accessToken(), tokenPair.refreshToken()); + if(res.isFirstLogin()){ + User user = userRepository.findById(res.userId()) + .orElseThrow(UserNotFoundException::new); + + user.setFirstLogin(false); + } + return res; } catch (BadCredentialsException | UsernameNotFoundException e) { throw new InvalidLoginException(); @@ -43,6 +59,5 @@ public LoginRes login(LoginReq req, HttpServletRequest servletReq) throws Runtim throw new InvalidLoginException(); } } - } From 891d63fe4672acd658b354fe56822ed75f0f8aae Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 13 Dec 2025 21:57:14 +0900 Subject: [PATCH 194/291] =?UTF-8?q?chore=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9E=90=20=ED=91=9C=EA=B8=B0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4=20Dto=EC=97=90=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=EC=9E=90=20ID=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/comment/controller/CommentController.java | 2 -- .../BookPick/mvp/domain/comment/dto/read/CommentListRes.java | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index 805127e..b4a8515 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -46,9 +46,7 @@ public ResponseEntity> getCommentList(@PathVariable // 2. -1한 페이지가 0보다 작으면 0으로 page = pagenationService.changeMinusPageToZeroPage(page); - CommentListRes res = commentService.getCommentList(curationId, page, size); - if (res.comments().isEmpty()) { return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_EMPTY, res)); } diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java index 7fee5cd..40ce6d2 100644 --- a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java +++ b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java @@ -17,6 +17,7 @@ public static CommentListRes of(List comments, PageInfo pageInfo // 📝 댓글 요약 DTO public record CommentSummary( Long commentId, + Long userID, Long parentId, String nickname, String profileImageUrl, @@ -26,6 +27,7 @@ public record CommentSummary( ) { public static CommentSummary of( Long commentId, + Long userID, Long parentId, String nickname, String profileImageUrl, @@ -33,7 +35,7 @@ public static CommentSummary of( LocalDateTime createdAt, LocalDateTime updatedAt ) { - return new CommentSummary(commentId, parentId, nickname, profileImageUrl, content, createdAt, updatedAt); + return new CommentSummary(commentId, userId, parentId, nickname, profileImageUrl, content, createdAt, updatedAt); } } } From 8f76132fdd8bc442e4c9fdfff5ed8cc6980836c6 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 13 Dec 2025 22:21:34 +0900 Subject: [PATCH 195/291] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=EC=9D=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=82=AD=EC=A0=9C=EB=A5=BC?= =?UTF-8?q?=20=ED=95=A0=20=EC=88=98=20=EC=9E=88=EA=B2=8C=20=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4,=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C,=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=EC=9E=90=EC=9D=98=20ID?= =?UTF-8?q?=20=EB=B0=98=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/comment/dto/read/CommentListRes.java | 2 +- .../BookPick/mvp/domain/comment/service/CommentService.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java index 40ce6d2..268d815 100644 --- a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java +++ b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java @@ -27,7 +27,7 @@ public record CommentSummary( ) { public static CommentSummary of( Long commentId, - Long userID, + Long userId, Long parentId, String nickname, String profileImageUrl, diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 94a0a29..5278200 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -76,6 +76,7 @@ public CommentListRes getCommentList(Long curationId, int page, int size) { List commentList = commentPage.getContent().stream() .map(comment -> CommentListRes.CommentSummary.of( comment.getId(), + comment.getUser().getId(), comment.getParent() != null ? comment.getParent().getId() : null, comment.getUser().getNickname(), comment.getUser().getProfileImageUrl(), From 81e74bd9f9335d8ac3e9566f464f13e1c43bc3f5 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 13 Dec 2025 23:32:22 +0900 Subject: [PATCH 196/291] =?UTF-8?q?feat=20:=20=EB=B3=B8=EC=9D=B8=EC=9D=B4?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=ED=95=9C=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B1=85=EC=9D=98=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=95=8C=EA=B8=B0=20=EC=9C=84=ED=95=B4,=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=A1=B0=ED=9A=8C,=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20Dto=EC=97=90=20imageUrl=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 4 +--- .../curation/dto/base/create/ETC/BookDto.java | 13 +++++++++++-- .../domain/curation/dto/base/get/one/BookInfo.java | 11 +++++++++++ .../curation/dto/base/get/one/RecommendInfo.java | 7 +++++++ .../curation/dto/base/get/one/ThumbnailInfo.java | 4 ++++ .../mvp/domain/curation/entity/Curation.java | 5 +++-- .../curation/service/base/CurationService.java | 10 +++------- src/main/resources/application.yml | 2 +- 8 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 6853748..30b4f95 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -1,14 +1,12 @@ // CurationListController.java에 추가 package BookPick.mvp.domain.curation.controller.base; -import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.service.base.CurationService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; @@ -39,7 +37,7 @@ public ResponseEntity> create( currentUserCheck.isValidCurrentUser(currentUser); - CurationCreateRes res = curationService.create(currentUser.getId(), req); + CurationCreateRes res = curationService.curationCreate(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java index 585494f..70a9f45 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java @@ -1,8 +1,17 @@ package BookPick.mvp.domain.curation.dto.base.create.ETC; +import BookPick.mvp.domain.curation.dto.base.get.one.BookInfo; +import BookPick.mvp.domain.curation.entity.Curation; + // 책 정보 public record BookDto( String title, String author, - String isbn -) {} \ No newline at end of file + String isbn, + String imageUrl +) { + public static BookDto of(Curation curation){ + return new BookDto(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn(), curation.getBookImageUrl()); + } + +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java new file mode 100644 index 0000000..2b5a3b4 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.curation.dto.base.get.one; + +import BookPick.mvp.domain.curation.entity.Curation; + +public record BookInfo(String title, String author, String isbn, String imageUrl) + +{ + public static BookInfo of(Curation curation){ + return new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn(), curation.getBookImageUrl()); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java new file mode 100644 index 0000000..91d2552 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java @@ -0,0 +1,7 @@ +package BookPick.mvp.domain.curation.dto.base.get.one; + +import java.util.List; + +public record RecommendInfo(List moods, List genres, + List keywords, List styles) { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java new file mode 100644 index 0000000..6e0b17c --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java @@ -0,0 +1,4 @@ +package BookPick.mvp.domain.curation.dto.base.get.one; + +public record ThumbnailInfo(String imageUrl, String imageColor) { +} diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index f8099bb..af82a6f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -1,7 +1,6 @@ package BookPick.mvp.domain.curation.entity; import BookPick.mvp.domain.curation.dto.base.CurationReq; -import BookPick.mvp.domain.curation.dto.base.CurationRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; @@ -44,6 +43,7 @@ public class Curation { private String bookTitle; private String bookAuthor; private String bookIsbn; + private String bookImageUrl; @Column(columnDefinition = "TEXT") @@ -107,13 +107,14 @@ public Curation() { } - public void update(CurationUpdateReq req) { + public void curationUpdate(CurationUpdateReq req) { this.title = req.title(); this.thumbnailUrl = req.thumbnail().imageUrl(); this.thumbnailColor = req.thumbnail().imageColor(); this.bookTitle = req.book().title(); this.bookAuthor = req.book().author(); this.bookIsbn = req.book().isbn(); + this.bookImageUrl = req.book().imageUrl(); this.review = req.review(); this.moods = req.recommend().moods(); this.genres = req.recommend().genres(); diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index b7f4444..006aee3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -7,16 +7,12 @@ import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; -import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; -import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; -import BookPick.mvp.domain.user.entity.CuratorSubscribe; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; @@ -26,7 +22,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.Optional; @Service @@ -41,7 +36,7 @@ public class CurationService { // -- 큐레이션 등록 -- @Transactional - public CurationCreateRes create(Long userId, CurationCreateReq req) { + public CurationCreateRes curationCreate(Long userId, CurationCreateReq req) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); @@ -55,6 +50,7 @@ public CurationCreateRes create(Long userId, CurationCreateReq req) { .bookTitle(req.book().title()) .bookAuthor(req.book().author()) .bookIsbn(req.book().isbn()) + .bookImageUrl(req.book().imageUrl()) .review(req.review()) .moods(req.recommend().moods()) .genres(req.recommend().genres()) @@ -111,7 +107,7 @@ public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUp throw new CurationAccessDeniedException(); } - curation.update(req); + curation.curationUpdate(req); return CurationUpdateRes.from(curation); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fad6a57..e2fc7b0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: jpa: hibernate: - ddl-auto: none + ddl-auto: update properties: hibernate: show_sql: false From f66ace59482e9ae0dcefcd95fbc948e672d78fab Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 13 Dec 2025 23:33:07 +0900 Subject: [PATCH 197/291] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=93=88=ED=99=94?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20record=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/dto/base/get/one/CurationGetRes.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index d6d49db..0ed4c89 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -4,7 +4,6 @@ import BookPick.mvp.domain.curation.entity.Curation; import java.time.LocalDateTime; -import java.util.List; public record CurationGetRes( Long id, @@ -58,7 +57,7 @@ public static CurationGetRes fromOwnerView(Curation curation, boolean subscribed subscribed, curation.getTitle(), new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), - new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), + BookInfo.of(curation), curation.getReview(), new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), @@ -72,13 +71,4 @@ public static CurationGetRes fromOwnerView(Curation curation, boolean subscribed } - public record ThumbnailInfo(String imageUrl, String imageColor) { - } - - public record BookInfo(String title, String author, String isbn) { - } - - public record RecommendInfo(List moods, List genres, - List keywords, List styles) { - } } \ No newline at end of file From d87c6863946b0a7e0ca98d08392915acbf9d6997 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 13 Dec 2025 23:48:46 +0900 Subject: [PATCH 198/291] =?UTF-8?q?refactor=20:=20service=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EB=AA=85=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=9D=B4=20=EC=95=9E=EC=97=90=EC=98=A4=EA=B2=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ReadingPreferenceController.java | 2 +- .../controller/base/CurationController.java | 7 +++++-- .../base/delete/CurationListDeleteController.java | 15 ++------------- .../controller/like/CurationLikeController.java | 2 +- .../controller/list/CurationListController.java | 3 +-- .../curation/service/base/CurationService.java | 4 +++- .../subscribe/CuratorSubscribeController.java | 4 ++-- .../mvp/domain/user/util/CurrentUserCheck.java | 3 +-- 8 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java index 5c9aac1..7ef307a 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/controller/ReadingPreferenceController.java @@ -41,7 +41,7 @@ public ResponseEntity> create( public ResponseEntity> getDetails( @AuthenticationPrincipal CustomUserDetails currentUser) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); ReadingPreferenceRes res = readingPreferenceService.findReadingPreference(currentUser.getId()); diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 30b4f95..1ea84a9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -35,7 +35,7 @@ public ResponseEntity> create( @Valid @RequestBody CurationCreateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); CurationCreateRes res = curationService.curationCreate(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) @@ -62,7 +62,10 @@ public ResponseEntity> updateCuration( @PathVariable Long curationId, @Valid @RequestBody CurationUpdateReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { - CurationUpdateRes res = curationService.modifyCuration(currentUser.getId(), curationId, req); + + currentUserCheck.validateLoginUser(currentUser); + + CurationUpdateRes res = curationService.curationUpdate(currentUser.getId(), curationId, req); return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java index bc8fe5e..54e659f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/delete/CurationListDeleteController.java @@ -1,26 +1,15 @@ package BookPick.mvp.domain.curation.controller.base.delete; -import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteReq; import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; -import BookPick.mvp.domain.curation.service.base.CurationService; import BookPick.mvp.domain.curation.service.base.delete.CurationDeleteService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; -import BookPick.mvp.global.api.SuccessCode.SuccessCode; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -41,7 +30,7 @@ public ResponseEntity> deleteCuration( @PathVariable Long curationId, @AuthenticationPrincipal CustomUserDetails currentUser) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); CurationDeleteRes res = curationDeleteService.removeCuration(currentUser.getId(), curationId); return ResponseEntity.ok() @@ -54,7 +43,7 @@ public ResponseEntity> deleteCurations( @AuthenticationPrincipal CustomUserDetails currentUser, @RequestBody CurationListDeleteReq req ) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); CurationListDeleteRes res = curationDeleteService.removeCurations(currentUser.getId(), req); return ResponseEntity.ok() diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java index 787eaaa..e9596a4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/like/CurationLikeController.java @@ -26,7 +26,7 @@ public class CurationLikeController { public ResponseEntity> likeOrUnlikeCuration(@AuthenticationPrincipal CustomUserDetails currentUser , @PathVariable Long curationId) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); boolean liked = curationLikeService.CurationLikeOrUnlike(currentUser.getId(), curationId); diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 151266b..7a1cf1d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -1,6 +1,5 @@ package BookPick.mvp.domain.curation.controller.list; -import BookPick.mvp.domain.auth.exception.InvalidTokenTypeException; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.enums.common.SortType; @@ -33,7 +32,7 @@ public ResponseEntity> getCurations( @AuthenticationPrincipal @Valid CustomUserDetails currentUser ) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); // 1. SortType 변환 diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 006aee3..1dc865b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -99,7 +99,9 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http // -- 큐레이션 수정 -- @Transactional - public CurationUpdateRes modifyCuration(Long userId, Long curationId, CurationUpdateReq req) { + public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { + + Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); diff --git a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java index 79b1a0a..dcde24d 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java @@ -35,7 +35,7 @@ public ResponseEntity> subscribe( @RequestBody @Valid CuratorSubscribeReq req, @AuthenticationPrincipal CustomUserDetails currentUser){ - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); CuratorSubscribeRes curatorSubscribeRes = curationSubscribeService.subscribe(currentUser.getId(), req); @@ -56,7 +56,7 @@ public ResponseEntity> getSubscribedCurato @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { - currentUserCheck.isValidCurrentUser(currentUser); + currentUserCheck.validateLoginUser(currentUser); SubscribedCuratorPageRes subscribedCuratorPageRes = curationSubscribeService.getSubscribedCurators(currentUser.getId(), page, size); return ResponseEntity.ok() .body(ApiResponse.success(CuratorSuccessCode.GET_CURATOR_SUBSCRIBE_LIST_SUCCESS, subscribedCuratorPageRes)); diff --git a/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java b/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java index 6db5a71..3f00171 100644 --- a/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java +++ b/src/main/java/BookPick/mvp/domain/user/util/CurrentUserCheck.java @@ -3,12 +3,11 @@ import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; import BookPick.mvp.domain.auth.service.CustomUserDetails; import org.springframework.stereotype.Component; -import org.springframework.stereotype.Service; @Component public class CurrentUserCheck { - public void isValidCurrentUser(CustomUserDetails currentUser) { + public void validateLoginUser(CustomUserDetails currentUser) { if (currentUser == null) { throw new NotAuthenticateUser(); } From cfd3b9f381b754d6fbec09e274e7456234f5d8b2 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 14 Dec 2025 00:12:19 +0900 Subject: [PATCH 199/291] chore: meaningless commit --- .../domain/curation/util/list/Handler/CurationPageHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index 79ae70b..864d8d3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -24,6 +24,7 @@ public class CurationPageHandler { // 1. 데이터 조회 (size+1개 : +1을 하는 이유는 다음 것을 항상 확인하기 위해서 public List getCurationsPage(Long userId, SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { Pageable pageable = PageRequest.of(0, size + 1); + // SORT_SIMILARITY일 경우 preferenceInfo를 사용, 나머지는 user 관련 정보 필요 없음 if (sortType == SortType.SORT_SIMILARITY && readingPreferenceInfo == null) { throw new UserReadingPreferenceNotExisted(); From 73874df5e01004f59ce4a5ed076601724cdf9a5d Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 14 Dec 2025 14:51:41 +0900 Subject: [PATCH 200/291] =?UTF-8?q?fet:=20=EB=B9=A0=EB=A5=B4=EA=B2=8C=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=94=94?= =?UTF-8?q?=EB=B2=A8=EB=A1=AD=20=EB=B8=8C=EB=9E=9C=EC=B9=98=20=ED=91=B8?= =?UTF-8?q?=EC=8B=9C=EC=9D=BC=EB=95=8C=EB=8F=84=20Ci/Cd=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a0e0348..48a59b1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,6 +7,10 @@ on: - develop # develop을 대상으로 하는 PR만 감지 types: [ closed ] # PR이 닫힐 때만 실행 + push: + branches: + - develop # develop을 대상으로 하는 PR만 감지 + jobs: # test: From 760c796c809df331b0bdaf1dc463f1b3c9b71d40 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 14 Dec 2025 16:26:33 +0900 Subject: [PATCH 201/291] =?UTF-8?q?feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=8C=93=EA=B8=80=20=EA=B0=9C=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C,=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=8B=9C=20+1=20=EB=B0=8F=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20-1=ED=95=98=EB=8A=94?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 2 +- .../comment/service/CommentService.java | 15 ++++++++---- .../mvp/domain/curation/entity/Curation.java | 23 +++++++++++++++++-- .../util/list/fetcher/CurationFetcher.java | 14 +++++++---- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index b4a8515..73a1f8b 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -79,7 +79,7 @@ public ResponseEntity> deleteComment( @PathVariable Long curationId, @PathVariable Long commentId ) { - CommentDeleteRes res = commentService.deleteComment(commentId); + CommentDeleteRes res = commentService.deleteComment(curationId, commentId); return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_DELETE_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 5278200..b1d0c5d 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -36,7 +36,7 @@ public class CommentService { private final CommentRepository commentRepository; - // -- C -- + // -- Create -- public CommentCreateRes createComment(Long userId, Long curationId, CommentCreateReq req) { User user = userRepository.findById(userId) @@ -62,12 +62,13 @@ public CommentCreateRes createComment(Long userId, Long curationId, CommentCreat .build(); Comment saved = commentRepository.save(comment); + curation.increaseCommentCount(); return CommentCreateRes.from(saved); } - // -- R -- + // -- Read -- @Transactional(readOnly = true) public CommentListRes getCommentList(Long curationId, int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); @@ -100,7 +101,7 @@ public CommentDetailRes getCommentDetail(Long commentId) { } - // -- U -- + // -- Update -- @Transactional public CommentUpdateRes updateComment(Long commentId, CommentUpdateReq req) { Comment comment = commentRepository.findById(commentId) @@ -115,13 +116,17 @@ public CommentUpdateRes updateComment(Long commentId, CommentUpdateReq req) { } - // -- D -- + // -- Delete -- @Transactional - public CommentDeleteRes deleteComment(Long commentId) { + public CommentDeleteRes deleteComment(Long curationId, Long commentId) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + Comment comment = commentRepository.findById(commentId) .orElseThrow(CommentNotFoundException::new); commentRepository.delete(comment); + curation.decreaseCommentCount(); return CommentDeleteRes.of(commentId, LocalDateTime.now()); } diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index af82a6f..504e23f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -8,6 +8,7 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; @@ -107,6 +108,7 @@ public Curation() { } + // 수정 public void curationUpdate(CurationUpdateReq req) { this.title = req.title(); this.thumbnailUrl = req.thumbnail().imageUrl(); @@ -122,6 +124,7 @@ public void curationUpdate(CurationUpdateReq req) { this.styles = req.recommend().styles(); } + // 임시저장 public static Curation createDraft(User user, CurationReq req) { Curation curation = Curation.from(user, req); curation.setDrafted(true); @@ -129,16 +132,32 @@ public static Curation createDraft(User user, CurationReq req) { return curation; } - + // 조회수 public void increaseViewCount() { this.viewCount++; updatePopularityScore(); // 인기도 재계산 } + // 인기도 public void updatePopularityScore() { - this.popularityScore = (likeCount * 3) + (commentCount * 2) + (viewCount * 1); + this.popularityScore = (likeCount * 3) + (commentCount * 2) + (viewCount); + } + + + // 댓글 개수 + public void increaseCommentCount() { + this.commentCount++; + this.updatePopularityScore(); + } + + public void decreaseCommentCount() { + if (this.commentCount > 0) { + this.commentCount--; + } + this.updatePopularityScore(); } + // --------------------------- // 팩토리 메서드 public static Curation from(User user, CurationReq req) { diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index a41acc5..fe258f8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -38,22 +38,28 @@ public List fetchCurations(Long userId, SortType sortType, Long cursor // 2) 🌟분류 기준 🌟 return switch (sortType) { - case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); // 인기순 - case SORT_LATEST -> curationRepository.findLatestCurations(cursor, pageable); // 최신순 + // 인기순 + case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); - // 1. 큐레이션 리스트 뽑기 - // 2. 입력값 : + // 최신순 + case SORT_LATEST -> curationRepository.findLatestCurations(cursor, pageable); + + // 취향 유사도순 case SORT_SIMILARITY -> { List recommended = curationRecommendationService.recommend(readingPreferenceInfo); List paginated = CurationMatchResultPagination.paginate(recommended, cursor, pageable); yield paginated.stream().map(CurationMatchResult::getCuration).collect(Collectors.toList()); } + + // 좋아요 순 case SORT_LIKED -> { List likedCurationList = curationLikeRepository.findAllByUserIdOrderByCreatedAtDesc(userId, pageable); yield likedCurationList.stream() .map(CurationLike::getCuration) .toList(); } + + // 내가 작성한 순 case SORT_MY -> curationRepository.findByUserId(userId, pageable); }; } From 8eb5d0f53d5c1ce82b39126bfc3cf1369a34c836 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 14 Dec 2025 18:42:05 +0900 Subject: [PATCH 202/291] =?UTF-8?q?feat=20:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=EC=95=88=EC=97=90=EC=84=9C=EB=A7=8C=20=EB=8D=94?= =?UTF-8?q?=ED=8B=B0=EC=B2=B4=ED=82=B9=EC=9D=B4=20=EC=9E=91=EB=8F=99?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20CreateCommen?= =?UTF-8?q?t=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/domain/comment/service/CommentService.java | 1 + .../mvp/domain/curation/service/list/CurationListService.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index b1d0c5d..cd9487d 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -37,6 +37,7 @@ public class CommentService { // -- Create -- + @Transactional public CommentCreateRes createComment(Long userId, Long curationId, CommentCreateReq req) { User user = userRepository.findById(userId) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 848e852..038ca6d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -68,7 +68,6 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null); CursorPage page = pageHandler.createCursorPage(curations, size); List content = pageHandler.convertToContentRes(page.getContent()); - return CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); } From 5f662cc144b0642d443645f22f164334e233ead0 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 14 Dec 2025 19:27:42 +0900 Subject: [PATCH 203/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=B0=98=ED=99=98=20?= =?UTF-8?q?DTO=EC=97=90=20=ED=98=84=EC=9E=AC=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=ED=96=88=EB=8A=94=EC=A7=80=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/base/get/list/CurationContentRes.java | 44 +++++++++++--- .../mvp/domain/curation/dto/like/LikeDto.java | 5 ++ .../mvp/domain/curation/entity/Curation.java | 1 + .../like/CurationLikeRepository.java | 14 ++++- .../service/list/CurationListService.java | 57 +++++++++++++++++-- .../list/Handler/CurationPageHandler.java | 14 ++++- 6 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/like/LikeDto.java diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 7b685dd..8c8d6bb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -6,68 +6,96 @@ import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.user.entity.User; +import java.time.LocalDateTime; import java.util.Set; public record CurationContentRes( + + // 1. 헤더 Long curationId, String title, + + // 2. 작성자 정보 Long userId, String nickName, String profileImageUrl, String introduction, + + + // 3. 작성 내용 ThumbnailRes thumbnail, String review, BookResInCuration book, + + // 4. 부수 정보 int likeCount, int commentCount, int viewCount, - Integer similarity, + int similarity, String matched, - Integer popularityScore, + int popularityScore, boolean isDrafted, - String createdAt + boolean isLiked, + + // 5. 시간 + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { - public static CurationContentRes from(Curation curation) { + public static CurationContentRes from(Curation curation, boolean isLiked) { return new CurationContentRes( curation.getId(), curation.getTitle(), + curation.getUser().getId(), curation.getUser().getNickname(), curation.getUser().getProfileImageUrl(), curation.getUser().getBio(), + new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), BookResInCuration.from(curation.getTitle(), curation.getBookAuthor(), curation.getBookIsbn()), + curation.getLikeCount(), curation.getCommentCount(), curation.getViewCount(), - null, + 0, null, curation.getPopularityScore(), curation.isDrafted(), - curation.getCreatedAt().toString() + isLiked, + + curation.getCreatedAt(), + curation.getUpdatedAt() ); } - public static CurationContentRes from(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo) { + public static CurationContentRes from(CurationMatchResult matchResult, ReadingPreferenceInfo preferenceInfo, boolean isLiked) { Curation curation = matchResult.getCuration(); return new CurationContentRes( curation.getId(), curation.getTitle(), + curation.getUser().getId(), matchResult.getUser().getNickname(), matchResult.getUser().getProfileImageUrl(), matchResult.getUser().getBio(), + + // Todo 1. 팩토리 메서드 구현 필요 new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), new BookResInCuration(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getLikeCount(), + curation.getCommentCount(), curation.getViewCount(), getSimilarity(matchResult, preferenceInfo), matchResult.getMatched(), curation.getPopularityScore(), curation.isDrafted(), - curation.getCreatedAt().toString() + isLiked, + + curation.getCreatedAt(), + curation.getUpdatedAt() ); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/like/LikeDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/like/LikeDto.java new file mode 100644 index 0000000..1084d25 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/like/LikeDto.java @@ -0,0 +1,5 @@ +package BookPick.mvp.domain.curation.dto.like; + +public class LikeDto { + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 504e23f..5cead40 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -92,6 +92,7 @@ public class Curation { @Column(name = "is_draft") private boolean isDrafted = false; + @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java index 6f47dab..1b435c5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/like/CurationLikeRepository.java @@ -3,15 +3,27 @@ import BookPick.mvp.domain.curation.entity.CurationLike; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; import java.util.Optional; -public interface CurationLikeRepository extends JpaRepository { +public interface CurationLikeRepository extends JpaRepository { Optional findByUserIdAndCurationId(Long userID, Long curationId); List findAllByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); Optional findByUserId(Long userId); + + + // CurationLikeRepository + @Query(""" + select cl.curation.id + from CurationLike cl + where cl.user.id = :userId + and cl.curation.id in :curationIds + """) + List findLikedCurationIds(Long userId, List curationIds); + } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 038ca6d..9745697 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -6,6 +6,7 @@ import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -25,6 +27,8 @@ public class CurationListService { private final CurationPageHandler pageHandler; private final ReadingPreferenceRepository readingPreferenceRepository; private final CurationRecommendationService curationRecommendationService; + private final CurationLikeRepository curationLikeRepository; + // 1. 큐레이션 리스트 조회 public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long userId) { @@ -51,24 +55,65 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, // 더 없으면 그냥 보여줌 List contentResults = hasNext ? paginated.subList(0, size) : paginated; - //6. 다음 커서 반환 + // 6. 다음 커서 반환 Long nextCursor = hasNext ? paginated.get(size).getCuration().getId() : null; + // 7-1. 좋아요 여부 계산을 위한 큐레이션 ID 목록 추출 + List curationIds = contentResults.stream() + .map(r -> r.getCuration().getId()) + .toList(); + + // 7-2. 사용자가 좋아요 누른 큐레이션 ID 조회 + Set likedIds = curationLikeRepository + .findLikedCurationIds(userId, curationIds) + .stream() + .collect(Collectors.toSet()); + - //7. 큐레이션 단건 응답 포멧 반환 + // 8. 큐레이션 리스트 Dto에 들어갈 단건 dto 생성 List content = contentResults.stream() - .map(result -> CurationContentRes.from(result, preferenceInfo)) + .map(result -> CurationContentRes.from( + result, + preferenceInfo, + likedIds.contains(result.getCuration().getId()) + )) .collect(Collectors.toList()); - //8. 큐레이션 리스트로 감싸기 + //9. 큐레이션 리스트로 감싸기 return CurationListGetRes.from(sortType, content, hasNext, nextCursor); } // 2) 내 취향 유사도순 X List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null); CursorPage page = pageHandler.createCursorPage(curations, size); - List content = pageHandler.convertToContentRes(page.getContent()); - return CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); + + + // 2-1. 좋아요 여부 계산을 위한 큐레이션 ID 목록 추출 + List curationIds = page.getContent().stream() + .map(Curation::getId) + .toList(); + + // 2-2. 사용자가 좋아요 누른 큐레이션 ID 조회 + Set likedIds = curationLikeRepository + .findLikedCurationIds(userId, curationIds) + .stream() + .collect(Collectors.toSet()); + + + List content = page.getContent().stream() + .map(c -> CurationContentRes.from( + c, + likedIds.contains(c.getId()) + )) + .collect(Collectors.toList()); + CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); + + return CurationListGetRes.from( + sortType, + content, + page.isHasNext(), + page.getNextCursor() + ); } // Issue 1) DTO 만들어서 독서취향 정보 레이어간 소통 vs 사용자 독서취향 실시간 수정 반영 고려 diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index 864d8d3..3038b39 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Component; import java.util.List; +import java.util.Set; @Component @RequiredArgsConstructor @@ -44,10 +45,17 @@ public CursorPage createCursorPage(List curations, int size) return new CursorPage<>(content, hasNext, nextCursor); } - // 3. DTO 변환 - public List convertToContentRes(List curations) { + // 3. DTO 변환 (isLiked 포함) + public List convertToContentRes( + List curations, + Set likedIds + ) { return curations.stream() - .map(CurationContentRes::from) + .map(c -> CurationContentRes.from( + c, + likedIds.contains(c.getId()) + )) .toList(); } + } \ No newline at end of file From 3f3cb943dc04aa4f5efc11ee5f64378aebcaa9eb Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 14 Dec 2025 23:05:07 +0900 Subject: [PATCH 204/291] =?UTF-8?q?feat=20:=20pr=20=20=EB=B9=A0=EB=A5=B8?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20merge=EB=90=9C=20=EA=B2=BD=EC=9A=B0=EB=A7=8C=20depl?= =?UTF-8?q?oy=EB=90=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 48a59b1..4780430 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -53,7 +53,7 @@ jobs: --push . deploy: needs: build-and-push - if: github.event.pull_request.merged == true # merge된 경우에만 실행 + # if: github.event.pull_request.merged == true # merge된 경우에만 실행 runs-on: ubuntu-latest steps: - name: Set up SSH # 1. SSH 설정 From 1bd41750d28b652d1c1e5d63f8944a699b67c255 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 15 Dec 2025 13:06:25 +0900 Subject: [PATCH 205/291] =?UTF-8?q?feat=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20DTO=EC=9D=98=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A0=95=EB=B3=B4=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?userID=20=EC=B9=B4=EB=A9=9C=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/comment/dto/read/CommentListRes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java index 268d815..5dc10f5 100644 --- a/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java +++ b/src/main/java/BookPick/mvp/domain/comment/dto/read/CommentListRes.java @@ -17,7 +17,7 @@ public static CommentListRes of(List comments, PageInfo pageInfo // 📝 댓글 요약 DTO public record CommentSummary( Long commentId, - Long userID, + Long userId, Long parentId, String nickname, String profileImageUrl, From fd7b3eee9bcb1165aa3f6d457df84c787ca9dbc1 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 15 Dec 2025 19:23:04 +0900 Subject: [PATCH 206/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=A1=B0=ED=9A=8C=EC=8B=9C,=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20=EC=B1=85=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=95=8C=EB=A9=B4=20=EC=95=88=EB=90=A8=EC=9C=BC=EB=A1=9C=20Boo?= =?UTF-8?q?k=20=EC=A0=95=EB=B3=B4=20Null=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 16 +--- .../base/read/CurationReadController.java | 49 ++++++++++++ .../draft/CurationDraftController.java | 60 --------------- .../domain/curation/dto/base/CurationReq.java | 4 +- .../domain/curation/dto/base/CurationRes.java | 17 ++-- .../dto/base/create/CurationCreateReq.java | 19 ----- .../dto/base/get/list/CurationContentRes.java | 7 +- .../mvp/domain/curation/entity/Curation.java | 31 ++------ .../domain/curation/enums/common/State.java | 6 ++ .../service/base/CurationService.java | 21 +---- .../base/delete/CurationDeleteService.java | 11 --- .../base/read/CurationReadService.java | 77 +++++++++++++++++++ .../service/draft/CurationDraftService.java | 30 -------- 13 files changed, 163 insertions(+), 185 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/enums/common/State.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 1ea84a9..a2ab2cf 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -2,7 +2,7 @@ package BookPick.mvp.domain.curation.controller.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; @@ -32,7 +32,7 @@ public class CurationController { @Operation(summary = "큐레이션 생성", description = "새 큐레이션을 생성합니다", tags = {"Curation"}) @PostMapping public ResponseEntity> create( - @Valid @RequestBody CurationCreateReq req, + @Valid @RequestBody CurationReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { currentUserCheck.validateLoginUser(currentUser); @@ -42,17 +42,7 @@ public ResponseEntity> create( .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); } - @Operation(summary = "큐레이션 단건 조회", description = "큐레이션 ID로 단건 조회", tags = {"Curation"}) - @GetMapping("/{curationId}") - public ResponseEntity> getCuration( - @PathVariable Long curationId, - HttpServletRequest req, - @AuthenticationPrincipal CustomUserDetails currentUser - ) { - CurationGetRes res = curationService.findCuration(curationId, currentUser, req); - return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); - } + diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java new file mode 100644 index 0000000..f6469ef --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java @@ -0,0 +1,49 @@ +package BookPick.mvp.domain.curation.controller.base.read; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.curation.service.base.read.CurationReadService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/curations") +@RequiredArgsConstructor +public class CurationReadController { + + private final CurationReadService curationReadService; + private final CurrentUserCheck currentUserCheck; + + + // Todo 1. CurationGetRes BookInfo Null 처리 필요 + @Operation(summary = "큐레이션 일반 단건 조회", description = "큐레이션 ID로 단건 조회", tags = {"Curation"}) + @GetMapping("/{curationId}") + public ResponseEntity> getCuration( + @PathVariable Long curationId, + HttpServletRequest req, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + currentUserCheck.validateLoginUser(currentUser); // 미 로그인 사용자 접근 방어 로직 + + + CurationGetRes res = curationReadService.findCuration(curationId, currentUser, req); + + + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); + } + + + +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java b/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java deleted file mode 100644 index 8fdee45..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/controller/draft/CurationDraftController.java +++ /dev/null @@ -1,60 +0,0 @@ -package BookPick.mvp.domain.curation.controller.draft; - -import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.CurationReq; -import BookPick.mvp.domain.curation.dto.base.CurationRes; -import BookPick.mvp.domain.curation.enums.common.CurationSuccessCode; -import BookPick.mvp.domain.curation.service.draft.CurationDraftService; -import BookPick.mvp.global.api.ApiResponse; -import io.swagger.v3.oas.annotations.Operation; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/v1/curation/draft") -@RequiredArgsConstructor -public class CurationDraftController { - - private final CurationDraftService curationDraftService; - - // 1. 임시 저장 컨트롤러 - // 확장성 : 임시저장, 드래프트 공유 및 유효기간 관리 등 - @PostMapping - @Operation(summary = "게시글 임시 저장", description = "유저가 작성한 큐레이션을 임시저장합니다", tags = {"Curation"}) - public ResponseEntity> saveDraft(@AuthenticationPrincipal CustomUserDetails currentUser, @RequestBody @Valid CurationReq req) { - - - // 1) 로그인 만료 검사 - if (currentUser == null) { - throw new NotAuthenticateUser(); - } - - CurationRes res = curationDraftService.draftSave(currentUser.getId(), req); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(CurationSuccessCode.CREATE_DRAFTED_CURATION_SUCCESS, res)); - } - - - // 2. 임시 저장 조회 컨트롤러 -// @Operation(summary = "임시저장 단건 조회", description = "작성자 임시저장 큐레이션 단건 조회", tags = {"Curation"}) -// @GetMapping("/{curationId}") -// public ResponseEntity> getCuration( -// @PathVariable Long curationId, -// HttpServletRequest req) { -// -// // 1) 로그인 만료 검사 -// if (currentUser == null) { -// throw new NotAuthenticateUser(); -// } -// CurationGetRes res = curationService.findCuration(curationId, req); -// return ResponseEntity.ok() -// .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); -// } - -} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java index 163b1c7..45d8b59 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java @@ -3,6 +3,7 @@ import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; +import BookPick.mvp.domain.curation.enums.common.State; // 메인 요청 DTO public record CurationReq( @@ -10,7 +11,8 @@ public record CurationReq( ThumbnailDto thumbnail, BookDto book, String review, - RecommendDto recommend + RecommendDto recommend, + State state ) { } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java index 9d85274..bbf23e8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java @@ -2,6 +2,7 @@ package BookPick.mvp.domain.curation.dto.base; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.enums.common.State; import java.time.LocalDateTime; import java.util.List; @@ -10,28 +11,32 @@ public record CurationRes( Long id, Long userId, String title, + ThumbnailInfo thumbnail, BookInfo book, String review, + RecommendInfo recommend, - Boolean isDeleted, + State state, + LocalDateTime createdAt, - LocalDateTime updatedAt, - LocalDateTime deletedAt + LocalDateTime updatedAt ) { public static CurationRes from(Curation curation) { return new CurationRes( curation.getId(), curation.getUser().getId(), curation.getTitle(), + new ThumbnailInfo(curation.getThumbnailUrl(), curation.getThumbnailColor()), new BookInfo(curation.getBookTitle(), curation.getBookAuthor(), curation.getBookIsbn()), curation.getReview(), + new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), - curation.isDrafted(), + curation.getState(), + curation.getCreatedAt(), - curation.getUpdatedAt(), - curation.getDeletedAt() + curation.getUpdatedAt() ); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java deleted file mode 100644 index 6599ada..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateReq.java +++ /dev/null @@ -1,19 +0,0 @@ -package BookPick.mvp.domain.curation.dto.base.create; - -import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; -import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; -import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; - -// 메인 요청 DTO -public record CurationCreateReq( - String title, - ThumbnailDto thumbnail, - BookDto book, - String review, - RecommendDto recommend -) { -} - - - - diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 8c8d6bb..b6bdf76 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -3,6 +3,7 @@ import BookPick.mvp.domain.author.entity.Author; import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.enums.common.State; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.user.entity.User; @@ -34,7 +35,7 @@ public record CurationContentRes( int similarity, String matched, int popularityScore, - boolean isDrafted, + State state, boolean isLiked, // 5. 시간 @@ -62,7 +63,7 @@ public static CurationContentRes from(Curation curation, boolean isLiked) { 0, null, curation.getPopularityScore(), - curation.isDrafted(), + curation.getState(), isLiked, curation.getCreatedAt(), @@ -91,7 +92,7 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr getSimilarity(matchResult, preferenceInfo), matchResult.getMatched(), curation.getPopularityScore(), - curation.isDrafted(), + curation.getState(), isLiked, curation.getCreatedAt(), diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 5cead40..38cc317 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -2,6 +2,7 @@ import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.enums.common.State; import BookPick.mvp.domain.user.entity.User; import jakarta.persistence.*; import lombok.*; @@ -26,11 +27,9 @@ public class Curation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; - private String title; @@ -45,8 +44,6 @@ public class Curation { private String bookAuthor; private String bookIsbn; private String bookImageUrl; - - @Column(columnDefinition = "TEXT") private String review; @@ -55,52 +52,41 @@ public class Curation { @CollectionTable(name = "curation_moods", joinColumns = @JoinColumn(name = "curation_id")) @Column(name = "mood") private List moods; - @ElementCollection @CollectionTable(name = "curation_genres", joinColumns = @JoinColumn(name = "curation_id")) @Column(name = "genre") private List genres; - @ElementCollection @CollectionTable(name = "curation_keywords", joinColumns = @JoinColumn(name = "curation_id")) @Column(name = "keyword") private List keywords; - @ElementCollection @CollectionTable(name = "curation_styles", joinColumns = @JoinColumn(name = "curation_id")) @Column(name = "style") private List styles; - @Builder.Default @Column(name = "like_count") private Integer likeCount = 0; - @Builder.Default @Column(name = "view_count") private Integer viewCount = 0; - @Builder.Default @Column(name = "comment_count") private Integer commentCount = 0; - - @Builder.Default @Column(name = "popularity_score") private Integer popularityScore = 0; - - @Column(name = "is_draft") - private boolean isDrafted = false; + private State state; @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; - @LastModifiedDate private LocalDateTime updatedAt; - private LocalDateTime deletedAt; + private LocalDateTime publishedAt; //Todo 1. 소프트 델리트 구현 필요 @@ -125,13 +111,9 @@ public void curationUpdate(CurationUpdateReq req) { this.styles = req.recommend().styles(); } - // 임시저장 - public static Curation createDraft(User user, CurationReq req) { - Curation curation = Curation.from(user, req); - curation.setDrafted(true); - return curation; - } + + // 조회수 public void increaseViewCount() { @@ -161,7 +143,7 @@ public void decreaseCommentCount() { // --------------------------- // 팩토리 메서드 - public static Curation from(User user, CurationReq req) { + public static Curation from(CurationReq req, User user) { return Curation.builder() .user(user) .title(req.title()) @@ -175,6 +157,7 @@ public static Curation from(User user, CurationReq req) { .genres(req.recommend().genres()) .keywords(req.recommend().keywords()) .styles(req.recommend().styles()) + .state(req.state()) .build(); } diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/common/State.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/State.java new file mode 100644 index 0000000..1c512e9 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/State.java @@ -0,0 +1,6 @@ +package BookPick.mvp.domain.curation.enums.common; + +public enum State { + PUBLISHED, + DRAFTED +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 1dc865b..d730b99 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -2,7 +2,7 @@ package BookPick.mvp.domain.curation.service.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; +import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; @@ -36,27 +36,12 @@ public class CurationService { // -- 큐레이션 등록 -- @Transactional - public CurationCreateRes curationCreate(Long userId, CurationCreateReq req) { + public CurationCreateRes curationCreate(Long userId, CurationReq req) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); - - Curation curation = Curation.builder() - .user(user) - .title(req.title()) - .thumbnailUrl(req.thumbnail().imageUrl()) - .thumbnailColor(req.thumbnail().imageColor()) - .bookTitle(req.book().title()) - .bookAuthor(req.book().author()) - .bookIsbn(req.book().isbn()) - .bookImageUrl(req.book().imageUrl()) - .review(req.review()) - .moods(req.recommend().moods()) - .genres(req.recommend().genres()) - .keywords(req.recommend().keywords()) - .styles(req.recommend().styles()) - .build(); + Curation curation = Curation.from(req, user); Curation saved = curationRepository.save(curation); diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java index 0946066..dfe8542 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/delete/CurationDeleteService.java @@ -1,33 +1,22 @@ // CurationListService.java package BookPick.mvp.domain.curation.service.base.delete; -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateReq; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteReq; import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java new file mode 100644 index 0000000..ea4df19 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -0,0 +1,77 @@ +// CurationListService.java +package BookPick.mvp.domain.curation.service.base.read; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.CurationReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationReadService { + + private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; + private final UserRepository userRepository; + private final CurationSubscribeService curationSubscribeService; + + + + + + // -- 큐레이션 단건 조회 -- + @Transactional + public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { + boolean isLikedCuration = false; + boolean isSubscribedCurator = false; + CurationGetRes res; + + Curation curation = curationRepository.findByIdWithUser(curationId) + .orElseThrow(CurationNotFoundException::new); + + curation.increaseViewCount(); // 큐레이션 조회수 +1 + + + if (user != null) { + // 1. 좋아요 정보 찾기 + Optional curationLike = curationLikeRepository.findByUserIdAndCurationId(user.getId(), curationId); + if (curationLike.isPresent()) { + isLikedCuration = true; + } + + // 2. 큐레이터 구독 여부 조회 + isSubscribedCurator = curationSubscribeService.isSubscribeCurator(user.getId(), curation.getUser().getId()); + +// // 3. 큐레이션 작성자면 책 정보 넣어서 큐레이션 반환 +// if (curation.getUser().getId().equals(user.getId())) { +// return CurationGetRes.fromOwnerView(curation, isSubscribedCurator, isLikedCuration); +// } + } + + return CurationGetRes.from(curation, isSubscribedCurator, isLikedCuration); + } + + + + + + +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java deleted file mode 100644 index 74b9413..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java +++ /dev/null @@ -1,30 +0,0 @@ -package BookPick.mvp.domain.curation.service.draft; - -import BookPick.mvp.domain.curation.dto.base.CurationReq; -import BookPick.mvp.domain.curation.dto.base.CurationRes; -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.common.UserNotFoundException; -import BookPick.mvp.domain.user.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class CurationDraftService { - private final CurationRepository curationRepository; - private final UserRepository userRepository; - - public CurationRes draftSave(Long userId, CurationReq req){ - User user = userRepository.findById(userId) - .orElseThrow(UserNotFoundException::new); - - Curation draftedCuration = Curation.createDraft(user, req); - Curation saved = curationRepository.save(draftedCuration); - - return CurationRes.from(saved); - } - } - - From 2daf311b8862c378768f6cd823e0256bac6ebf43 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 15 Dec 2025 19:32:35 +0900 Subject: [PATCH 207/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=9A=A9=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EC=9A=94=EC=B2=AD=20=EC=8B=9C,=20=EC=9E=90=EC=8B=A0?= =?UTF-8?q?=EC=9D=98=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=B4=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=EA=B2=83=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=EC=97=90=EB=8A=94=20403?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=20=EB=B0=98=ED=99=98=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base/read/CurationReadController.java | 23 ++++++++++++++++--- .../base/read/CurationReadService.java | 15 ++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java index f6469ef..3b9880b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java @@ -26,8 +26,7 @@ public class CurationReadController { private final CurrentUserCheck currentUserCheck; - // Todo 1. CurationGetRes BookInfo Null 처리 필요 - @Operation(summary = "큐레이션 일반 단건 조회", description = "큐레이션 ID로 단건 조회", tags = {"Curation"}) + @Operation(summary = "큐레이션 일반 조회용 단건 조회", description = "큐레이션 ID로 단건 조회", tags = {"Curation"}) @GetMapping("/{curationId}") public ResponseEntity> getCuration( @PathVariable Long curationId, @@ -37,7 +36,25 @@ public ResponseEntity> getCuration( currentUserCheck.validateLoginUser(currentUser); // 미 로그인 사용자 접근 방어 로직 - CurationGetRes res = curationReadService.findCuration(curationId, currentUser, req); + CurationGetRes res = curationReadService.findCuration(curationId, currentUser, req,false); + + + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_GET_SUCCESS, res)); + } + + + @Operation(summary = "큐레이션 수정용 단건 조회", description = "큐레이션 ID로 단건 조회", tags = {"Curation"}) + @GetMapping("/{curationId}/edit") + public ResponseEntity> getCurationForEdit( + @PathVariable Long curationId, + HttpServletRequest req, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + currentUserCheck.validateLoginUser(currentUser); // 미 로그인 사용자 접근 방어 로직 + + + CurationGetRes res = curationReadService.findCuration(curationId, currentUser, req, true); return ResponseEntity.ok() diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index ea4df19..e9a3649 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -39,7 +39,7 @@ public class CurationReadService { // -- 큐레이션 단건 조회 -- @Transactional - public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { + public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req, boolean isEdit) { boolean isLikedCuration = false; boolean isSubscribedCurator = false; CurationGetRes res; @@ -60,10 +60,15 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http // 2. 큐레이터 구독 여부 조회 isSubscribedCurator = curationSubscribeService.isSubscribeCurator(user.getId(), curation.getUser().getId()); -// // 3. 큐레이션 작성자면 책 정보 넣어서 큐레이션 반환 -// if (curation.getUser().getId().equals(user.getId())) { -// return CurationGetRes.fromOwnerView(curation, isSubscribedCurator, isLikedCuration); -// } + // 3. 큐레이션 작성자면 책 정보 넣어서 큐레이션 반환 + if ( isEdit) { + if(curation.getUser().getId().equals(user.getId())){ + return CurationGetRes.fromOwnerView(curation, isSubscribedCurator, isLikedCuration); + } + else{ + throw new CurationAccessDeniedException(); + } + } } return CurationGetRes.from(curation, isSubscribedCurator, isLikedCuration); From 2e4e7aa1f983c9ab5b7b7aa28bacba6c3d7ea8e4 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 16 Dec 2025 11:51:35 +0900 Subject: [PATCH 208/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=B0=9C=ED=96=89=20=EB=B0=8F=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 7 ++- .../domain/curation/dto/base/CurationReq.java | 2 +- .../dto/base/create/CurationCreateRes.java | 5 +- .../dto/base/update/CurationUpdateReq.java | 3 +- .../mvp/domain/curation/entity/Curation.java | 18 ++++-- .../service/base/CurationService.java | 12 ---- .../base/create/CurationCreateService.java | 59 +++++++++++++++++++ 7 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index a2ab2cf..4bfce64 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -4,14 +4,13 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; -import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -27,6 +26,8 @@ public class CurationController { private final CurationService curationService; + private final CurationCreateService curationCreateService; + private final CurrentUserCheck currentUserCheck; @Operation(summary = "큐레이션 생성", description = "새 큐레이션을 생성합니다", tags = {"Curation"}) @@ -37,7 +38,7 @@ public ResponseEntity> create( currentUserCheck.validateLoginUser(currentUser); - CurationCreateRes res = curationService.curationCreate(currentUser.getId(), req); + CurationCreateRes res = curationCreateService.createCuration(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java index 45d8b59..c5ea63f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java @@ -12,7 +12,7 @@ public record CurationReq( BookDto book, String review, RecommendDto recommend, - State state + Boolean isDrafted ) { } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java index 8cc1da3..edc2b50 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java @@ -3,9 +3,10 @@ import BookPick.mvp.domain.curation.entity.Curation; public record CurationCreateRes( - Long id + Long id, + Boolean isDrafted ) { public static CurationCreateRes from(Curation curation){ - return new CurationCreateRes(curation.getId()); + return new CurationCreateRes(curation.getId(), curation.getIsDrafted()); } } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java index a75a667..c879c4d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateReq.java @@ -10,5 +10,6 @@ public record CurationUpdateReq( ThumbnailDto thumbnail, BookDto book, String review, - RecommendDto recommend + RecommendDto recommend, + Boolean isDrafted ) {} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 38cc317..a9bb45a 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -77,7 +77,7 @@ public class Curation { @Builder.Default @Column(name = "popularity_score") private Integer popularityScore = 0; - private State state; + private Boolean isDrafted; @CreatedDate @@ -109,12 +109,10 @@ public void curationUpdate(CurationUpdateReq req) { this.genres = req.recommend().genres(); this.keywords = req.recommend().keywords(); this.styles = req.recommend().styles(); + this.isDrafted = req.isDrafted(); } - - - // 조회수 public void increaseViewCount() { this.viewCount++; @@ -157,8 +155,18 @@ public static Curation from(CurationReq req, User user) { .genres(req.recommend().genres()) .keywords(req.recommend().keywords()) .styles(req.recommend().styles()) - .state(req.state()) + .isDrafted(req.isDrafted()) .build(); } + // 발행 + public void publish() { + this.isDrafted = false; + this.publishedAt = LocalDateTime.now(); + } + + // 임시저장 + public void draft() { + this.isDrafted = true; + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index d730b99..10cf3bf 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -34,19 +34,7 @@ public class CurationService { private final CurationSubscribeService curationSubscribeService; - // -- 큐레이션 등록 -- - @Transactional - public CurationCreateRes curationCreate(Long userId, CurationReq req) { - - User user = userRepository.findById(userId) - .orElseThrow(UserNotFoundException::new); - - Curation curation = Curation.from(req, user); - Curation saved = curationRepository.save(curation); - - return CurationCreateRes.from(saved); - } // -- 큐레이션 단건 조회 -- diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java new file mode 100644 index 0000000..26dbe29 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java @@ -0,0 +1,59 @@ +// CurationListService.java +package BookPick.mvp.domain.curation.service.base.create; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.CurationReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationCreateService { + + private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; + private final UserRepository userRepository; + private final CurationSubscribeService curationSubscribeService; + + + // -- 큐레이션 등록 -- + @Transactional + public CurationCreateRes createCuration(Long userId, CurationReq req) { + + + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + Curation curation = Curation.from(req, user); + if (req.isDrafted()) { + curation.draft(); + }else{ + curation.publish(); + } + Curation saved = curationRepository.save(curation); + + return CurationCreateRes.from(saved); + + } + + + +} From 8baec6d315a6f7f94a20e39c4b052edec599f2d1 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 16 Dec 2025 15:50:26 +0900 Subject: [PATCH 209/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=9E=91=EC=84=B1=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=EC=9D=BC=EB=B0=98=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BookPick/mvp/domain/curation/dto/base/CurationReq.java | 3 +++ .../BookPick/mvp/domain/curation/dto/base/CurationRes.java | 4 ++-- .../curation/dto/base/get/list/CurationContentRes.java | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java index c5ea63f..5baa52b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationReq.java @@ -4,6 +4,7 @@ import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; import BookPick.mvp.domain.curation.enums.common.State; +import jakarta.validation.constraints.NotNull; // 메인 요청 DTO public record CurationReq( @@ -12,6 +13,8 @@ public record CurationReq( BookDto book, String review, RecommendDto recommend, + + @NotNull Boolean isDrafted ) { } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java index bbf23e8..941963d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/CurationRes.java @@ -17,7 +17,7 @@ public record CurationRes( String review, RecommendInfo recommend, - State state, + Boolean isDrafted, LocalDateTime createdAt, LocalDateTime updatedAt @@ -33,7 +33,7 @@ public static CurationRes from(Curation curation) { curation.getReview(), new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), - curation.getState(), + curation.getIsDrafted(), curation.getCreatedAt(), curation.getUpdatedAt() diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index b6bdf76..19733b0 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -35,8 +35,8 @@ public record CurationContentRes( int similarity, String matched, int popularityScore, - State state, boolean isLiked, + Boolean isDrafted, // 5. 시간 LocalDateTime createdAt, @@ -63,8 +63,8 @@ public static CurationContentRes from(Curation curation, boolean isLiked) { 0, null, curation.getPopularityScore(), - curation.getState(), isLiked, + curation.getIsDrafted(), curation.getCreatedAt(), curation.getUpdatedAt() @@ -92,8 +92,8 @@ public static CurationContentRes from(CurationMatchResult matchResult, ReadingPr getSimilarity(matchResult, preferenceInfo), matchResult.getMatched(), curation.getPopularityScore(), - curation.getState(), isLiked, + curation.getIsDrafted(), curation.getCreatedAt(), curation.getUpdatedAt() From 96bb282333915650cc9e635909227536f1b31263 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 10:58:24 +0900 Subject: [PATCH 210/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EC=8A=A4?= =?UTF-8?q?=EC=9B=A8=EA=B1=B0=20=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/curation/controller/base/CurationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 4bfce64..d8e14cd 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -30,7 +30,7 @@ public class CurationController { private final CurrentUserCheck currentUserCheck; - @Operation(summary = "큐레이션 생성", description = "새 큐레이션을 생성합니다", tags = {"Curation"}) + @Operation(summary = "큐레이션 생성(일반 및 임시저장)", description = "새 큐레이션을 생성합니다 drafted가 true면 임시저장", tags = {"Curation"}) @PostMapping public ResponseEntity> create( @Valid @RequestBody CurationReq req, From e05831cb293ee9194ef67422051b197524a261e4 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 11:26:16 +0900 Subject: [PATCH 211/291] =?UTF-8?q?chore=20:=20develop=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EC=B2=B4=EB=A6=AC=ED=94=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/curation/entity/Curation.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index a9bb45a..0cf7e07 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -150,6 +150,7 @@ public static Curation from(CurationReq req, User user) { .bookTitle(req.book().title()) .bookAuthor(req.book().author()) .bookIsbn(req.book().isbn()) + .bookImageUrl(req.book().imageUrl()) .review(req.review()) .moods(req.recommend().moods()) .genres(req.recommend().genres()) From 61e68681ea53e50c18caf74554928efcc38a7387 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 13:30:20 +0900 Subject: [PATCH 212/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=88=98=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 새로운글 2. 임시저장한 글 3. 발행한 글 link : https://github.com/Book-Pick/bookpick-front/issues/53#issuecomment-3654715818 --- .../list/CurationListController.java | 23 +++++++++++++++++++ .../dto/base/create/CurationCreateRes.java | 2 +- .../mvp/domain/curation/entity/Curation.java | 2 +- .../service/base/CurationService.java | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 7a1cf1d..e28dd04 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -46,6 +46,29 @@ public ResponseEntity> getCurations( } +// @Operation(summary = "큐레이션 임시저장 목록 조회", description = "임시저장", tags = {"Curation"}) +// @GetMapping +// public ResponseEntity> getCurations( +// @RequestParam(defaultValue = "latest") String sort, +// @RequestParam(required = false) Long cursor, +// @RequestParam(defaultValue = "10") int size, +// @AuthenticationPrincipal @Valid CustomUserDetails currentUser +// ) { +// +// currentUserCheck.validateLoginUser(currentUser); +// +// +// // 1. SortType 변환 +// SortType sortType = SortType.fromValue(sort); +// +// // 2. 큐레이션 리스트 반환 +// CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); +// +// return ResponseEntity.ok() +// .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); +// } + + } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java index edc2b50..256755c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateRes.java @@ -9,4 +9,4 @@ public record CurationCreateRes( public static CurationCreateRes from(Curation curation){ return new CurationCreateRes(curation.getId(), curation.getIsDrafted()); } -} +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 0cf7e07..2509d2c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -109,7 +109,7 @@ public void curationUpdate(CurationUpdateReq req) { this.genres = req.recommend().genres(); this.keywords = req.recommend().keywords(); this.styles = req.recommend().styles(); - this.isDrafted = req.isDrafted(); + this.isDrafted = req.isDrafted(); // 임시저장 및 발행 처리 } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 10cf3bf..b0b31ca 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -82,7 +82,7 @@ public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUp throw new CurationAccessDeniedException(); } - curation.curationUpdate(req); + curation.curationUpdate(req); // 임시저장 및 발행 처리도 가능 return CurationUpdateRes.from(curation); } From a93e3e2db1d2719dde9644948a0a7cfd4dd1fb47 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 17:54:00 +0900 Subject: [PATCH 213/291] chore : not meaningfull commnit --- .../mvp/domain/curation/service/base/CurationService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index b0b31ca..83e2485 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -73,8 +73,6 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http // -- 큐레이션 수정 -- @Transactional public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { - - Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); @@ -86,8 +84,4 @@ public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUp return CurationUpdateRes.from(curation); } - - - - } From 8ea56484ba460034124acc7f89d445117a25b577 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 20:21:50 +0900 Subject: [PATCH 214/291] =?UTF-8?q?feat=20:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=95=84=EC=A7=81=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EB=B9=88=20=EB=8F=85?= =?UTF-8?q?=EC=84=9C=EC=B7=A8=ED=96=A5=EC=9D=B4=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EB=90=98=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90,=20=EC=B6=94?= =?UTF-8?q?=ED=9B=84=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20=EB=8F=85?= =?UTF-8?q?=EC=84=9C=EC=B7=A8=ED=96=A5=20=EC=84=A4=EC=A0=95=ED=96=88?= =?UTF-8?q?=EC=9D=8C=EC=9D=84=20=ED=99=95=EC=9D=B8=20=ED=95=A0=20=EC=88=98?= =?UTF-8?q?=20=EC=9E=88=EB=8A=94=20=EC=BB=AC=EB=9F=BC(isCompleted)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/ReadingPreference.java | 5 ++- .../list/CurationListController.java | 42 +++++++++---------- .../dto/base/get/list/CurationListGetRes.java | 13 ++++++ .../service/list/CurationListService.java | 16 +++++-- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index cafa68f..bc6824d 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -74,6 +74,8 @@ public class ReadingPreference { @Column(name = "reading_style") private List readingStyles; + private boolean isCompleted = false; + private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -121,8 +123,9 @@ public static ReadingPreference clearPreferences(User user) { .moods(new ArrayList<>()) .readingHabits(new ArrayList<>()) .genres(new ArrayList<>()) - .readingStyles(new ArrayList<>()) .keywords(new ArrayList<>()) + .readingStyles(new ArrayList<>()) + .isCompleted(false) .build(); } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index e28dd04..079c924 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -46,27 +46,27 @@ public ResponseEntity> getCurations( } -// @Operation(summary = "큐레이션 임시저장 목록 조회", description = "임시저장", tags = {"Curation"}) -// @GetMapping -// public ResponseEntity> getCurations( -// @RequestParam(defaultValue = "latest") String sort, -// @RequestParam(required = false) Long cursor, -// @RequestParam(defaultValue = "10") int size, -// @AuthenticationPrincipal @Valid CustomUserDetails currentUser -// ) { -// -// currentUserCheck.validateLoginUser(currentUser); -// -// -// // 1. SortType 변환 -// SortType sortType = SortType.fromValue(sort); -// -// // 2. 큐레이션 리스트 반환 -// CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); -// -// return ResponseEntity.ok() -// .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); -// } + /*@Operation(summary = "큐레이션 임시저장 목록 조회", description = "임시저장", tags = {"Curation"}) + @GetMapping + public ResponseEntity> getCurations( + @RequestParam(defaultValue = "latest") String sort, + @RequestParam(required = false) Long cursor, + @RequestParam(defaultValue = "10") int size, + @AuthenticationPrincipal @Valid CustomUserDetails currentUser + ) { + + currentUserCheck.validateLoginUser(currentUser); + + + // 1. SortType 변환 + SortType sortType = SortType.fromValue(sort); + + // 2. 큐레이션 리스트 반환 + CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); + + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); + }*/ } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java index ecaf73b..21e2b7d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationListGetRes.java @@ -2,6 +2,8 @@ package BookPick.mvp.domain.curation.dto.base.get.list; import BookPick.mvp.domain.curation.enums.common.SortType; + +import java.util.ArrayList; import java.util.List; public record CurationListGetRes( @@ -23,4 +25,15 @@ public static CurationListGetRes from(SortType sortType, List(), + 0, + false, + null + ); + } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 9745697..6efdaa4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.curation.service.list; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; @@ -17,6 +18,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -33,14 +35,17 @@ public class CurationListService { // 1. 큐레이션 리스트 조회 public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long userId) { - // 1. 내 취향 유사도 순 O if (sortType == SortType.SORT_SIMILARITY) { // 1. 유저 독서 취향 반환 - ReadingPreferenceInfo preferenceInfo = readingPreferenceRepository.findByUserId(userId) - .map(ReadingPreferenceInfo::from) - .orElseThrow(UserReadingPreferenceNotExisted::new); + ReadingPreference readingPreference = readingPreferenceRepository.findByUserId(userId).orElse(null); + + + if(readingPreference == null){ + return CurationListGetRes.ofEmpty(sortType); + } + ReadingPreferenceInfo preferenceInfo = ReadingPreferenceInfo.from(readingPreference); // 2. 매칭된 큐레이션 리스트트 조회 List recommended = curationRecommendationService.recommend(preferenceInfo); @@ -116,6 +121,9 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, ); } + + + // Issue 1) DTO 만들어서 독서취향 정보 레이어간 소통 vs 사용자 독서취향 실시간 수정 반영 고려 // 1. 사용자는 독서취향을 한번 설정하면 자주 바꾸지 않는다. // 2. 따라서 DTO 생성 후 넣기로 결정 From 546cd0c56b918d7f2c7ab8d2c06bdccb508de3cb Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 20:23:36 +0900 Subject: [PATCH 215/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EC=84=A4=EC=A0=95=EC=8B=9C,=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=ED=95=B4=EB=86=A8=EB=8B=A4=EB=8A=94=20=EB=B6=80=EB=B6=84=20com?= =?UTF-8?q?plted=EB=A1=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreference/service/ReadingPreferenceService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index 857218d..dc79088 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -123,6 +123,11 @@ public ReadingPreferenceRes modifyReadingPreference(Long userId, ReadingPreferen preference.setFavoriteBooks(savedBooks); preference.setFavoriteAuthors(savedAuthors); + if(!preference.isCompleted()) { + preference.setCompleted(true); + } + + readingPreferenceValidCheckService.validateReadingPreferenceReq(req); // ReadingPreferenceReq 검증 preference.update(req); From 29869482f19527447fb9e9a510421ae04b0a707f Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 20 Dec 2025 20:33:08 +0900 Subject: [PATCH 216/291] =?UTF-8?q?feat=20:=20=EC=B7=A8=ED=96=A5=EC=9C=A0?= =?UTF-8?q?=EC=82=AC=EB=8F=84=20=EA=B8=B0=EB=B0=98=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=9D=98=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20DTO=EA=B0=80=20=EB=8B=AC=EB=9D=BC=EC=84=9C?= =?UTF-8?q?=20=ED=94=84=EB=A1=A0=ED=8A=B8=EA=B0=80=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=96=B4=EB=A0=B5=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90,=20=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EB=B9=88=20=EB=B0=B0?= =?UTF-8?q?=EC=97=B4=EB=A1=9C=20=EC=9D=91=EB=8B=B5=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issue : https://github.com/Book-Pick/bookpick-front/issues/54 --- .../enums/resCode/PreferenceErrorCode.java | 1 - .../enums/resCode/PreferenceSuccessCode.java | 18 ++++++++++++++++++ .../list/CurationListController.java | 7 +++++++ .../enums/common/CurationSuccessCode.java | 10 +++++++--- .../service/list/CurationListService.java | 6 +----- src/main/resources/application.yml | 2 +- 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java index 2d3dd64..05ea34e 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceErrorCode.java @@ -9,7 +9,6 @@ @AllArgsConstructor public enum PreferenceErrorCode implements ErrorCodeInterface { - PREFERENCE_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 독서취향이 설정되지 않았습니다."), WRONG_READING_PREFERENCE_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 독서취향 요청값입니다."); diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java new file mode 100644 index 0000000..77d1c6a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.ReadingPreference.enums.resCode; + +import BookPick.mvp.global.enums.ErrorCodeInterface; +import BookPick.mvp.global.enums.SuccessCodeInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum PreferenceSuccessCode implements SuccessCodeInterface { + + + PREFERENCE_NOT_FOUND(HttpStatus.OK, "사용자의 독서취향이 설정되지 않았습니다."); + + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 079c924..c45df73 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -1,5 +1,7 @@ package BookPick.mvp.domain.curation.controller.list; +import BookPick.mvp.domain.ReadingPreference.enums.resCode.PreferenceErrorCode; +import BookPick.mvp.domain.ReadingPreference.enums.resCode.PreferenceSuccessCode; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.enums.common.SortType; @@ -41,6 +43,11 @@ public ResponseEntity> getCurations( // 2. 큐레이션 리스트 반환 CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); + if(curationListGetRes.size() == 0 ){ + return ResponseEntity.ok() + .body(ApiResponse.success(PreferenceSuccessCode.PREFERENCE_NOT_FOUND, curationListGetRes)); + } + return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); } diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java index 34d6738..0b0faf9 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java @@ -10,21 +10,25 @@ public enum CurationSuccessCode implements SuccessCodeInterface { - // 1. 임시저장 + + // 임시저장 CREATE_DRAFTED_CURATION_SUCCESS(HttpStatus.CREATED, "큐레이션 임시저장에 성공했습니다"), - // 2. 좋아요 + // 좋아요 CURATION_LIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요를 성공적으로 실행하였습니다."), CURATION_DISLIKE_SUCCESS(HttpStatus.OK, "큐레이션 좋아요 취소를 성공적으로 실행하였습니다."), - // 3. 삭제 + + + // 삭제 CURATION_DELETE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 삭제하였습니다."), CURATION_LIST_DELETE_SUCCESS(HttpStatus.OK, "다수의 큐레이션을 성공적으로 삭제하였습니다."); + private final HttpStatus status; private final String message; } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 6efdaa4..0aa9f7b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -40,11 +40,7 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, // 1. 유저 독서 취향 반환 ReadingPreference readingPreference = readingPreferenceRepository.findByUserId(userId).orElse(null); - - - if(readingPreference == null){ - return CurationListGetRes.ofEmpty(sortType); - } + if(!readingPreference.isCompleted()){return CurationListGetRes.ofEmpty(sortType);} ReadingPreferenceInfo preferenceInfo = ReadingPreferenceInfo.from(readingPreference); // 2. 매칭된 큐레이션 리스트트 조회 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e2fc7b0..fad6a57 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,7 +10,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: none properties: hibernate: show_sql: false From a6bce7742df40adc14dffa6525e4010d6d16d25c Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 12:47:09 +0900 Subject: [PATCH 217/291] chore : not meaningfull commit --- .../ReadingPreference/enums/resCode/PreferenceSuccessCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java index 77d1c6a..05a0b94 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/resCode/PreferenceSuccessCode.java @@ -11,7 +11,7 @@ public enum PreferenceSuccessCode implements SuccessCodeInterface { - PREFERENCE_NOT_FOUND(HttpStatus.OK, "사용자의 독서취향이 설정되지 않았습니다."); + PREFERENCE_NOT_FOUND(HttpStatus.OK, "사용자의 독서취향이 설정되지 않았습니다."); private final HttpStatus status; private final String message; From 316e21d13a2ba1d560bade62def85dd2c31dbbc9 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 13:29:25 +0900 Subject: [PATCH 218/291] =?UTF-8?q?chore=20:=20gitIgnore=EC=97=90=20applic?= =?UTF-8?q?ation.yml=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0a0b8cd..b64d908 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ aws/ # macOS system files .DS_Store + +#applicatoin.yml +./src/main/resources/application.yml \ No newline at end of file From 06da6e4d32849d973ad12f31dfd981505cc5b522 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 14:41:42 +0900 Subject: [PATCH 219/291] =?UTF-8?q?chore=20:=20application-local.yml?= =?UTF-8?q?=EB=A1=9C=20=EB=A1=9C=EC=BB=AC=20yml=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/main/resources/application-dev.yml | 19 +++++++++ src/main/resources/application-local.yml | 34 ++++++++++++++++ src/main/resources/application-prod.yml | 19 +++++++++ src/main/resources/application.yml | 36 ---------------- src/main/resources/origin/origin.yml | 52 ++++++++++++++++++++++++ 6 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-local.yml create mode 100644 src/main/resources/application-prod.yml create mode 100644 src/main/resources/origin/origin.yml diff --git a/.gitignore b/.gitignore index b64d908..ad1aae0 100644 --- a/.gitignore +++ b/.gitignore @@ -119,4 +119,5 @@ aws/ #applicatoin.yml -./src/main/resources/application.yml \ No newline at end of file +./src/main/resources/origin/origin.yml +./src/main/resources/.env \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..4f5a0af --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,19 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + format_sql: true + +jwt: + secret: ${JWT_SECRET} + access-token-expire: ${JWT_ACCESS_EXPIRE} + refresh-token-expire: ${JWT_REFRESH_EXPIRE} diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..efd0ead --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,34 @@ +spring: + datasource: + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + show_sql: true + +logging: + level: + org.springframework.web: DEBUG + BookPick: DEBUG + +jwt: + access: + secret: ${JWT_ACCESS_SECRET} + expiration: ${JWT_ACCESS_EXPIRATION} + refresh: + secret: ${JWT_REFRESH_SECRET} + expiration: ${JWT_REFRESH_EXPIRATION} + +api: + kakao: + key: ${KAKAO_API_KEY} + gemini: + api: + key: ${GEMINI_API_KEY} + url: ${GEMINI_API_URL} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..eed7fff --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,19 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?serverTimezone=Asia/Seoul + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + jpa: + hibernate: + ddl-auto: update + show-sql: true + properties: + hibernate: + format_sql: true + +jwt: + secret: ${JWT_SECRET} + access-token-expire: ${JWT_ACCESS_EXPIRE} + refresh-token-expire: ${JWT_REFRESH_EXPIRE} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fad6a57..442293c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,19 +2,6 @@ spring: application: name: BookPick - datasource: - url: jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick - username: nan7789 - password: gustjq3735! - driver-class-name: com.mysql.cj.jdbc.Driver - - jpa: - hibernate: - ddl-auto: none - properties: - hibernate: - show_sql: false - server: port: 8081 error: @@ -23,29 +10,6 @@ server: include-stacktrace: always include-exception: true - - logging: level: root: INFO - org.springframework.web: DEBUG - org.springframework.security: DEBUG - BookPick: DEBUG - -jwt: - access: - secret: c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM - expiration: 900000 # 15분 = 15 * 60 * 1000 - refresh: - secret: d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw - expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 - -api : - kakao : - key : 103086ac1d365cf71f026f6caac34fb3 - gemini: - api: - key: AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw - url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent - - diff --git a/src/main/resources/origin/origin.yml b/src/main/resources/origin/origin.yml new file mode 100644 index 0000000..f44642b --- /dev/null +++ b/src/main/resources/origin/origin.yml @@ -0,0 +1,52 @@ +spring: + application: + name: BookPick + + datasource: + url: jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick + username: nan7789 + password: gustjq3735! + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + show_sql: false + +server: + port: 8081 + error: + include-message: always + include-binding-errors: always + include-stacktrace: always + include-exception: true + + + +logging: + level: + root: INFO + org.springframework.web: DEBUG + org.springframework.security: DEBUG + BookPick: DEBUG + +jwt: + access: + secret: c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM + expiration: 900000 # 15분 = 15 * 60 * 1000 + refresh: + secret: d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw + expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 + +api : + kakao : + key : 103086ac1d365cf71f026f6caac34fb3 + gemini: + api: + key: AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw + url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + + + From e2fa45cd4a261904e96dc124d9adcc232cc289f4 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 16:16:31 +0900 Subject: [PATCH 220/291] =?UTF-8?q?chore=20:=20api=20=ED=82=A4=EA=B0=92=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=98=88=EB=AF=BC=ED=95=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=B3=B4=ED=98=B8=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=20.env=20=ED=8C=8C=EC=9D=BC=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=EB=A1=9C=20=EA=B4=80=EB=A6=AC=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/{deploy.yml => dev.yml} | 0 .github/workflows/prod.yml | 73 +++++++++++++++++++++++ Dockerfile | 2 +- src/main/resources/application-dev.yml | 29 ++++++--- src/main/resources/application-local.yml | 2 +- src/main/resources/application-prod.yml | 38 ++++++------ 6 files changed, 116 insertions(+), 28 deletions(-) rename .github/workflows/{deploy.yml => dev.yml} (100%) create mode 100644 .github/workflows/prod.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/dev.yml similarity index 100% rename from .github/workflows/deploy.yml rename to .github/workflows/dev.yml diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml new file mode 100644 index 0000000..5f59a03 --- /dev/null +++ b/.github/workflows/prod.yml @@ -0,0 +1,73 @@ +#name: BookPick BE dev CI/CD (v 1.2) +# +# +#on: +# pull_request: +# branches: +# - develop # develop을 대상으로 하는 PR만 감지 +# types: [ closed ] # PR이 닫힐 때만 실행 +# +# push: +# branches: +# - develop # develop을 대상으로 하는 PR만 감지 +# +# +#jobs: +# # test: +# # runs-on: ubuntu-latest +# # steps: +# # +# # - name : 1. checkout repo +# # uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 +# # +# ## - name: 2. run test +# ## run: ./gradlew test +# # +# # - name: 3. set up JDK 21 +# # uses: actions/setup-java@v2 +# # with: +# # java-version: 21 +# # distribution: 'temurin' +# # +# # - name: 4. grant execute permission for gradlew +# # run: chmod +x gradlew +# +# +# build-and-push: +# # needs: test +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 +# +# +# - name: 3. Docker Hub Login +# uses: docker/login-action@v3 +# with: +# username: ${{secrets.DOCKER_USERNAME}} +# password: ${{secrets.DOCKER_TOKEN}} +# +# - name: 4. Spring Image Build and Push +# run: | +# docker build --platform linux/amd64 \ +# -t ${{ secrets.DOCKER_USERNAME }}/${{secrets.IMAGE}} \ +# --push . +# deploy: +# needs: build-and-push +# # if: github.event.pull_request.merged == true # merge된 경우에만 실행 +# runs-on: ubuntu-latest +# steps: +# - name: Set up SSH # 1. SSH 설정 +# run: | +# mkdir -p ~/.ssh +# echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa +# chmod 600 ~/.ssh/id_rsa +# ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts +# +# - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 +# run: | +# ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} 'bash -s' < Date: Sun, 21 Dec 2025 16:31:23 +0900 Subject: [PATCH 221/291] =?UTF-8?q?chore=20:=20origin.yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 4 +--- .gitignore | 3 ++- src/main/resources/origin/origin.yml | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 4780430..19ffae1 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -7,9 +7,7 @@ on: - develop # develop을 대상으로 하는 PR만 감지 types: [ closed ] # PR이 닫힐 때만 실행 - push: - branches: - - develop # develop을 대상으로 하는 PR만 감지 + jobs: diff --git a/.gitignore b/.gitignore index ad1aae0..b198a12 100644 --- a/.gitignore +++ b/.gitignore @@ -120,4 +120,5 @@ aws/ #applicatoin.yml ./src/main/resources/origin/origin.yml -./src/main/resources/.env \ No newline at end of file +./src/main/resources/.env +./ \ No newline at end of file diff --git a/src/main/resources/origin/origin.yml b/src/main/resources/origin/origin.yml index f44642b..a2e34d0 100644 --- a/src/main/resources/origin/origin.yml +++ b/src/main/resources/origin/origin.yml @@ -45,7 +45,7 @@ api : key : 103086ac1d365cf71f026f6caac34fb3 gemini: api: - key: AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw + key: AIzaSyB_WGw2GtuKeXP9uzup9SQlcN7Bvt8v0eM url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent From 94e65c9633c7f3d78f5f08436440c27ec8aa190e Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 16:34:40 +0900 Subject: [PATCH 222/291] =?UTF-8?q?chore=20:=20develop=EC=97=90=20push?= =?UTF-8?q?=EB=90=98=EB=A9=B4=20=EC=9E=91=EB=8F=99=EB=90=98=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 19ffae1..efe543d 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -2,12 +2,16 @@ name: BookPick BE dev CI/CD (v 1.2) on: - pull_request: - branches: - - develop # develop을 대상으로 하는 PR만 감지 - types: [ closed ] # PR이 닫힐 때만 실행 +# pull_request: +# branches: +# - develop # develop을 대상으로 하는 PR만 감지 +# types: [ closed ] # PR이 닫힐 때만 실행 + # PR Merge 수락 시에도 작동됨 + push: + branches: + - develop # develop을 대상으로 하는 PR만 감지 jobs: From 53dc1cb0c61198695088e83884679e576a80c7fa Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 16:46:11 +0900 Subject: [PATCH 223/291] =?UTF-8?q?chore=20:=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EA=B0=9C=EB=B0=9C=EC=8B=9C=20=ED=99=95=EC=9E=A5=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EB=A0=A4=ED=95=9C=20prod.yml=EC=9D=B4=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC=EB=A5=BC=20=ED=95=B4?= =?UTF-8?q?=EB=86=94=EB=8F=84=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=EB=90=98=EC=96=B4=EC=84=9C,=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prod.yml | 73 -------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 .github/workflows/prod.yml diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml deleted file mode 100644 index 5f59a03..0000000 --- a/.github/workflows/prod.yml +++ /dev/null @@ -1,73 +0,0 @@ -#name: BookPick BE dev CI/CD (v 1.2) -# -# -#on: -# pull_request: -# branches: -# - develop # develop을 대상으로 하는 PR만 감지 -# types: [ closed ] # PR이 닫힐 때만 실행 -# -# push: -# branches: -# - develop # develop을 대상으로 하는 PR만 감지 -# -# -#jobs: -# # test: -# # runs-on: ubuntu-latest -# # steps: -# # -# # - name : 1. checkout repo -# # uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 -# # -# ## - name: 2. run test -# ## run: ./gradlew test -# # -# # - name: 3. set up JDK 21 -# # uses: actions/setup-java@v2 -# # with: -# # java-version: 21 -# # distribution: 'temurin' -# # -# # - name: 4. grant execute permission for gradlew -# # run: chmod +x gradlew -# -# -# build-and-push: -# # needs: test -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 -# -# -# - name: 3. Docker Hub Login -# uses: docker/login-action@v3 -# with: -# username: ${{secrets.DOCKER_USERNAME}} -# password: ${{secrets.DOCKER_TOKEN}} -# -# - name: 4. Spring Image Build and Push -# run: | -# docker build --platform linux/amd64 \ -# -t ${{ secrets.DOCKER_USERNAME }}/${{secrets.IMAGE}} \ -# --push . -# deploy: -# needs: build-and-push -# # if: github.event.pull_request.merged == true # merge된 경우에만 실행 -# runs-on: ubuntu-latest -# steps: -# - name: Set up SSH # 1. SSH 설정 -# run: | -# mkdir -p ~/.ssh -# echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa -# chmod 600 ~/.ssh/id_rsa -# ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts -# -# - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 -# run: | -# ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} 'bash -s' < Date: Sun, 21 Dec 2025 16:51:54 +0900 Subject: [PATCH 224/291] =?UTF-8?q?chore=20:=20=EB=A6=AC=EC=86=8C=EC=8A=A4?= =?UTF-8?q?=20=EC=98=A4=EB=A6=AC=EC=A7=84=20=ED=8F=B4=EB=8D=94=20gitInore?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b198a12..5afea09 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,5 @@ aws/ #applicatoin.yml -./src/main/resources/origin/origin.yml +./src/main/resources/origin ./src/main/resources/.env -./ \ No newline at end of file From cfe10e78c9ae05fa7d3215e82edad08e8887909d Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 21 Dec 2025 17:13:30 +0900 Subject: [PATCH 225/291] =?UTF-8?q?chore=20:=20=EB=A6=AC=EC=86=8C=EC=8A=A4?= =?UTF-8?q?=20=EC=98=A4=EB=A6=AC=EC=A7=84=20=EA=B9=83=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/origin/origin.yml | 52 ---------------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/main/resources/origin/origin.yml diff --git a/src/main/resources/origin/origin.yml b/src/main/resources/origin/origin.yml deleted file mode 100644 index a2e34d0..0000000 --- a/src/main/resources/origin/origin.yml +++ /dev/null @@ -1,52 +0,0 @@ -spring: - application: - name: BookPick - - datasource: - url: jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick - username: nan7789 - password: gustjq3735! - driver-class-name: com.mysql.cj.jdbc.Driver - - jpa: - hibernate: - ddl-auto: none - properties: - hibernate: - show_sql: false - -server: - port: 8081 - error: - include-message: always - include-binding-errors: always - include-stacktrace: always - include-exception: true - - - -logging: - level: - root: INFO - org.springframework.web: DEBUG - org.springframework.security: DEBUG - BookPick: DEBUG - -jwt: - access: - secret: c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM - expiration: 900000 # 15분 = 15 * 60 * 1000 - refresh: - secret: d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw - expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 - -api : - kakao : - key : 103086ac1d365cf71f026f6caac34fb3 - gemini: - api: - key: AIzaSyB_WGw2GtuKeXP9uzup9SQlcN7Bvt8v0eM - url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent - - - From c3404ece8e08e0bea89860c28e24141b5ff9e27d Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 22 Dec 2025 14:41:45 +0900 Subject: [PATCH 226/291] =?UTF-8?q?docs=20:=20Readme=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- README.md | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 212 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5afea09..eac8405 100644 --- a/.gitignore +++ b/.gitignore @@ -101,8 +101,9 @@ out/ !**/src/main/**/out/ !**/src/test/**/out/ -### NetBeans ### -/nbproject/private/ +Eㅇㄴdsf + + /nbbuild/ /dist/ /nbdist/ diff --git a/README.md b/README.md index 0292963..66256fb 100644 --- a/README.md +++ b/README.md @@ -1 +1,209 @@ -# bookpick-back +# BookPick Backend + +BookPick MVP 버전의 백엔드 API 서버입니다. AI 기반 도서 큐레이션 및 추천 서비스를 제공합니다. + +## Tech Stack + +### Core +- **Java 17** +- **Spring Boot 3.5.6** +- **Spring Security 6** +- **Spring Data JPA** +- **MySQL 8.0+** + +### Libraries & Tools +- **JWT (JJWT 0.12.6)** - 사용자 인증/인가 +- **SpringDoc OpenAPI 2.8.11** - API 문서 자동화 (Swagger UI) +- **Lombok** - 보일러플레이트 코드 제거 +- **Gemini AI** - AI 기반 도서 추천 및 큐레이션 +- **Gradle** - 빌드 도구 +- **Docker** - 컨테이너화 및 배포 + +## Key Features + +### 인증 및 사용자 관리 +- JWT 기반 인증/인가 시스템 +- 회원가입, 로그인, 로그아웃 +- Access Token / Refresh Token 관리 + +### 도서 큐레이션 +- AI 기반 도서 추천 (Gemini API 연동) +- 큐레이션 생성, 조회, 수정, 삭제 (CRUD) +- 커서 기반 페이지네이션 +- 좋아요 기능 + +### 커뮤니티 +- 댓글 작성 및 관리 +- 사용자 독서 취향 관리 + +## Project Structure + +``` +src/main/java/BookPick/mvp +├── domain +│ ├── auth # 인증/인가 +│ │ ├── controller +│ │ ├── service +│ │ └── dto +│ ├── curation # 도서 큐레이션 +│ │ ├── controller +│ │ ├── service +│ │ ├── repository +│ │ ├── dto +│ │ └── util +│ │ └── gemini # Gemini AI 연동 +│ ├── comment # 댓글 +│ └── ReadingPreference # 독서 취향 +├── security # Security 설정 +│ ├── config +│ └── handler +└── global # 공통 설정 및 유틸 + +src/main/resources +├── application.yml # 공통 설정 +├── application-local.yml # 로컬 환경 +├── application-dev.yml # 개발 환경 +└── application-prod.yml # 운영 환경 +``` + +## Getting Started + +### Prerequisites +- JDK 17 이상 +- MySQL 8.0+ +- Gradle 8.x (Wrapper 포함) + +### Environment Setup + +1. 환경변수 설정 파일 생성 +```bash +cp src/main/resources/.env.example src/main/resources/.env +``` + +2. `.env` 파일 설정 +```properties +# Database +SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/bookpick?serverTimezone=Asia/Seoul +SPRING_DATASOURCE_USERNAME=your_username +SPRING_DATASOURCE_PASSWORD=your_password + +# JWT +JWT_SECRET=your-secret-key-here +JWT_ACCESS_TOKEN_EXPIRATION=3600000 +JWT_REFRESH_TOKEN_EXPIRATION=604800000 + +# Gemini API +GEMINI_API_KEY=your-gemini-api-key +``` + +### Local Development + +```bash +# 의존성 설치 및 빌드 +./gradlew clean build + +# 애플리케이션 실행 (local 프로파일) +./gradlew bootRun --args='--spring.profiles.active=local' + +# 또는 +java -jar build/libs/*.jar --spring.profiles.active=local +``` + +서버는 `http://localhost:8081`에서 실행됩니다. + +### Docker + +```bash +# 이미지 빌드 +docker build -t bookpick-backend . + +# 컨테이너 실행 +docker run -p 8081:8081 \ + -e SPRING_PROFILES_ACTIVE=dev \ + --name bookpick-api \ + bookpick-backend +``` + +## API Documentation + +애플리케이션 실행 후 아래 주소에서 API 문서를 확인할 수 있습니다: + +- **Swagger UI**: `http://localhost:8081/swagger-ui.html` +- **OpenAPI JSON**: `http://localhost:8081/v3/api-docs` + +### Main Endpoints + +#### 인증 +- `POST /api/auth/signup` - 회원가입 +- `POST /api/auth/login` - 로그인 +- `POST /api/auth/logout` - 로그아웃 +- `POST /api/auth/refresh` - 토큰 갱신 + +#### 큐레이션 +- `GET /api/curations` - 큐레이션 목록 조회 (커서 페이지네이션) +- `GET /api/curations/{id}` - 큐레이션 상세 조회 +- `POST /api/curations` - 큐레이션 생성 +- `PUT /api/curations/{id}` - 큐레이션 수정 +- `DELETE /api/curations/{id}` - 큐레이션 삭제 +- `POST /api/curations/{id}/like` - 좋아요 토글 + +#### 독서 취향 +- `GET /api/preferences` - 독서 취향 조회 +- `POST /api/preferences` - 독서 취향 등록/수정 + +## Development + +### Code Style +- Java 코드 스타일은 Google Java Style Guide 준수 +- Lombok 활용하여 보일러플레이트 최소화 + +### Testing +```bash +# 전체 테스트 실행 +./gradlew test + +# 특정 테스트 실행 +./gradlew test --tests "BookPick.mvp.domain.auth.*" +``` + +### Branch Strategy +- `main` - 운영 환경 배포 브랜치 +- `develop` - 개발 환경 통합 브랜치 +- `features/*` - 기능 개발 브랜치 +- `hotfix/*` - 긴급 버그 수정 브랜치 + +## CI/CD + +GitHub Actions를 통한 자동 배포 파이프라인: +- `develop` 브랜치 푸시 시 개발 서버 자동 배포 +- PR 생성 시 빌드 및 테스트 자동 실행 + +## Troubleshooting + +### MySQL 연결 오류 +``` +Caused by: java.sql.SQLException: Access denied for user +``` +→ `.env` 파일의 데이터베이스 계정 정보를 확인하세요. + +### JWT 관련 오류 +``` +io.jsonwebtoken.security.WeakKeyException +``` +→ `JWT_SECRET` 환경변수가 충분히 긴 키(최소 256bit)인지 확인하세요. + +## Contributing + +1. 이슈 생성 또는 할당된 이슈 확인 +2. Feature 브랜치 생성 (`git checkout -b features/AmazingFeature`) +3. 변경사항 커밋 (`git commit -m 'Add some AmazingFeature'`) +4. 브랜치에 푸시 (`git push origin features/AmazingFeature`) +5. Pull Request 생성 + +## License + +This project is licensed under the MIT License. + +## Contact + +프로젝트 관련 문의사항은 이슈를 통해 남겨주세요. From 0ec4b866f4f815798694a53d67f29581a5231090 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 22 Dec 2025 21:47:10 +0900 Subject: [PATCH 227/291] =?UTF-8?q?chore=20:=20=EC=8B=9C=ED=81=AC=EB=A6=BF?= =?UTF-8?q?=20=ED=82=A4=20=EA=B0=92=20=EC=9C=A0=EC=B6=9C=EC=9D=84=20?= =?UTF-8?q?=EB=A7=89=EA=B8=B0=20=EC=9C=84=ED=95=9C,=20=EA=B9=83=ED=97=88?= =?UTF-8?q?=EB=B8=8C=20=EC=9B=90=EB=B3=B8=20yml=20=EB=B0=8F=20.env=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/.env | 16 +++++++++ src/main/resources/origin/origin.yml | 52 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/main/resources/.env create mode 100644 src/main/resources/origin/origin.yml diff --git a/src/main/resources/.env b/src/main/resources/.env new file mode 100644 index 0000000..a8c6596 --- /dev/null +++ b/src/main/resources/.env @@ -0,0 +1,16 @@ +SPRING_PROFILES_ACTIVE=local + +DB_HOST=hii.mysql.database.azure.com +DB_PORT=3306 +DB_NAME=book_pick +DB_USERNAME=nan7789 +DB_PASSWORD=gustjq3735! + +JWT_ACCESS_SECRET=c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM +JWT_ACCESS_EXPIRATION=900000 +JWT_REFRESH_SECRET=d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw +JWT_REFRESH_EXPIRATION=604800000 + +KAKAO_API_KEY=103086ac1d365cf71f026f6caac34fb3 +GEMINI_API_KEY=AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw +GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent diff --git a/src/main/resources/origin/origin.yml b/src/main/resources/origin/origin.yml new file mode 100644 index 0000000..a2e34d0 --- /dev/null +++ b/src/main/resources/origin/origin.yml @@ -0,0 +1,52 @@ +spring: + application: + name: BookPick + + datasource: + url: jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick + username: nan7789 + password: gustjq3735! + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + show_sql: false + +server: + port: 8081 + error: + include-message: always + include-binding-errors: always + include-stacktrace: always + include-exception: true + + + +logging: + level: + root: INFO + org.springframework.web: DEBUG + org.springframework.security: DEBUG + BookPick: DEBUG + +jwt: + access: + secret: c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM + expiration: 900000 # 15분 = 15 * 60 * 1000 + refresh: + secret: d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw + expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 + +api : + kakao : + key : 103086ac1d365cf71f026f6caac34fb3 + gemini: + api: + key: AIzaSyB_WGw2GtuKeXP9uzup9SQlcN7Bvt8v0eM + url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + + + From c9d0551ed959c5cf8f7f46f3c887d7774107369d Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 22 Dec 2025 21:56:43 +0900 Subject: [PATCH 228/291] =?UTF-8?q?chore=20:=20dto=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/read/CurationReadController.java | 3 +-- .../domain/curation/dto/base/create/ETC/BookDto.java | 1 - .../dto/base/get/one/{ => field}/BookInfo.java | 2 +- .../dto/base/get/one/{ => field}/CurationGetRes.java | 2 +- .../dto/base/get/one/{ => field}/RecommendInfo.java | 2 +- .../dto/base/get/one/{ => field}/ThumbnailInfo.java | 2 +- .../domain/curation/service/base/CurationService.java | 6 +----- .../service/base/create/CurationCreateService.java | 10 ---------- .../service/base/read/CurationReadService.java | 8 +------- src/main/resources/application-local.yml | 2 +- 10 files changed, 8 insertions(+), 30 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/{ => field}/BookInfo.java (84%) rename src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/{ => field}/CurationGetRes.java (97%) rename src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/{ => field}/RecommendInfo.java (73%) rename src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/{ => field}/ThumbnailInfo.java (53%) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java index 3b9880b..e1d0cbb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java @@ -1,8 +1,7 @@ package BookPick.mvp.domain.curation.controller.base.read; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.curation.dto.base.get.one.field.CurationGetRes; import BookPick.mvp.domain.curation.service.base.read.CurationReadService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java index 70a9f45..dc46fe6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/ETC/BookDto.java @@ -1,6 +1,5 @@ package BookPick.mvp.domain.curation.dto.base.create.ETC; -import BookPick.mvp.domain.curation.dto.base.get.one.BookInfo; import BookPick.mvp.domain.curation.entity.Curation; // 책 정보 diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/BookInfo.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/BookInfo.java index 2b5a3b4..efc0531 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/BookInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/BookInfo.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.base.get.one; +package BookPick.mvp.domain.curation.dto.base.get.one.field; import BookPick.mvp.domain.curation.entity.Curation; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/CurationGetRes.java similarity index 97% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/CurationGetRes.java index 0ed4c89..9c841a7 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/CurationGetRes.java @@ -1,5 +1,5 @@ // CurationGetRes.java -package BookPick.mvp.domain.curation.dto.base.get.one; +package BookPick.mvp.domain.curation.dto.base.get.one.field; import BookPick.mvp.domain.curation.entity.Curation; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/RecommendInfo.java similarity index 73% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/RecommendInfo.java index 91d2552..c8bd6b5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/RecommendInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/RecommendInfo.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.base.get.one; +package BookPick.mvp.domain.curation.dto.base.get.one.field; import java.util.List; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/ThumbnailInfo.java similarity index 53% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/ThumbnailInfo.java index 6e0b17c..0c502f6 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/ThumbnailInfo.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/ThumbnailInfo.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.dto.base.get.one; +package BookPick.mvp.domain.curation.dto.base.get.one.field; public record ThumbnailInfo(String imageUrl, String imageColor) { } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 83e2485..830192e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -2,9 +2,7 @@ package BookPick.mvp.domain.curation.service.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.CurationReq; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.get.one.field.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.entity.Curation; @@ -13,8 +11,6 @@ import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java index 26dbe29..7a033df 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java @@ -1,29 +1,19 @@ // CurationListService.java package BookPick.mvp.domain.curation.service.base.create; -import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; -import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor public class CurationCreateService { diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index e9a3649..f84e0a4 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -2,19 +2,13 @@ package BookPick.mvp.domain.curation.service.base.read; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.CurationReq; -import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.base.get.one.field.CurationGetRes; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index deff22f..083af58 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -26,7 +26,7 @@ jwt: expiration: ${JWT_REFRESH_EXPIRATION} api: - kakao: + kakao:W key: ${KAKAO_API_KEY} gemini: api: From b30aeb1455951b2aabb889d89187a3d09cbf0a20 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 22 Dec 2025 23:05:25 +0900 Subject: [PATCH 229/291] =?UTF-8?q?chore=20:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base/read/CurationReadController.java | 2 +- .../get/one/{field => }/CurationGetRes.java | 8 ++- .../service/base/CurationService.java | 2 +- .../base/read/CurationReadService.java | 4 +- src/main/resources/.env | 16 ------ src/main/resources/application-local.yml | 2 +- src/main/resources/origin/origin.yml | 52 ------------------- 7 files changed, 12 insertions(+), 74 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/{field => }/CurationGetRes.java (87%) delete mode 100644 src/main/resources/.env delete mode 100644 src/main/resources/origin/origin.yml diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java index e1d0cbb..4bedc85 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.curation.controller.base.read; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.get.one.field.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.service.base.read.CurationReadService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/CurationGetRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java similarity index 87% rename from src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/CurationGetRes.java rename to src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java index 9c841a7..e31d736 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/field/CurationGetRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/one/CurationGetRes.java @@ -1,6 +1,9 @@ // CurationGetRes.java -package BookPick.mvp.domain.curation.dto.base.get.one.field; +package BookPick.mvp.domain.curation.dto.base.get.one; +import BookPick.mvp.domain.curation.dto.base.get.one.field.BookInfo; +import BookPick.mvp.domain.curation.dto.base.get.one.field.RecommendInfo; +import BookPick.mvp.domain.curation.dto.base.get.one.field.ThumbnailInfo; import BookPick.mvp.domain.curation.entity.Curation; import java.time.LocalDateTime; @@ -18,6 +21,7 @@ public record CurationGetRes( String review, RecommendInfo recommend, Boolean isLiked, + Boolean isDrafted, Integer likeCount, Integer viewCount, Integer CommentCount, @@ -40,6 +44,7 @@ public static CurationGetRes from(Curation curation, boolean subscribed, boolean new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), isLiked, + curation.getIsDrafted(), curation.getLikeCount(), curation.getViewCount(), curation.getCommentCount(), @@ -62,6 +67,7 @@ public static CurationGetRes fromOwnerView(Curation curation, boolean subscribed new RecommendInfo(curation.getMoods(), curation.getGenres(), curation.getKeywords(), curation.getStyles()), isLiked, + curation.getIsDrafted(), curation.getLikeCount(), curation.getViewCount(), curation.getCommentCount(), diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java index 830192e..3eea9de 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java @@ -2,7 +2,7 @@ package BookPick.mvp.domain.curation.service.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.get.one.field.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.entity.Curation; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index f84e0a4..6ff8d95 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -2,7 +2,7 @@ package BookPick.mvp.domain.curation.service.base.read; import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.get.one.field.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; @@ -55,7 +55,7 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http isSubscribedCurator = curationSubscribeService.isSubscribeCurator(user.getId(), curation.getUser().getId()); // 3. 큐레이션 작성자면 책 정보 넣어서 큐레이션 반환 - if ( isEdit) { + if (isEdit) { if(curation.getUser().getId().equals(user.getId())){ return CurationGetRes.fromOwnerView(curation, isSubscribedCurator, isLikedCuration); } diff --git a/src/main/resources/.env b/src/main/resources/.env deleted file mode 100644 index a8c6596..0000000 --- a/src/main/resources/.env +++ /dev/null @@ -1,16 +0,0 @@ -SPRING_PROFILES_ACTIVE=local - -DB_HOST=hii.mysql.database.azure.com -DB_PORT=3306 -DB_NAME=book_pick -DB_USERNAME=nan7789 -DB_PASSWORD=gustjq3735! - -JWT_ACCESS_SECRET=c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM -JWT_ACCESS_EXPIRATION=900000 -JWT_REFRESH_SECRET=d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw -JWT_REFRESH_EXPIRATION=604800000 - -KAKAO_API_KEY=103086ac1d365cf71f026f6caac34fb3 -GEMINI_API_KEY=AIzaSyAKdrlSfIagTGQhdtdaRmTyTVuI2QQLEsw -GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 083af58..deff22f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -26,7 +26,7 @@ jwt: expiration: ${JWT_REFRESH_EXPIRATION} api: - kakao:W + kakao: key: ${KAKAO_API_KEY} gemini: api: diff --git a/src/main/resources/origin/origin.yml b/src/main/resources/origin/origin.yml deleted file mode 100644 index a2e34d0..0000000 --- a/src/main/resources/origin/origin.yml +++ /dev/null @@ -1,52 +0,0 @@ -spring: - application: - name: BookPick - - datasource: - url: jdbc:mysql://hii.mysql.database.azure.com:3306/book_pick - username: nan7789 - password: gustjq3735! - driver-class-name: com.mysql.cj.jdbc.Driver - - jpa: - hibernate: - ddl-auto: none - properties: - hibernate: - show_sql: false - -server: - port: 8081 - error: - include-message: always - include-binding-errors: always - include-stacktrace: always - include-exception: true - - - -logging: - level: - root: INFO - org.springframework.web: DEBUG - org.springframework.security: DEBUG - BookPick: DEBUG - -jwt: - access: - secret: c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM - expiration: 900000 # 15분 = 15 * 60 * 1000 - refresh: - secret: d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw - expiration: 604800000 # 7일 = 7 * 24 * 60 * 60 * 1000 - -api : - kakao : - key : 103086ac1d365cf71f026f6caac34fb3 - gemini: - api: - key: AIzaSyB_WGw2GtuKeXP9uzup9SQlcN7Bvt8v0eM - url : https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent - - - From 5d57ec11de47f47326ddb8fc367bb7fc961e2bb3 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 22 Dec 2025 23:08:47 +0900 Subject: [PATCH 230/291] =?UTF-8?q?chore=20:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index eac8405..819eb55 100644 --- a/.gitignore +++ b/.gitignore @@ -119,6 +119,6 @@ aws/ .DS_Store -#applicatoin.yml +#applicatoin.ym ./src/main/resources/origin ./src/main/resources/.env From 10e06b8b4a7019f750cf0819af5f94f4b5bab4d2 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 22 Dec 2025 23:11:29 +0900 Subject: [PATCH 231/291] =?UTF-8?q?chore=20:=20=EC=9D=98=EB=AF=B8=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EC=BB=A4=EB=B0=8B=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 819eb55..8fadfc6 100644 --- a/.gitignore +++ b/.gitignore @@ -120,5 +120,5 @@ aws/ #applicatoin.ym -./src/main/resources/origin -./src/main/resources/.env +src/main/resources/origin +src/main/resources/.env \ No newline at end of file From 5c315981f98c07e63894cc0679893eebc9614965 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 23 Dec 2025 22:06:54 +0900 Subject: [PATCH 232/291] =?UTF-8?q?chore=20:=20todo=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/controller/list/CurationListController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index c45df73..3cf0414 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -25,6 +25,8 @@ public class CurationListController { private final CurrentUserCheck currentUserCheck; + // Todo 1. isDrafted = false 인, 큐레이션 리스트 반환 + @Operation(summary = "큐레이션 목록 조회", description = "최신순 / 인기순 / 사용자 취향 유사도 순", tags = {"Curation"}) @GetMapping public ResponseEntity> getCurations( @@ -53,9 +55,9 @@ public ResponseEntity> getCurations( } - /*@Operation(summary = "큐레이션 임시저장 목록 조회", description = "임시저장", tags = {"Curation"}) + @Operation(summary = "큐레이션 임시저장 목록 조회", description = "임시저장", tags = {"Curation"}) @GetMapping - public ResponseEntity> getCurations( + public ResponseEntity> getDraftedCurations( @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size, @@ -73,7 +75,7 @@ public ResponseEntity> getCurations( return ResponseEntity.ok() .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); - }*/ + } } From 326c7fe058212a4a9428803299c2c923f1e6febe Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 23 Dec 2025 22:06:54 +0900 Subject: [PATCH 233/291] =?UTF-8?q?feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=B6=84=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 6 +-- .../list/CurationListController.java | 25 ++---------- .../read/CurationReadController.java | 2 +- .../base/create/CurationCreateService.java | 40 +++++++++++++++---- .../service/draft/CurationDraftService.java | 9 +++++ .../service/list/CurationListService.java | 5 ++- 6 files changed, 53 insertions(+), 34 deletions(-) rename src/main/java/BookPick/mvp/domain/curation/controller/{base => }/read/CurationReadController.java (97%) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index d8e14cd..f9a9e82 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -32,13 +32,13 @@ public class CurationController { @Operation(summary = "큐레이션 생성(일반 및 임시저장)", description = "새 큐레이션을 생성합니다 drafted가 true면 임시저장", tags = {"Curation"}) @PostMapping - public ResponseEntity> create( + public ResponseEntity> createCuration( @Valid @RequestBody CurationReq req, @AuthenticationPrincipal CustomUserDetails currentUser) { currentUserCheck.validateLoginUser(currentUser); - CurationCreateRes res = curationCreateService.createCuration(currentUser.getId(), req); + CurationCreateRes res = curationCreateService.publishCuration(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); } @@ -47,7 +47,7 @@ public ResponseEntity> create( - @Operation(summary = "큐레이션 수정", description = "큐레이션 정보를 수정", tags = {"Curation"}) + @Operation(summary = "큐레이션 수정 (재발행 및 재 임시저장", description = "큐레이션 정보를 수정", tags = {"Curation"}) @PatchMapping("/{curationId}") public ResponseEntity> updateCuration( @PathVariable Long curationId, diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index c45df73..2431193 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -25,12 +25,15 @@ public class CurationListController { private final CurrentUserCheck currentUserCheck; + // Todo 1. isDrafted = false 인, 큐레이션 리스트 반환 + @Operation(summary = "큐레이션 목록 조회", description = "최신순 / 인기순 / 사용자 취향 유사도 순", tags = {"Curation"}) @GetMapping public ResponseEntity> getCurations( @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "false") boolean drafted, @AuthenticationPrincipal @Valid CustomUserDetails currentUser ) { @@ -41,7 +44,7 @@ public ResponseEntity> getCurations( SortType sortType = SortType.fromValue(sort); // 2. 큐레이션 리스트 반환 - CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); + CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, drafted, currentUser.getId()); if(curationListGetRes.size() == 0 ){ return ResponseEntity.ok() @@ -53,27 +56,7 @@ public ResponseEntity> getCurations( } - /*@Operation(summary = "큐레이션 임시저장 목록 조회", description = "임시저장", tags = {"Curation"}) - @GetMapping - public ResponseEntity> getCurations( - @RequestParam(defaultValue = "latest") String sort, - @RequestParam(required = false) Long cursor, - @RequestParam(defaultValue = "10") int size, - @AuthenticationPrincipal @Valid CustomUserDetails currentUser - ) { - - currentUserCheck.validateLoginUser(currentUser); - - - // 1. SortType 변환 - SortType sortType = SortType.fromValue(sort); - // 2. 큐레이션 리스트 반환 - CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, currentUser.getId()); - - return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); - }*/ } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java b/src/main/java/BookPick/mvp/domain/curation/controller/read/CurationReadController.java similarity index 97% rename from src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java rename to src/main/java/BookPick/mvp/domain/curation/controller/read/CurationReadController.java index 4bedc85..6617a55 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/read/CurationReadController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/read/CurationReadController.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.curation.controller.base.read; +package BookPick.mvp.domain.curation.controller.read; import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java index 7a033df..d908cc3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java @@ -24,20 +24,46 @@ public class CurationCreateService { private final CurationSubscribeService curationSubscribeService; + // 분기 + public CurationCreateRes saveCuration(Long userId, CurationReq req) { + + + // 발행 + if(!req.isDrafted()){ + return publishCuration(userId, req); + } + else{ + return draftCuration(userId,req); + } + + } // -- 큐레이션 등록 -- @Transactional - public CurationCreateRes createCuration(Long userId, CurationReq req) { + public CurationCreateRes publishCuration(Long userId, CurationReq req) { User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); - Curation curation = Curation.from(req, user); - if (req.isDrafted()) { - curation.draft(); - }else{ - curation.publish(); - } + Curation curation = Curation.from(req, user ); + curation.publish(); + Curation saved = curationRepository.save(curation); + + return CurationCreateRes.from(saved); + + } + + + // -- 큐레이션 임시저장 -- + @Transactional + public CurationCreateRes draftCuration(Long userId, CurationReq req) { + + + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + Curation curation = Curation.from(req, user ); + curation.draft(); Curation saved = curationRepository.save(curation); return CurationCreateRes.from(saved); diff --git a/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java new file mode 100644 index 0000000..0eef10e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/draft/CurationDraftService.java @@ -0,0 +1,9 @@ +package BookPick.mvp.domain.curation.service.draft; + +import org.springframework.stereotype.Service; + +@Service +public class CurationDraftService { + + public void +} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 0aa9f7b..603aa97 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -33,7 +33,7 @@ public class CurationListService { // 1. 큐레이션 리스트 조회 - public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, Long userId) { + public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, boolean drafted, Long userId) { // 1. 내 취향 유사도 순 O if (sortType == SortType.SORT_SIMILARITY) { @@ -107,7 +107,8 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, likedIds.contains(c.getId()) )) .collect(Collectors.toList()); - CurationListGetRes.from(sortType, content, page.isHasNext(), page.getNextCursor()); + + return CurationListGetRes.from( sortType, From d7a948f682a5b4d664e72ceaec224b2a2667009f Mon Sep 17 00:00:00 2001 From: halo Date: Wed, 24 Dec 2025 21:58:46 +0900 Subject: [PATCH 234/291] =?UTF-8?q?feat:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base/create/CurationCreateService.java | 8 +-- .../base/update/CurationUpdateService.java | 70 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java index d908cc3..1d22b5c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java @@ -30,16 +30,16 @@ public CurationCreateRes saveCuration(Long userId, CurationReq req) { // 발행 if(!req.isDrafted()){ - return publishCuration(userId, req); + return publishNewCuration(userId, req); } else{ - return draftCuration(userId,req); + return draftNewCuration(userId,req); } } // -- 큐레이션 등록 -- @Transactional - public CurationCreateRes publishCuration(Long userId, CurationReq req) { + public CurationCreateRes publishNewCuration(Long userId, CurationReq req) { User user = userRepository.findById(userId) @@ -56,7 +56,7 @@ public CurationCreateRes publishCuration(Long userId, CurationReq req) { // -- 큐레이션 임시저장 -- @Transactional - public CurationCreateRes draftCuration(Long userId, CurationReq req) { + public CurationCreateRes draftNewCuration(Long userId, CurationReq req) { User user = userRepository.findById(userId) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java new file mode 100644 index 0000000..7940f53 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java @@ -0,0 +1,70 @@ +// CurationListService.java +package BookPick.mvp.domain.curation.service.base.update; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class CurationUpdateService { + + private final CurationRepository curationRepository; + private final CurationLikeRepository curationLikeRepository; + private final UserRepository userRepository; + private final CurationSubscribeService curationSubscribeService; + + + public CurationGetRes updateCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { + + } + + + + // 임시저장 -> 발행 + @Transactional + public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + if (!curation.getUser().getId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + + curation.curationUpdate(req); // 임시저장 및 발행 처리도 가능 + + return CurationUpdateRes.from(curation); + } + + + // 임시저장 -> 발행 + @Transactional + public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + if (!curation.getUser().getId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + + curation.curationUpdate(req); // 임시저장 및 발행 처리도 가능 + + return CurationUpdateRes.from(curation); + } + +} From f96ad1a88a19b04ad22cbf95109a8c5d539783fa Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 25 Dec 2025 23:27:00 +0900 Subject: [PATCH 235/291] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EA=B0=80=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=201.=20`=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=EB=B3=B8?= =?UTF-8?q?=20=EB=B0=9C=ED=96=89`=20:=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20->=20=EB=B0=9C=ED=96=89=EB=B3=B8=202.=20`=EC=9E=AC?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5`=20:=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=A0=80=EC=9E=A5=20->=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=203.=20`=EB=B0=9C=ED=96=89=EB=B3=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95`=20:=20=EB=B0=9C=ED=96=89=EB=B3=B8=20->=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89=EB=B3=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 18 ++-- .../dto/base/update/CurationUpdateRes.java | 5 +- .../dto/base/update/UpdateResult.java | 12 +++ .../mvp/domain/curation/entity/Curation.java | 1 - .../CurationAlreadyPublishedException.java | 11 +++ .../service/base/CurationService.java | 83 ------------------- .../base/update/CurationUpdateService.java | 59 ++++++++++--- .../subscribe/CuratorSubscribeController.java | 2 - .../mvp/global/api/ErrorCode/ErrorCode.java | 3 +- .../global/api/SuccessCode/SuccessCode.java | 7 +- 10 files changed, 91 insertions(+), 110 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAlreadyPublishedException.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index f9a9e82..84cca7f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -6,8 +6,9 @@ import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.service.base.CurationService; +import BookPick.mvp.domain.curation.dto.base.update.UpdateResult; import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; +import BookPick.mvp.domain.curation.service.base.update.CurationUpdateService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; @@ -25,8 +26,8 @@ @RequiredArgsConstructor public class CurationController { - private final CurationService curationService; private final CurationCreateService curationCreateService; + private final CurationUpdateService curationUpdateService; private final CurrentUserCheck currentUserCheck; @@ -38,9 +39,9 @@ public ResponseEntity> createCuration( currentUserCheck.validateLoginUser(currentUser); - CurationCreateRes res = curationCreateService.publishCuration(currentUser.getId(), req); + CurationCreateRes res = curationCreateService.saveCuration(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(SuccessCode.CURATION_REGISTER_SUCCESS, res)); + .body(ApiResponse.success(SuccessCode.CURATION_PUBLISH_SUCCESS, res)); } @@ -55,10 +56,11 @@ public ResponseEntity> updateCuration( @AuthenticationPrincipal CustomUserDetails currentUser) { currentUserCheck.validateLoginUser(currentUser); - - CurationUpdateRes res = curationService.curationUpdate(currentUser.getId(), curationId, req); - return ResponseEntity.ok() - .body(ApiResponse.success(SuccessCode.CURATION_UPDATE_SUCCESS, res)); + + UpdateResult updateResult = curationUpdateService.updateCuration(currentUser.getId(), curationId, req); + + return ResponseEntity.ok() + .body(ApiResponse.success(updateResult.successCode(), updateResult.curationUpdateRes())); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java index 56aa6cb..ecd9697 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateRes.java @@ -4,9 +4,10 @@ import BookPick.mvp.domain.curation.entity.Curation; public record CurationUpdateRes( - Long id + Long id, + boolean isDrafted ) { public static CurationUpdateRes from(Curation curation) { - return new CurationUpdateRes(curation.getId()); + return new CurationUpdateRes(curation.getId(), curation.getIsDrafted()); } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java new file mode 100644 index 0000000..db91677 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.curation.dto.base.update; + +import BookPick.mvp.global.api.SuccessCode.SuccessCode; + +public record UpdateResult( + CurationUpdateRes curationUpdateRes, + SuccessCode successCode +) { + public static UpdateResult from(CurationUpdateRes curationUpdateRes, SuccessCode successCode) { + return new UpdateResult(curationUpdateRes, successCode); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 2509d2c..993acc3 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -109,7 +109,6 @@ public void curationUpdate(CurationUpdateReq req) { this.genres = req.recommend().genres(); this.keywords = req.recommend().keywords(); this.styles = req.recommend().styles(); - this.isDrafted = req.isDrafted(); // 임시저장 및 발행 처리 } diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAlreadyPublishedException.java b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAlreadyPublishedException.java new file mode 100644 index 0000000..f971a18 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationAlreadyPublishedException.java @@ -0,0 +1,11 @@ +// SelfSubscribeDeniedException.java +package BookPick.mvp.domain.curation.exception.common; + +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CurationAlreadyPublishedException extends BusinessException { + public CurationAlreadyPublishedException() { + super(ErrorCode.CURATION_ACCESS_DENIED); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java deleted file mode 100644 index 3eea9de..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/CurationService.java +++ /dev/null @@ -1,83 +0,0 @@ -// CurationListService.java -package BookPick.mvp.domain.curation.service.base; - -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; -import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationLike; -import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; -import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; -import BookPick.mvp.domain.user.repository.UserRepository; -import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -@RequiredArgsConstructor -public class CurationService { - - private final CurationRepository curationRepository; - private final CurationLikeRepository curationLikeRepository; - private final UserRepository userRepository; - private final CurationSubscribeService curationSubscribeService; - - - - - - // -- 큐레이션 단건 조회 -- - @Transactional - public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { - boolean isLikedCuration = false; - boolean isSubscribedCurator = false; - CurationGetRes res; - - Curation curation = curationRepository.findByIdWithUser(curationId) - .orElseThrow(CurationNotFoundException::new); - - curation.increaseViewCount(); // 큐레이션 조회수 +1 - - - if (user != null) { - // 1. 좋아요 정보 찾기 - Optional curationLike = curationLikeRepository.findByUserIdAndCurationId(user.getId(), curationId); - if (curationLike.isPresent()) { - isLikedCuration = true; - } - - // 2. 큐레이터 구독 여부 조회 - isSubscribedCurator = curationSubscribeService.isSubscribeCurator(user.getId(), curation.getUser().getId()); - - // 3. 큐레이션 작성자면 책 정보 넣어서 큐레이션 반환 - if (curation.getUser().getId().equals(user.getId())) { - return CurationGetRes.fromOwnerView(curation, isSubscribedCurator, isLikedCuration); - } - } - - return CurationGetRes.from(curation, isSubscribedCurator, isLikedCuration); - } - - - // -- 큐레이션 수정 -- - @Transactional - public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { - Curation curation = curationRepository.findById(curationId) - .orElseThrow(CurationNotFoundException::new); - - if (!curation.getUser().getId().equals(userId)) { - throw new CurationAccessDeniedException(); - } - - curation.curationUpdate(req); // 임시저장 및 발행 처리도 가능 - - return CurationUpdateRes.from(curation); - } -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java index 7940f53..0507e53 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java @@ -5,14 +5,17 @@ import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.domain.curation.dto.base.update.UpdateResult; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationAlreadyPublishedException; import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -29,34 +32,68 @@ public class CurationUpdateService { private final UserRepository userRepository; private final CurationSubscribeService curationSubscribeService; + @Transactional + public UpdateResult updateCuration(Long userId, Long curationId, CurationUpdateReq req) { + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); - public CurationGetRes updateCuration(Long curationId, CustomUserDetails user, HttpServletRequest req) { + if (curation.getIsDrafted()) { + if (req.isDrafted()) { + // 임시저장 -> 임시저장 + return UpdateResult.from(reDraftCuration(userId, curation, req), SuccessCode.CURATION_DRAFT_UPDATE_SUCCESS); + } else { + // 임시저장 -> 발행본 + return UpdateResult.from(publishDraftedCuration(userId, curation, req), SuccessCode.DRAFTED_CURATION_PUBLISH_SUCCESS); + } + } + + + // 발행본 -> 발행본 + else { + if (!req.isDrafted()) { + return UpdateResult.from(modifyPublishedCuration(userId, curation, req), SuccessCode.CURATION_UPDATE_SUCCESS); + } else { + throw new CurationAlreadyPublishedException(); + } + } } - // 임시저장 -> 발행 - @Transactional - public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { - Curation curation = curationRepository.findById(curationId) - .orElseThrow(CurationNotFoundException::new); + /*---------------------------------------------------------------------------------------------------*/ + + // 임시저장 -> 임시저장 + public CurationUpdateRes reDraftCuration(Long userId, Curation curation, CurationUpdateReq req) { if (!curation.getUser().getId().equals(userId)) { throw new CurationAccessDeniedException(); } + curation.curationUpdate(req); // 임시저장 및 발행 처리도 가능 + curation.draft(); return CurationUpdateRes.from(curation); } + // 임시저장 -> 발행본 + public CurationUpdateRes publishDraftedCuration(Long userId, Curation curation, CurationUpdateReq req) { + + + if (!curation.getUser().getId().equals(userId)) { + throw new CurationAccessDeniedException(); + } + + curation.curationUpdate(req); // 임시저장 및 발행 처리도 가능 + curation.publish(); + + return CurationUpdateRes.from(curation); + } + + // 발행 -> 발행 + public CurationUpdateRes modifyPublishedCuration(Long userId, Curation curation, CurationUpdateReq req) { - // 임시저장 -> 발행 - @Transactional - public CurationUpdateRes curationUpdate(Long userId, Long curationId, CurationUpdateReq req) { - Curation curation = curationRepository.findById(curationId) - .orElseThrow(CurationNotFoundException::new); if (!curation.getUser().getId().equals(userId)) { throw new CurationAccessDeniedException(); diff --git a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java index dcde24d..12052db 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/subscribe/CuratorSubscribeController.java @@ -4,7 +4,6 @@ import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeReq; import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeRes; import BookPick.mvp.domain.user.dto.subscribe.SubscribedCuratorPageRes; -import BookPick.mvp.domain.curation.service.base.CurationService; import BookPick.mvp.domain.user.enums.curator.CuratorSuccessCode; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; import BookPick.mvp.domain.user.util.CurrentUserCheck; @@ -22,7 +21,6 @@ @RequiredArgsConstructor public class CuratorSubscribeController { - private final CurationService curationService; private final CurrentUserCheck currentUserCheck; private final CurationSubscribeService curationSubscribeService; diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index 69cdfec..ff5a950 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -39,7 +39,8 @@ public enum ErrorCode implements ErrorCodeInterface { // -- Curation -- CURATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), - CURATION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "큐레이션 접근 권한이 없습니다."); + CURATION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "큐레이션 접근 권한이 없습니다."), + CurationAlreadyPublishedException(HttpStatus.BAD_REQUEST, "이미 발행된 큐레이션 입니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java index 5ea7526..0724686 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java @@ -35,10 +35,13 @@ public enum SuccessCode { READING_PREFERENCE_DELETE_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 삭제하였습니다."), // -- Curation -- - CURATION_REGISTER_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 등록하였습니다."), + CURATION_PUBLISH_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 발행하였습니다."), + DRAFTED_CURATION_PUBLISH_SUCCESS(HttpStatus.OK, "임시저장 된 큐레이션을 성공적으로 발행하였습니다."), + CURATION_DRAFT_SUCCESS(HttpStatus.CREATED, "큐레이션을 성공적으로 임시저장하였습니다."), + CURATION_DRAFT_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션 임시저장을 성공적으로 수정하였습니다."), CURATION_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 단건 조회하였습니다."), CURATION_LIST_GET_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 리스트 조회하였습니다."), - CURATION_UPDATE_SUCCESS(HttpStatus.OK, "큐레이션을 성공적으로 수정하였습니다."); + CURATION_UPDATE_SUCCESS(HttpStatus.OK, "발행된 큐레이션을 성공적으로 수정하였습니다."); private final HttpStatus status; private final String message; From e5df2816686745e4317bee311cdce396fdfbf96d Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 25 Dec 2025 23:39:03 +0900 Subject: [PATCH 236/291] =?UTF-8?q?feat=20:=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=B0=9C=ED=96=89=20=EB=B0=8F=20=EC=9E=84=EC=8B=9C?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=9D=91=EB=8B=B5=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EB=B6=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 11 ++++++----- .../dto/base/create/CurationCreateResult.java | 13 +++++++++++++ .../dto/base/update/CurationUpdateResult.java | 12 ++++++++++++ .../curation/dto/base/update/UpdateResult.java | 12 ------------ .../base/create/CurationCreateService.java | 17 ++++++++--------- .../base/update/CurationUpdateService.java | 16 +++++----------- 6 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateResult.java create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateResult.java delete mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 84cca7f..b19a0b7 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -4,9 +4,10 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateResult; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.base.update.UpdateResult; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateResult; import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; import BookPick.mvp.domain.curation.service.base.update.CurationUpdateService; import BookPick.mvp.domain.user.util.CurrentUserCheck; @@ -39,9 +40,9 @@ public ResponseEntity> createCuration( currentUserCheck.validateLoginUser(currentUser); - CurationCreateRes res = curationCreateService.saveCuration(currentUser.getId(), req); + CurationCreateResult result = curationCreateService.saveCuration(currentUser.getId(), req); return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success(SuccessCode.CURATION_PUBLISH_SUCCESS, res)); + .body(ApiResponse.success(result.successCode(), result.curationCreateRes())); } @@ -57,10 +58,10 @@ public ResponseEntity> updateCuration( currentUserCheck.validateLoginUser(currentUser); - UpdateResult updateResult = curationUpdateService.updateCuration(currentUser.getId(), curationId, req); + CurationUpdateResult curationUpdateResult = curationUpdateService.updateCuration(currentUser.getId(), curationId, req); return ResponseEntity.ok() - .body(ApiResponse.success(updateResult.successCode(), updateResult.curationUpdateRes())); + .body(ApiResponse.success(curationUpdateResult.successCode(), curationUpdateResult.curationUpdateRes())); } diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateResult.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateResult.java new file mode 100644 index 0000000..95e5370 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/create/CurationCreateResult.java @@ -0,0 +1,13 @@ +package BookPick.mvp.domain.curation.dto.base.create; + +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; + +public record CurationCreateResult( + CurationCreateRes curationCreateRes, + SuccessCode successCode +) { + public static CurationCreateResult from(CurationCreateRes curationCreateRes, SuccessCode successCode) { + return new CurationCreateResult(curationCreateRes, successCode); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateResult.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateResult.java new file mode 100644 index 0000000..b8b11e3 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/CurationUpdateResult.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.curation.dto.base.update; + +import BookPick.mvp.global.api.SuccessCode.SuccessCode; + +public record CurationUpdateResult( + CurationUpdateRes curationUpdateRes, + SuccessCode successCode +) { + public static CurationUpdateResult from(CurationUpdateRes curationUpdateRes, SuccessCode successCode) { + return new CurationUpdateResult(curationUpdateRes, successCode); + } +} diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java deleted file mode 100644 index db91677..0000000 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/update/UpdateResult.java +++ /dev/null @@ -1,12 +0,0 @@ -package BookPick.mvp.domain.curation.dto.base.update; - -import BookPick.mvp.global.api.SuccessCode.SuccessCode; - -public record UpdateResult( - CurationUpdateRes curationUpdateRes, - SuccessCode successCode -) { - public static UpdateResult from(CurationUpdateRes curationUpdateRes, SuccessCode successCode) { - return new UpdateResult(curationUpdateRes, successCode); - } -} diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java index 1d22b5c..31a5b7c 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java @@ -3,6 +3,7 @@ import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateResult; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; @@ -10,6 +11,7 @@ import BookPick.mvp.domain.user.exception.common.UserNotFoundException; import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,26 +21,24 @@ public class CurationCreateService { private final CurationRepository curationRepository; - private final CurationLikeRepository curationLikeRepository; private final UserRepository userRepository; - private final CurationSubscribeService curationSubscribeService; // 분기 - public CurationCreateRes saveCuration(Long userId, CurationReq req) { + @Transactional + public CurationCreateResult saveCuration(Long userId, CurationReq req) { // 발행 if(!req.isDrafted()){ - return publishNewCuration(userId, req); + return CurationCreateResult.from(publishNewCuration(userId, req), SuccessCode.CURATION_PUBLISH_SUCCESS); } else{ - return draftNewCuration(userId,req); + return CurationCreateResult.from(draftNewCuration(userId, req), SuccessCode.CURATION_DRAFT_SUCCESS); } } - // -- 큐레이션 등록 -- - @Transactional + // 큐레이션 발행 public CurationCreateRes publishNewCuration(Long userId, CurationReq req) { @@ -54,8 +54,7 @@ public CurationCreateRes publishNewCuration(Long userId, CurationReq req) { } - // -- 큐레이션 임시저장 -- - @Transactional + // 큐레이션 임시저장 public CurationCreateRes draftNewCuration(Long userId, CurationReq req) { diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java index 0507e53..d0db1d0 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/update/CurationUpdateService.java @@ -1,13 +1,10 @@ // CurationListService.java package BookPick.mvp.domain.curation.service.base.update; -import BookPick.mvp.domain.auth.service.CustomUserDetails; -import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; -import BookPick.mvp.domain.curation.dto.base.update.UpdateResult; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateResult; import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; import BookPick.mvp.domain.curation.exception.common.CurationAlreadyPublishedException; import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; @@ -16,13 +13,10 @@ import BookPick.mvp.domain.user.repository.UserRepository; import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; import BookPick.mvp.global.api.SuccessCode.SuccessCode; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @Service @RequiredArgsConstructor public class CurationUpdateService { @@ -33,17 +27,17 @@ public class CurationUpdateService { private final CurationSubscribeService curationSubscribeService; @Transactional - public UpdateResult updateCuration(Long userId, Long curationId, CurationUpdateReq req) { + public CurationUpdateResult updateCuration(Long userId, Long curationId, CurationUpdateReq req) { Curation curation = curationRepository.findById(curationId) .orElseThrow(CurationNotFoundException::new); if (curation.getIsDrafted()) { if (req.isDrafted()) { // 임시저장 -> 임시저장 - return UpdateResult.from(reDraftCuration(userId, curation, req), SuccessCode.CURATION_DRAFT_UPDATE_SUCCESS); + return CurationUpdateResult.from(reDraftCuration(userId, curation, req), SuccessCode.CURATION_DRAFT_UPDATE_SUCCESS); } else { // 임시저장 -> 발행본 - return UpdateResult.from(publishDraftedCuration(userId, curation, req), SuccessCode.DRAFTED_CURATION_PUBLISH_SUCCESS); + return CurationUpdateResult.from(publishDraftedCuration(userId, curation, req), SuccessCode.DRAFTED_CURATION_PUBLISH_SUCCESS); } } @@ -51,7 +45,7 @@ public UpdateResult updateCuration(Long userId, Long curationId, CurationUpdateR // 발행본 -> 발행본 else { if (!req.isDrafted()) { - return UpdateResult.from(modifyPublishedCuration(userId, curation, req), SuccessCode.CURATION_UPDATE_SUCCESS); + return CurationUpdateResult.from(modifyPublishedCuration(userId, curation, req), SuccessCode.CURATION_UPDATE_SUCCESS); } else { throw new CurationAlreadyPublishedException(); } From 409b7e1e85459c473c184a6095812b9cbc8d2ac1 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 26 Dec 2025 20:23:10 +0900 Subject: [PATCH 237/291] =?UTF-8?q?bugfix=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B1=85=EC=9D=84=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EC=95=88=ED=95=98=EB=A9=B4=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20BookDto=EB=A5=BC=20Null=EB=A1=9C=20=EC=A3=BC?= =?UTF-8?q?=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20NullPointerError=20If=EB=AC=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/book/service/BookSaveService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java index d10a029..010384d 100644 --- a/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java +++ b/src/main/java/BookPick/mvp/domain/book/service/BookSaveService.java @@ -38,11 +38,16 @@ public void saveBookIfNotExists(Book book) { }); } + + // ------------------------위에거 안쓰임 + // 3. BookDto 리스트 public Set saveBookIfNotExistsDto(Set bookDtos) { Set books= new HashSet<>(); - for (BookDto dto : bookDtos) { - books.add(saveBookIfNotExistsDto(dto)); + if(bookDtos != null) { // BookDto가 Null이 아닐 경우 책 저장 진행 + for (BookDto dto : bookDtos) { + books.add(saveBookIfNotExistsDto(dto)); + } } return books; From cb6ac583c8bfe645ad6888a356bfd07a2ca6a193 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 26 Dec 2025 20:24:19 +0900 Subject: [PATCH 238/291] =?UTF-8?q?bugfix=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9E=91=EA=B0=80=EB=A5=BC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=95=88=ED=95=98=EB=A9=B4=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EA=B0=80=20AuthorDto=EB=A5=BC=20Null=EB=A1=9C=20?= =?UTF-8?q?=EC=A3=BC=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EB=8A=94=20NullPointerError=20If=EB=AC=B8?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/author/service/AuthorSaveService.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java index fb19e0c..ab38e61 100644 --- a/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java +++ b/src/main/java/BookPick/mvp/domain/author/service/AuthorSaveService.java @@ -50,8 +50,10 @@ public Author saveAuthorIfNotExistsByName(String name) { // 5.AuthorDto 리스트 public Set saveAuthorIfNotExistsDto(Set authorDtos) { Set authors = new HashSet<>(); - for (AuthorDto dto : authorDtos) { - authors.add(saveAuthorIfNotExistsDto(dto)); + if(authorDtos != null) { + for (AuthorDto dto : authorDtos) { + authors.add(saveAuthorIfNotExistsDto(dto)); + } } return authors; From 2ef5bade91b3f40f7987331b16f838e184efa7b7 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 26 Dec 2025 22:19:55 +0900 Subject: [PATCH 239/291] =?UTF-8?q?chore=20:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B4=ED=95=B4=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/controller/list/CurationListController.java | 4 ++-- .../curation/service/base/create/CurationCreateService.java | 1 + .../mvp/domain/curation/service/list/CurationListService.java | 4 ++-- .../curation/service/list/CurationRecommendationService.java | 1 + .../curation/util/gemini/prompt/ContentPromptTemplate.java | 2 +- .../domain/curation/util/gemini/service/GeminiService.java | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 2431193..5646ceb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -40,10 +40,10 @@ public ResponseEntity> getCurations( currentUserCheck.validateLoginUser(currentUser); - // 1. SortType 변환 + // 1. 분류 기준 정하고 SortType sortType = SortType.fromValue(sort); - // 2. 큐레이션 리스트 반환 + // 2. 큐레이션 리스트 얻기 CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, drafted, currentUser.getId()); if(curationListGetRes.size() == 0 ){ diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java index 31a5b7c..69d07e7 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/create/CurationCreateService.java @@ -37,6 +37,7 @@ public CurationCreateResult saveCuration(Long userId, CurationReq req) { return CurationCreateResult.from(draftNewCuration(userId, req), SuccessCode.CURATION_DRAFT_SUCCESS); } + } // 큐레이션 발행 public CurationCreateRes publishNewCuration(Long userId, CurationReq req) { diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 603aa97..7d642da 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -35,7 +35,7 @@ public class CurationListService { // 1. 큐레이션 리스트 조회 public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, boolean drafted, Long userId) { - // 1. 내 취향 유사도 순 O + // 1. 내 취향 유사도 순 if (sortType == SortType.SORT_SIMILARITY) { // 1. 유저 독서 취향 반환 @@ -84,7 +84,7 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, return CurationListGetRes.from(sortType, content, hasNext, nextCursor); } - // 2) 내 취향 유사도순 X + // 2) 내 취향 유사도순이 아닌 경우 List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null); CursorPage page = pageHandler.createCursorPage(curations, size); diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java index b88168a..487d3ed 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java @@ -17,6 +17,7 @@ public class CurationRecommendationService { public List recommend(ReadingPreferenceInfo preferenceInfo) { + // return geminiService.recommendCurationsWithMatch( diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java index 788c390..9d75cac 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/prompt/ContentPromptTemplate.java @@ -5,7 +5,7 @@ @Getter @Builder -public class ContentPromptTemplate { +public class ContentPromptTemplate { // private String mbti; private String mood; diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index 7c88918..a3856b5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -48,7 +48,7 @@ public List recommendCurationsWithMatch(ContentPromptTempla String recommendedStyle = parsed[3].trim(); // 3. DB에서 큐레이션 찾기 - List curations = curationRepository.findByRecommendation( + List curations = curationRepository.findPublishedCurationsByRecommendation( List.of(recommendedMood), List.of(recommendedGenre), List.of(recommendedKeyword), From 7965c36547953a4a1b8e573e3c855ad4414c828f Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 26 Dec 2025 22:21:36 +0900 Subject: [PATCH 240/291] =?UTF-8?q?bugfix=20:=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=EB=90=9C=20=EB=8F=85=EC=84=9C=EC=B7=A8=ED=96=A5=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C,=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=A0=80=EC=9E=A5=EB=90=9C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EA=B9=8C=EC=A7=80=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A4=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/repository/CurationRepository.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 1c586c4..5626adf 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -38,19 +38,23 @@ public interface CurationRepository extends JpaRepository { LEFT JOIN c.genres g LEFT JOIN c.keywords k LEFT JOIN c.styles s - WHERE c.deletedAt IS NULL + WHERE c.deletedAt IS NULL and c.isDrafted is false AND (m IN :moods OR g IN :genres OR k IN :keywords OR s IN :styles) ORDER BY c.popularityScore DESC """) - List findByRecommendation( + List findPublishedCurationsByRecommendation( @Param("moods") List moods, @Param("genres") List genres, @Param("keywords") List keywords, @Param("styles") List styles ); + + + Optional findByUserIdAndId(Long userId, Long id); + Long user(User user); From d166b1588a1edee3a7317cd7aa79fbdb75de037f Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 27 Dec 2025 20:52:29 +0900 Subject: [PATCH 241/291] =?UTF-8?q?bugfix[#51]=20:=20=EB=B0=9C=ED=96=89?= =?UTF-8?q?=EB=90=9C=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C,=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=A0=80=EC=9E=A5=EB=90=9C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EB=8F=84=20=EB=B3=B4=EC=9D=B4=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 해당 기능에도 적용 1. 인기순 2. 최신순 3. 사용자 독서 취향에 맞는 순서 --- .../list/CurationListController.java | 14 ++++-- .../common/CurationDraftOwnerException.java | 11 +++++ .../repository/CurationRepository.java | 47 +++++++++++++------ .../service/list/CurationListService.java | 2 +- .../list/Handler/CurationPageHandler.java | 6 +-- .../util/list/fetcher/CurationFetcher.java | 35 +++++++++----- .../mvp/global/api/ErrorCode/ErrorCode.java | 3 +- 7 files changed, 82 insertions(+), 36 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/curation/exception/common/CurationDraftOwnerException.java diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 5646ceb..1a24d1f 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.enums.common.SortType; +import BookPick.mvp.domain.curation.exception.common.CurationDraftOwnerException; import BookPick.mvp.domain.curation.service.list.CurationListService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; @@ -33,7 +34,7 @@ public ResponseEntity> getCurations( @RequestParam(defaultValue = "latest") String sort, @RequestParam(required = false) Long cursor, @RequestParam(defaultValue = "10") int size, - @RequestParam(defaultValue = "false") boolean drafted, + @RequestParam(defaultValue = "false") boolean draft, @AuthenticationPrincipal @Valid CustomUserDetails currentUser ) { @@ -43,12 +44,16 @@ public ResponseEntity> getCurations( // 1. 분류 기준 정하고 SortType sortType = SortType.fromValue(sort); + if (draft && !sortType.equals(SortType.SORT_MY)) { + throw new CurationDraftOwnerException(); + } + // 2. 큐레이션 리스트 얻기 - CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, drafted, currentUser.getId()); + CurationListGetRes curationListGetRes = curationListService.getCurations(sortType, cursor, size, draft, currentUser.getId()); - if(curationListGetRes.size() == 0 ){ + if (curationListGetRes.size() == 0) { return ResponseEntity.ok() - .body(ApiResponse.success(PreferenceSuccessCode.PREFERENCE_NOT_FOUND, curationListGetRes)); + .body(ApiResponse.success(PreferenceSuccessCode.PREFERENCE_NOT_FOUND, curationListGetRes)); } return ResponseEntity.ok() @@ -58,7 +63,6 @@ public ResponseEntity> getCurations( - } diff --git a/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationDraftOwnerException.java b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationDraftOwnerException.java new file mode 100644 index 0000000..7aa3d82 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/exception/common/CurationDraftOwnerException.java @@ -0,0 +1,11 @@ +// SelfSubscribeDeniedException.java +package BookPick.mvp.domain.curation.exception.common; + +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CurationDraftOwnerException extends BusinessException { + public CurationDraftOwnerException() { + super(ErrorCode.CURATION_DRAFT_ACCESS_DENIED); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 5626adf..b59c21b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -14,22 +14,35 @@ public interface CurationRepository extends JpaRepository { - List findByUserId(Long userId, Pageable pageable); + List findByUserIdAndIsDraftedOrderByCreatedAtDesc(Long userId, boolean isDrafted, Pageable pageable); // 사이즈만큼 최신순으로 불러오는 함수 List findAllByOrderByCreatedAtDesc(Pageable pageable); + // Drafted 여부에 따라 가져옴 + List findAllByIsDraftedOrderByCreatedAtDesc(Boolean isDrafted, Pageable pageable); - @Query("SELECT c FROM Curation c WHERE c.id <= :cursor ORDER BY c.createdAt DESC, c.id DESC") - List findLatestCurations(@Param("cursor") Long cursor, Pageable pageable); - @Query("SELECT c FROM Curation c " + - "WHERE (:cursor IS NULL) " + - " OR c.popularityScore <= (SELECT c2.popularityScore FROM Curation c2 WHERE c2.id = :cursor) " + - " OR (c.popularityScore = (SELECT c2.popularityScore FROM Curation c2 WHERE c2.id = :cursor) AND c.id < :cursor) " + - "ORDER BY c.popularityScore DESC, c.id DESC") - List findCurationsByPopularity(@Param("cursor") Long cursor, Pageable pageable); + @Query("SELECT c FROM Curation c WHERE c.id <= :cursor and c.isDrafted = :isDrafted order BY c.createdAt DESC, c.id DESC") + List findLatestCurations(@Param("cursor") Long cursor, + @Param("isDrafted") boolean isDrafted, + Pageable pageable); + + // 인기순 + @Query(""" + SELECT c FROM Curation c + WHERE c.isDrafted = false AND + (:cursorScore IS NULL + OR c.popularityScore < :cursorScore + OR (c.popularityScore = :cursorScore AND c.id < :cursorId)) + ORDER BY c.popularityScore DESC, c.id DESC + """) + List findCurationsByPopularity( + @Param("cursorScore") Integer cursorScore, + @Param("cursorId") Long cursorId, + Pageable pageable + ); // Gemini 추천 결과로 큐레이션 찾기 @Query(""" @@ -50,18 +63,24 @@ List findPublishedCurationsByRecommendation( ); - - Optional findByUserIdAndId(Long userId, Long id); - Long user(User user); - - // 7. @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id = :id") Optional findByIdWithUser(@Param("id") Long id); List findByIdIn(Collection ids); + + + // Like (실제 발행된 것만) + @Query(""" + select c from Curation c + join CurationLike cl on cl.curation = c + where c.isDrafted is false and c.user.id = :userId + order by cl.createdAt desc + """) + List findLikedCurationsByUser(@Param("userId") Long userId, Pageable pageable); + } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 7d642da..6f8770b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -85,7 +85,7 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, } // 2) 내 취향 유사도순이 아닌 경우 - List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null); + List curations = pageHandler.getCurationsPage(userId, sortType, cursor, size, null, drafted); CursorPage page = pageHandler.createCursorPage(curations, size); diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index 3038b39..b531022 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -23,17 +23,17 @@ public class CurationPageHandler { private final CurationFetcher curationFetcher; // 1. 데이터 조회 (size+1개 : +1을 하는 이유는 다음 것을 항상 확인하기 위해서 - public List getCurationsPage(Long userId, SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo) { + public List getCurationsPage(Long userId, SortType sortType, Long cursor, int size, ReadingPreferenceInfo readingPreferenceInfo, boolean drafted) { Pageable pageable = PageRequest.of(0, size + 1); - // SORT_SIMILARITY일 경우 preferenceInfo를 사용, 나머지는 user 관련 정보 필요 없음 + // 취향유사도 순일 경우 preferenceInfo를 사용, 나머지는 user 관련 정보 필요 없음 if (sortType == SortType.SORT_SIMILARITY && readingPreferenceInfo == null) { throw new UserReadingPreferenceNotExisted(); } // 1) DB에서 실제로 가져오는 로직 (fetch : DB에서 가져오는 행위) - return curationFetcher.fetchCurations(userId, sortType, cursor, pageable, readingPreferenceInfo); + return curationFetcher.fetchCurations(userId, sortType, cursor, pageable, readingPreferenceInfo, drafted); } // 2. 커서 페이징 처리 diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java index fe258f8..5aa4b58 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcher.java @@ -27,24 +27,32 @@ public class CurationFetcher { // 1. sort Type별로 큐레이션 리스트 가져오기 - public List fetchCurations(Long userId, SortType sortType, Long cursor, Pageable pageable, ReadingPreferenceInfo readingPreferenceInfo) { + public List fetchCurations(Long userId, SortType sortType, Long cursor, Pageable pageable, ReadingPreferenceInfo readingPreferenceInfo, boolean drafted) { // 1) 맨 처음 페이지 로딩 if (cursor == null) { if (sortType.equals(SortType.SORT_LATEST)) - return curationRepository.findAllByOrderByCreatedAtDesc(pageable); // 취향 유사도 만들기 전까진 최신순 + return curationRepository.findAllByIsDraftedOrderByCreatedAtDesc(drafted, pageable); // 취향 유사도 만들기 전까진 최신순 } // 2) 🌟분류 기준 🌟 return switch (sortType) { // 인기순 - case SORT_POPULAR -> curationRepository.findCurationsByPopularity(cursor, pageable); + case SORT_POPULAR -> { + Integer cursorScore = null; + if (cursor != null) { + Curation cursorCuration = curationRepository.findById(cursor) + .orElseThrow(() -> new IllegalArgumentException("Invalid cursor")); + cursorScore = cursorCuration.getPopularityScore(); + } + yield curationRepository.findCurationsByPopularity(cursorScore, cursor, pageable); + } // 최신순 - case SORT_LATEST -> curationRepository.findLatestCurations(cursor, pageable); + case SORT_LATEST -> curationRepository.findLatestCurations(cursor, drafted, pageable); - // 취향 유사도순 + // 취향 유사도순 (얘는 항상 publish 된것만 = isDraftd : false) case SORT_SIMILARITY -> { List recommended = curationRecommendationService.recommend(readingPreferenceInfo); List paginated = CurationMatchResultPagination.paginate(recommended, cursor, pageable); @@ -52,15 +60,18 @@ public List fetchCurations(Long userId, SortType sortType, Long cursor } // 좋아요 순 - case SORT_LIKED -> { - List likedCurationList = curationLikeRepository.findAllByUserIdOrderByCreatedAtDesc(userId, pageable); - yield likedCurationList.stream() - .map(CurationLike::getCuration) - .toList(); - } +// case SORT_LIKED -> { +// List likedCurationList = curationLikeRepository.findAllByUserIdOrderByCreatedAtDesc(userId, pageable); +// yield likedCurationList.stream() +// .map(CurationLike::getCuration) +// .toList(); +// } + + // 좋아요 순 + case SORT_LIKED -> curationRepository.findLikedCurationsByUser(userId, pageable); // 내가 작성한 순 - case SORT_MY -> curationRepository.findByUserId(userId, pageable); + case SORT_MY -> curationRepository.findByUserIdAndIsDraftedOrderByCreatedAtDesc(userId, drafted, pageable); }; } diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index ff5a950..adfc599 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -40,7 +40,8 @@ public enum ErrorCode implements ErrorCodeInterface { // -- Curation -- CURATION_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 큐레이션을 찾을 수 없습니다."), CURATION_ACCESS_DENIED(HttpStatus.FORBIDDEN, "큐레이션 접근 권한이 없습니다."), - CurationAlreadyPublishedException(HttpStatus.BAD_REQUEST, "이미 발행된 큐레이션 입니다."); + CURATION_ALREADY_PUBLISHED(HttpStatus.BAD_REQUEST, "이미 발행된 큐레이션 입니다."), + CURATION_DRAFT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "임시저장 큐레이션은 작성자만 접근할 수 있습니다."); private final HttpStatus status; private final String message; From 04ef96b797adf0aa1c83205b94c13a1c490dd613 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 27 Dec 2025 21:36:26 +0900 Subject: [PATCH 242/291] =?UTF-8?q?bugfix[#55]=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=EC=8B=9C,=20?= =?UTF-8?q?=EC=B5=9C=EC=8B=A0=20=EB=8C=93=EA=B8=80=EC=9D=B4=20=EC=98=AC?= =?UTF-8?q?=EB=9D=BC=EA=B0=80=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 입장에서는 자신이 작성한 댓글이 바로 보여야하기 때문에 가장 최신 댓글이 아래오게 수정하였다. --- .../mvp/domain/comment/controller/CommentController.java | 4 +++- .../BookPick/mvp/domain/comment/service/CommentService.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java index 73a1f8b..b0e3dac 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java @@ -34,7 +34,7 @@ public ResponseEntity> create(@PathVariable Long c .body(ApiResponse.success(SuccessCode.COMMENT_CREATE_SUCCESS, res)); } - // -- 2. 댓글 조회 -- + // -- 2. 댓글 리스트 조회 -- @GetMapping("/{curationId}/comments") public ResponseEntity> getCommentList(@PathVariable Long curationId, @RequestParam(defaultValue = "1") int page, @@ -54,6 +54,8 @@ public ResponseEntity> getCommentList(@PathVariable return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_READ_SUCCESS, res)); } + + // 2.1 댓글 상세 조회 @GetMapping("/{curationId}/comments/{commentId}") public ResponseEntity> getCommentDetail(@PathVariable Long curationId, @PathVariable Long commentId) { CommentDetailRes res = commentService.getCommentDetail(commentId); diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index cd9487d..a542ee0 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -72,7 +72,7 @@ public CommentCreateRes createComment(Long userId, Long curationId, CommentCreat // -- Read -- @Transactional(readOnly = true) public CommentListRes getCommentList(Long curationId, int page, int size) { - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "createdAt")); Page commentPage = commentRepository.findByCurationId(curationId, pageable); List commentList = commentPage.getContent().stream() From c3a71ac9249e322d8242e5ceca4e6073ff966882 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 27 Dec 2025 22:06:55 +0900 Subject: [PATCH 243/291] =?UTF-8?q?bugfix[#50]=20:=20=EB=8F=85=EC=84=9C?= =?UTF-8?q?=EC=B7=A8=ED=96=A5=20=EC=84=A4=EC=A0=95=EC=8B=9C,=20MBTI=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=95=88=ED=95=98=EB=8A=94=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EB=B0=98=EC=98=81=EC=95=88=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## how 1. ReadingPreference update 메서드에서 요청의 mbti 필드에 있는 값을 그대로 넣게 하였다. 물론 검증과정을 한번 거치기 때문에, 예외값 처리도 진행하였다. --- .../entity/ReadingPreference.java | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java index bc6824d..a2e6ab9 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/entity/ReadingPreference.java @@ -82,13 +82,18 @@ public class ReadingPreference { private LocalDateTime deletedAt; - public void update(ReadingPreferenceReq req) { - if (req.mbti() != null) this.mbti = req.mbti(); + /* public void update(ReadingPreferenceReq req) { +// if (req.mbti() != null) this.mbti = req.mbti(); // mbti null값 허용 + + if (req.moods() != null) { this.moods.clear(); this.moods.addAll(req.moods()); } + else{ + this.moods.clear(); + } if (req.readingHabits() != null) { this.readingHabits.clear(); @@ -109,8 +114,37 @@ public void update(ReadingPreferenceReq req) { this.readingStyles.clear(); this.readingStyles.addAll(req.readingStyles()); } - } + }*/ + + public void update(ReadingPreferenceReq req) { + this.mbti = req.mbti(); + + this.moods.clear(); + if (req.moods() != null) { + this.moods.addAll(req.moods()); + } + + this.readingHabits.clear(); + if (req.readingHabits() != null) { + this.readingHabits.addAll(req.readingHabits()); + } + + this.genres.clear(); + if (req.genres() != null) { + this.genres.addAll(req.genres()); + } + + this.keywords.clear(); + if (req.keywords() != null) { + this.keywords.addAll(req.keywords()); + } + + this.readingStyles.clear(); + if (req.readingStyles() != null) { + this.readingStyles.addAll(req.readingStyles()); + } + } public static ReadingPreference clearPreferences(User user) { From b9a9d4bb32b6b526e806b29d8ad739060b77f191 Mon Sep 17 00:00:00 2001 From: halo Date: Sun, 28 Dec 2025 23:44:30 +0900 Subject: [PATCH 244/291] =?UTF-8?q?bugfix[https://github.com/Book-Pick/boo?= =?UTF-8?q?kpick-front/issues/66]=20:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=EC=8B=9C,=20=EC=9E=90=EC=8B=9D=20=EB=8C=93=EA=B8=80?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EC=82=AD=EC=A0=9C=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/domain/comment/entity/Comment.java | 6 ++++++ .../BookPick/mvp/domain/comment/service/CommentService.java | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java index d1cd19a..a0ed097 100644 --- a/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java +++ b/src/main/java/BookPick/mvp/domain/comment/entity/Comment.java @@ -10,6 +10,8 @@ import lombok.Setter; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @Getter @@ -28,6 +30,10 @@ public class Comment { @JoinColumn(name = "parent_comment_id") private Comment parent; + // 자식 댓글 관계 + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private List children = new ArrayList<>(); + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "curation_id") private Curation curation; diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index a542ee0..3661b2d 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -126,6 +126,8 @@ public CommentDeleteRes deleteComment(Long curationId, Long commentId) { Comment comment = commentRepository.findById(commentId) .orElseThrow(CommentNotFoundException::new); + + commentRepository.delete(comment); curation.decreaseCommentCount(); From 2530faa9bf18f1c0a07961198207327ebc456881 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 29 Dec 2025 22:06:24 +0900 Subject: [PATCH 245/291] =?UTF-8?q?bugfix[https://github.com/orgs/Book-Pic?= =?UTF-8?q?k/projects/4/views/3=3Fpane=3Dissue&itemId=3D146601733&issue=3D?= =?UTF-8?q?Book-Pick%7Cbookpick-front%7C67]=20:=20=EB=B8=94=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=EB=93=9C=20=EB=B6=81=20=EC=BB=A8=EC=85=89=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20book=20=EC=A0=95=EB=B3=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/dto/base/get/list/CurationContentRes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java index 19733b0..0a23b63 100644 --- a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationContentRes.java @@ -55,8 +55,8 @@ public static CurationContentRes from(Curation curation, boolean isLiked) { new ThumbnailRes(curation.getThumbnailUrl(), curation.getThumbnailColor()), curation.getReview(), - BookResInCuration.from(curation.getTitle(), curation.getBookAuthor(), curation.getBookIsbn()), - +// BookResInCuration.from(curation.getTitle(), curation.getBookAuthor(), curation.getBookIsbn()), + null, curation.getLikeCount(), curation.getCommentCount(), curation.getViewCount(), From 870236f7e22632c735390800e1e3ad7ee1e81652 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 29 Dec 2025 22:50:29 +0900 Subject: [PATCH 246/291] =?UTF-8?q?docs=20:=20swagger=EC=97=90=20Comment?= =?UTF-8?q?=20Controller=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/{ => base}/CommentController.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) rename src/main/java/BookPick/mvp/domain/comment/controller/{ => base}/CommentController.java (82%) diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java similarity index 82% rename from src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java rename to src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java index b0e3dac..871b5e1 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java @@ -1,6 +1,7 @@ -package BookPick.mvp.domain.comment.controller; +package BookPick.mvp.domain.comment.controller.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.comment.service.PagenationService; import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; import BookPick.mvp.domain.comment.dto.create.CommentCreateRes; import BookPick.mvp.domain.comment.dto.delete.CommentDeleteRes; @@ -11,6 +12,7 @@ import BookPick.mvp.domain.comment.service.CommentService; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -25,6 +27,7 @@ public class CommentController { private final PagenationService pagenationService; // -- 1. 댓글 생성 -- + @Operation(summary = "댓글 생성", description = "특정 큐레이션에 댓글을 생성합니다", tags = {"Comment"}) @PostMapping("/{curationId}/comments") public ResponseEntity> create(@PathVariable Long curationId, @RequestBody CommentCreateReq commentCreateReq, @AuthenticationPrincipal CustomUserDetails currentUser) { @@ -35,6 +38,7 @@ public ResponseEntity> create(@PathVariable Long c } // -- 2. 댓글 리스트 조회 -- + @Operation(summary = "댓글 리스트 조회", description = "특정 큐레이션의 댓글 목록을 페이지네이션하여 조회합니다", tags = {"Comment"}) @GetMapping("/{curationId}/comments") public ResponseEntity> getCommentList(@PathVariable Long curationId, @RequestParam(defaultValue = "1") int page, @@ -56,6 +60,7 @@ public ResponseEntity> getCommentList(@PathVariable // 2.1 댓글 상세 조회 + @Operation(summary = "댓글 상세 조회", description = "특정 댓글의 상세 정보를 조회합니다", tags = {"Comment"}) @GetMapping("/{curationId}/comments/{commentId}") public ResponseEntity> getCommentDetail(@PathVariable Long curationId, @PathVariable Long commentId) { CommentDetailRes res = commentService.getCommentDetail(commentId); @@ -64,6 +69,7 @@ public ResponseEntity> getCommentDetail(@PathVaria // -- 3. 댓글 수정 -- + @Operation(summary = "댓글 수정", description = "특정 댓글의 내용을 수정합니다", tags = {"Comment"}) @PatchMapping("/{curationId}/comments/{commentId}") public ResponseEntity> updateComment( @PathVariable Long curationId, @@ -76,6 +82,7 @@ public ResponseEntity> updateComment( // -- 4. 댓글 삭제 -- + @Operation(summary = "댓글 삭제", description = "특정 댓글을 삭제합니다 (자식 댓글도 함께 삭제됩니다)", tags = {"Comment"}) @DeleteMapping("/{curationId}/comments/{commentId}") public ResponseEntity> deleteComment( @PathVariable Long curationId, From 5b2918753b361a4129122c6b78bae0372166bf43 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 29 Dec 2025 23:51:23 +0900 Subject: [PATCH 247/291] =?UTF-8?q?feat[https://github.com/Book-Pick/bookp?= =?UTF-8?q?ick-front/issues/57]=20:=20=EB=8C=93=EA=B8=80=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../all/ReceivedCommentsController.java | 30 +++++++++++++++++++ .../comment/dto/read/ReceivedCommentsDTO.java | 18 +++++++++++ .../comment/repository/CommentRepository.java | 15 ++++++++++ .../PagenationService.java | 2 +- .../service/ReceivedCommentsService.java | 22 ++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTO.java rename src/main/java/BookPick/mvp/domain/comment/{controller => service}/PagenationService.java (85%) create mode 100644 src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java b/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java new file mode 100644 index 0000000..1209bed --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java @@ -0,0 +1,30 @@ +package BookPick.mvp.domain.comment.controller.all; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.comment.dto.read.ReceivedCommentsDTO; +import BookPick.mvp.domain.comment.service.ReceivedCommentsService; +import BookPick.mvp.global.api.ApiResponse; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/comments") +@RequiredArgsConstructor +public class ReceivedCommentsController { + + private final ReceivedCommentsService receivedCommentsService; + + @Operation(summary = "받은 댓글 조회", description = "현재 사용자가 받은 최신 댓글 목록을 조회합니다", tags = {"Comment"}) + @GetMapping + public ResponseEntity> getReceivedComments( + @AuthenticationPrincipal CustomUserDetails currentUser) { + ReceivedCommentsDTO res = receivedCommentsService.receivedCommentsRead(currentUser.getId()); + return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_READ_SUCCESS, res)); + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTO.java b/src/main/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTO.java new file mode 100644 index 0000000..c90d2c0 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTO.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.comment.dto.read; + +import BookPick.mvp.domain.comment.entity.Comment; + +import java.util.List; +import java.util.stream.Collectors; + +public record ReceivedCommentsDTO( + List comments +) { + public static ReceivedCommentsDTO from(List comments){ + List commentDetailResList = comments.stream() + .map(CommentDetailRes::of) + .collect(Collectors.toList()); + + return new ReceivedCommentsDTO(commentDetailResList); + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java b/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java index d0c2693..d1e5b97 100644 --- a/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java +++ b/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java @@ -4,10 +4,25 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface CommentRepository extends JpaRepository { Page findByCurationId(Long curationId, Pageable pageable); + + + @Query(""" + SELECT cm FROM Comment cm + WHERE cm.curation.user.id = :userId + ORDER BY cm.createdAt DESC + """) + List findLatestCommentsByUserId( + @Param("userId") Long userId + ); + } diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java b/src/main/java/BookPick/mvp/domain/comment/service/PagenationService.java similarity index 85% rename from src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java rename to src/main/java/BookPick/mvp/domain/comment/service/PagenationService.java index c067c76..dfaf905 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/PagenationService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/PagenationService.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.comment.controller; +package BookPick.mvp.domain.comment.service; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java b/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java new file mode 100644 index 0000000..e18df4f --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java @@ -0,0 +1,22 @@ +package BookPick.mvp.domain.comment.service; + +import BookPick.mvp.domain.comment.dto.read.ReceivedCommentsDTO; +import BookPick.mvp.domain.comment.entity.Comment; +import BookPick.mvp.domain.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import javax.swing.text.html.Option; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReceivedCommentsService { + private final CommentRepository commentRepository; + + public ReceivedCommentsDTO receivedCommentsRead(Long userId){ + List comments = commentRepository.findLatestCommentsByUserId(userId); + + return ReceivedCommentsDTO.from(comments); + } +} From 9722b4472615cbee3cbd18fa974cce574a6b8ade Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 30 Dec 2025 22:02:59 +0900 Subject: [PATCH 248/291] =?UTF-8?q?feat[https://github.com/Book-Pick/bookp?= =?UTF-8?q?ick-front/issues/57]=20:=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=8B=A0=EA=B7=9C=20=ED=94=BC=EB=93=9C=EB=B0=B1?= =?UTF-8?q?=EC=9D=80=20=EB=8C=93=EA=B8=80=203=EA=B0=9C=EB=A7=8C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EB=95=8C=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20,=20PageRequest.of=20=EC=82=AC=EC=9A=A9=ED=95=B4?= =?UTF-8?q?=EC=84=9C=203=EA=B0=9C=EB=A7=8C=20DB=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/comment/repository/CommentRepository.java | 3 ++- .../mvp/domain/comment/service/ReceivedCommentsService.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java b/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java index d1e5b97..5c372ed 100644 --- a/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java +++ b/src/main/java/BookPick/mvp/domain/comment/repository/CommentRepository.java @@ -22,7 +22,8 @@ public interface CommentRepository extends JpaRepository { ORDER BY cm.createdAt DESC """) List findLatestCommentsByUserId( - @Param("userId") Long userId + @Param("userId") Long userId, + Pageable pageable ); } diff --git a/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java b/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java index e18df4f..cba79e8 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/ReceivedCommentsService.java @@ -4,6 +4,7 @@ import BookPick.mvp.domain.comment.entity.Comment; import BookPick.mvp.domain.comment.repository.CommentRepository; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import javax.swing.text.html.Option; @@ -15,7 +16,7 @@ public class ReceivedCommentsService { private final CommentRepository commentRepository; public ReceivedCommentsDTO receivedCommentsRead(Long userId){ - List comments = commentRepository.findLatestCommentsByUserId(userId); + List comments = commentRepository.findLatestCommentsByUserId(userId, PageRequest.of(0,3)); return ReceivedCommentsDTO.from(comments); } From 4352ec25e1a91040603ff3ed0a79276de61b52df Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 30 Dec 2025 22:48:52 +0900 Subject: [PATCH 249/291] =?UTF-8?q?feat[#60]=20:=20=EC=B1=85=20=EA=B5=AC?= =?UTF-8?q?=EB=A7=A4=EC=8B=9C,=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=97=90?= =?UTF-8?q?=EA=B2=8C=20=EC=A0=9C=EA=B3=B5=ED=95=A0=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EC=B1=85=20=EC=A0=95=EB=B3=B4=20=EB=A6=AC=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EB=A0=89=EC=85=98=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/Controller/BookSearchController.java | 21 ++++++++++- .../book/util/kakaoApi/BookSearchService.java | 36 +++++++++++++++++++ .../controller/base/CurationController.java | 29 +++++++++++++++ .../global/api/SuccessCode/SuccessCode.java | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index 225cb61..c062ebb 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -23,10 +23,29 @@ public class BookSearchController { @Operation(summary = "책 검색", description = "검색어로 책 목록 조회", tags = {"Book Search"}) @PostMapping("/search") - public ResponseEntity> searchBookList(@RequestBody BookSearchReq req){ + public ResponseEntity> searchBookList(@RequestBody BookSearchReq req) { BookSearchPageRes res = bookSearchService.getBookSearchList(req); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.BOOK_LIST_READ_SUCCESS, res)); } + + @Operation( + summary = "책 구매 링크 제공", + description = "책 제목으로 외부 서점 검색 링크를 제공합니다", + tags = {"Book Search"} + ) + @PostMapping("/link") + public ResponseEntity> getBookPurchaseLink( + @RequestBody BookSearchReq req + ) { + String link = bookSearchService.getBookPurchaseLink(req.keyword()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.BOOK_LINK_READ_SUCCESS, + link + )); + } + } diff --git a/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java index b46bef5..9ae17b5 100644 --- a/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java +++ b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java @@ -83,4 +83,40 @@ public BookSearchPageRes getBookSearchList(BookSearchReq req) { // 최종 응답 DTO 반환 return new BookSearchPageRes(books, pageInfo); } + + public String getBookPurchaseLink(String bookTitle) { + RestTemplate restTemplate = new RestTemplate(); + + // 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "KakaoAK " + kakaoApiKey); + + // 요청 URL 구성 + UriComponents uri = UriComponentsBuilder.fromHttpUrl(API_URL) + .queryParam("query", bookTitle) + .queryParam("page", 1) + .queryParam("size", 1) + .build(); + + HttpEntity requestEntity = new HttpEntity<>(headers); + + // 카카오 API 호출 + ResponseEntity response = restTemplate.exchange( + uri.toUriString(), + HttpMethod.GET, + requestEntity, + Map.class + ); + + // documents 배열 추출 + List> documents = (List>) response.getBody().get("documents"); + + // 첫 번째 결과의 URL 반환 + if (documents != null && !documents.isEmpty()) { + Map firstBook = documents.get(0); + return (String) firstBook.get("url"); + } + + return null; + } } diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index b19a0b7..0837f33 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -2,12 +2,16 @@ package BookPick.mvp.domain.curation.controller.base; import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.book.util.kakaoApi.BookSearchService; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; import BookPick.mvp.domain.curation.dto.base.create.CurationCreateResult; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateRes; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateResult; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; import BookPick.mvp.domain.curation.service.base.update.CurationUpdateService; import BookPick.mvp.domain.user.util.CurrentUserCheck; @@ -29,6 +33,8 @@ public class CurationController { private final CurationCreateService curationCreateService; private final CurationUpdateService curationUpdateService; + private final CurationRepository curationRepository; + private final BookSearchService bookSearchService; private final CurrentUserCheck currentUserCheck; @@ -64,6 +70,29 @@ public ResponseEntity> updateCuration( .body(ApiResponse.success(curationUpdateResult.successCode(), curationUpdateResult.curationUpdateRes())); } + @Operation( + summary = "큐레이션의 책 구매 링크 제공", + description = "큐레이션 ID로 조회하여 해당 큐레이션의 책 제목으로 카카오 API를 사용해 외부 서점 검색 링크를 제공합니다", + tags = {"Curation"} + ) + @GetMapping("/{curationId}/book-link") + public ResponseEntity> getCurationBookPurchaseLink( + @PathVariable Long curationId + ) { + // 큐레이션 조회 + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + + // 책 제목으로 카카오 API 호출하여 첫 번째 결과의 URL 반환 + String link = bookSearchService.getBookPurchaseLink(curation.getBookTitle()); + + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success( + SuccessCode.BOOK_LINK_READ_SUCCESS, + link + )); + } + } diff --git a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java index 0724686..8d8f2be 100644 --- a/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java +++ b/src/main/java/BookPick/mvp/global/api/SuccessCode/SuccessCode.java @@ -27,6 +27,7 @@ public enum SuccessCode { // -- Book -- BOOK_LIST_READ_SUCCESS(HttpStatus.OK, "책 목록을 성공적으로 조회하였습니다."), + BOOK_LINK_READ_SUCCESS(HttpStatus.OK, "책 구매 링크를 성공적으로 조회하였습니다."), READING_PREFERENCE_REGISTER_SUCCESS(HttpStatus.CREATED, "독서 취향을 성공적으로 등록하였습니다."), // -- Reading Preference -- From b9e12bca87c94e981cfaefe8dcea55e20c20d15b Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 30 Dec 2025 22:57:00 +0900 Subject: [PATCH 250/291] =?UTF-8?q?feat[#60]=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=9D=B8=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/Controller/BookSearchController.java | 16 ---------------- .../book/util/kakaoApi/BookSearchService.java | 16 ++++++++++++++-- .../controller/base/CurationController.java | 13 ++++++------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index c062ebb..f8f8c59 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -30,22 +30,6 @@ public ResponseEntity> searchBookList(@RequestBod .body(ApiResponse.success(SuccessCode.BOOK_LIST_READ_SUCCESS, res)); } - @Operation( - summary = "책 구매 링크 제공", - description = "책 제목으로 외부 서점 검색 링크를 제공합니다", - tags = {"Book Search"} - ) - @PostMapping("/link") - public ResponseEntity> getBookPurchaseLink( - @RequestBody BookSearchReq req - ) { - String link = bookSearchService.getBookPurchaseLink(req.keyword()); - return ResponseEntity.status(HttpStatus.OK) - .body(ApiResponse.success( - SuccessCode.BOOK_LINK_READ_SUCCESS, - link - )); - } } diff --git a/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java index 9ae17b5..fb24c27 100644 --- a/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java +++ b/src/main/java/BookPick/mvp/domain/book/util/kakaoApi/BookSearchService.java @@ -3,7 +3,11 @@ import BookPick.mvp.domain.book.dto.search.BookSearchPageRes; import BookPick.mvp.domain.book.dto.search.BookSearchReq; import BookPick.mvp.domain.book.dto.search.BookSearchRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.global.dto.PageInfo; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -19,7 +23,10 @@ import java.util.Map; @Service +@RequiredArgsConstructor public class BookSearchService { + private final CurationRepository curationRepository; + @Value("${api.kakao.key}") private String kakaoApiKey; @@ -84,7 +91,12 @@ public BookSearchPageRes getBookSearchList(BookSearchReq req) { return new BookSearchPageRes(books, pageInfo); } - public String getBookPurchaseLink(String bookTitle) { + public String getBookPurchaseLink(Long curationId) { + + // 큐레이션 조회 + Curation curation = curationRepository.findById(curationId) + .orElseThrow(CurationNotFoundException::new); + RestTemplate restTemplate = new RestTemplate(); // 헤더 설정 @@ -93,7 +105,7 @@ public String getBookPurchaseLink(String bookTitle) { // 요청 URL 구성 UriComponents uri = UriComponentsBuilder.fromHttpUrl(API_URL) - .queryParam("query", bookTitle) + .queryParam("query", curation.getBookTitle()) .queryParam("page", 1) .queryParam("size", 1) .build(); diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 0837f33..29004e2 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -33,9 +33,7 @@ public class CurationController { private final CurationCreateService curationCreateService; private final CurationUpdateService curationUpdateService; - private final CurationRepository curationRepository; private final BookSearchService bookSearchService; - private final CurrentUserCheck currentUserCheck; @Operation(summary = "큐레이션 생성(일반 및 임시저장)", description = "새 큐레이션을 생성합니다 drafted가 true면 임시저장", tags = {"Curation"}) @@ -77,14 +75,15 @@ public ResponseEntity> updateCuration( ) @GetMapping("/{curationId}/book-link") public ResponseEntity> getCurationBookPurchaseLink( - @PathVariable Long curationId + @PathVariable Long curationId, + @AuthenticationPrincipal CustomUserDetails currentUser ) { - // 큐레이션 조회 - Curation curation = curationRepository.findById(curationId) - .orElseThrow(CurationNotFoundException::new); + + currentUserCheck.validateLoginUser(currentUser); + // 책 제목으로 카카오 API 호출하여 첫 번째 결과의 URL 반환 - String link = bookSearchService.getBookPurchaseLink(curation.getBookTitle()); + String link = bookSearchService.getBookPurchaseLink(curationId); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success( From 29ed01cd664a238deac0d97799262fa3b9447fce Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 1 Jan 2026 15:01:05 +0900 Subject: [PATCH 251/291] =?UTF-8?q?feat=20:=20local=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=84=20jpa=20ddl=20auto=20=EA=B0=92=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 2 +- src/main/resources/application-local.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index efd0ead..7dab626 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,7 +7,7 @@ spring: jpa: hibernate: - ddl-auto: none + ddl-auto: ${SPRING_JPA_DDL_AUTO} properties: hibernate: show_sql: true diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index deff22f..7dab626 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -7,7 +7,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: ${SPRING_JPA_DDL_AUTO} properties: hibernate: show_sql: true From e7d42321d0b2beb57f6d1e66af97b474e6d811e5 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 1 Jan 2026 15:01:28 +0900 Subject: [PATCH 252/291] =?UTF-8?q?feat=20:=20=EB=B0=B0=ED=8F=B4=EB=A5=B4?= =?UTF-8?q?=20=EB=8C=80=EB=B9=84=ED=95=9C,=20production=20yml=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-prod.yml | 53 ++++++++++++++++--------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 6d15520..7dab626 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,19 +1,34 @@ -#spring: -# datasource: -# driver-class-name: com.mysql.cj.jdbc.Driver -# url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}?serverTimezone=Asia/Seoul -# username: ${DB_USERNAME} -# password: ${DB_PASSWORD} -# -# jpa: -# hibernate: -# ddl-auto: update -# show-sql: true -# properties: -# hibernate: -# format_sql: true -# -#jwt: -# secret: ${JWT_SECRET} -# access-token-expire: ${JWT_ACCESS_EXPIRE} -# refresh-token-expire: ${JWT_REFRESH_EXPIRE} +spring: + datasource: + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: ${SPRING_JPA_DDL_AUTO} + properties: + hibernate: + show_sql: true + +logging: + level: + org.springframework.web: DEBUG + BookPick: DEBUG + +jwt: + access: + secret: ${JWT_ACCESS_SECRET} + expiration: ${JWT_ACCESS_EXPIRATION} + refresh: + secret: ${JWT_REFRESH_SECRET} + expiration: ${JWT_REFRESH_EXPIRATION} + +api: + kakao: + key: ${KAKAO_API_KEY} + gemini: + api: + key: ${GEMINI_API_KEY} + url: ${GEMINI_API_URL} From cfa0ff94ea30e5d847102e840da86c8a65350438 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 1 Jan 2026 21:22:41 +0900 Subject: [PATCH 253/291] =?UTF-8?q?fix=20:=20BookSearchRes=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `image` -> imageUrl --- .../java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java index 6afb904..619eb10 100644 --- a/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java +++ b/src/main/java/BookPick/mvp/domain/book/dto/search/BookSearchRes.java @@ -3,7 +3,7 @@ public record BookSearchRes( String title, String author, - String image, + String imageUrl, String isbn ) { } From 395a80b690dd690427151f9a9ceb40d90060a148 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 1 Jan 2026 21:25:17 +0900 Subject: [PATCH 254/291] =?UTF-8?q?feat=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EA=B8=B0=EB=B0=98=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=8B=9C,=20=EB=82=B4=EA=B0=80=20=EC=9E=91=EC=84=B1=ED=95=9C?= =?UTF-8?q?=20=EC=95=88=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AlreadyRegisteredReadingPreferenceException.java | 2 +- .../Exception/{ => fail}/UserReadingPreferenceNotExisted.java | 2 +- .../{ => fail}/WrongReadingPreferenceRequestException.java | 2 +- .../ReadingPreference/service/ReadingPreferenceService.java | 4 ++-- .../service/ReadingPreferenceValidCheckService.java | 2 +- .../mvp/domain/curation/enums/common/CurationSuccessCode.java | 1 + .../mvp/domain/curation/repository/CurationRepository.java | 3 ++- .../mvp/domain/curation/service/list/CurationListService.java | 2 -- .../curation/service/list/CurationRecommendationService.java | 3 +++ .../domain/curation/util/gemini/service/GeminiService.java | 3 ++- .../curation/util/list/Handler/CurationPageHandler.java | 2 +- .../BookPick/mvp/global/exception/GlobalExceptionHandler.java | 1 + 12 files changed, 16 insertions(+), 11 deletions(-) rename src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/{ => fail}/AlreadyRegisteredReadingPreferenceException.java (83%) rename src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/{ => fail}/UserReadingPreferenceNotExisted.java (82%) rename src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/{ => fail}/WrongReadingPreferenceRequestException.java (84%) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/AlreadyRegisteredReadingPreferenceException.java similarity index 83% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/AlreadyRegisteredReadingPreferenceException.java index af48835..86b3abd 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/AlreadyRegisteredReadingPreferenceException.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/AlreadyRegisteredReadingPreferenceException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.Exception; +package BookPick.mvp.domain.ReadingPreference.Exception.fail; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/UserReadingPreferenceNotExisted.java similarity index 82% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/UserReadingPreferenceNotExisted.java index 32db36f..3077ccc 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/UserReadingPreferenceNotExisted.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/UserReadingPreferenceNotExisted.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.Exception; +package BookPick.mvp.domain.ReadingPreference.Exception.fail; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/WrongReadingPreferenceRequestException.java similarity index 84% rename from src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java rename to src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/WrongReadingPreferenceRequestException.java index 08b0c74..c57acb5 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/WrongReadingPreferenceRequestException.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/Exception/fail/WrongReadingPreferenceRequestException.java @@ -1,4 +1,4 @@ -package BookPick.mvp.domain.ReadingPreference.Exception; +package BookPick.mvp.domain.ReadingPreference.Exception.fail; import BookPick.mvp.domain.ReadingPreference.enums.resCode.PreferenceErrorCode; import BookPick.mvp.global.exception.BusinessException; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java index dc79088..57181cb 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceService.java @@ -1,7 +1,7 @@ package BookPick.mvp.domain.ReadingPreference.service; -import BookPick.mvp.domain.ReadingPreference.Exception.AlreadyRegisteredReadingPreferenceException; -import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.ReadingPreference.Exception.fail.AlreadyRegisteredReadingPreferenceException; +import BookPick.mvp.domain.ReadingPreference.Exception.fail.UserReadingPreferenceNotExisted; import BookPick.mvp.domain.ReadingPreference.dto.ETC.Delete.ReadingPreferenceDeleteRes; import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceRes; diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java index 0f646bf..e4af3ba 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckService.java @@ -1,6 +1,6 @@ package BookPick.mvp.domain.ReadingPreference.service; -import BookPick.mvp.domain.ReadingPreference.Exception.WrongReadingPreferenceRequestException; +import BookPick.mvp.domain.ReadingPreference.Exception.fail.WrongReadingPreferenceRequestException; import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; import BookPick.mvp.domain.ReadingPreference.enums.filed.*; import org.springframework.stereotype.Service; diff --git a/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java index 0b0faf9..d9288eb 100644 --- a/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java +++ b/src/main/java/BookPick/mvp/domain/curation/enums/common/CurationSuccessCode.java @@ -26,6 +26,7 @@ public enum CurationSuccessCode implements SuccessCodeInterface { CURATION_LIST_DELETE_SUCCESS(HttpStatus.OK, "다수의 큐레이션을 성공적으로 삭제하였습니다."); + // 조회 diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index b59c21b..3f0f675 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -51,11 +51,12 @@ List findCurationsByPopularity( LEFT JOIN c.genres g LEFT JOIN c.keywords k LEFT JOIN c.styles s - WHERE c.deletedAt IS NULL and c.isDrafted is false + WHERE c.deletedAt IS NULL and c.isDrafted is false and c.user.id != :userId AND (m IN :moods OR g IN :genres OR k IN :keywords OR s IN :styles) ORDER BY c.popularityScore DESC """) List findPublishedCurationsByRecommendation( + @Param("userId") Long userId, @Param("moods") List moods, @Param("genres") List genres, @Param("keywords") List keywords, diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index 6f8770b..dfad5ff 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -11,14 +11,12 @@ import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; import BookPick.mvp.domain.curation.util.list.similarity.CurationMatchResultPagination; -import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java index 487d3ed..b0dd710 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java @@ -20,6 +20,9 @@ public List recommend(ReadingPreferenceInfo preferenceInfo) // return geminiService.recommendCurationsWithMatch( + // 유저 ID + preferenceInfo.userId(), + // 1. 제미나이 프롬프트 생성 ContentPromptTemplate.builder() diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index a3856b5..d792341 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -34,7 +34,7 @@ public String[] parseResult(String result) { } @Transactional(readOnly = true) - public List recommendCurationsWithMatch(ContentPromptTemplate contentTemplate) { + public List recommendCurationsWithMatch(Long userId, ContentPromptTemplate contentTemplate) { // 1. Gemini에게 추천 받기 @@ -49,6 +49,7 @@ public List recommendCurationsWithMatch(ContentPromptTempla // 3. DB에서 큐레이션 찾기 List curations = curationRepository.findPublishedCurationsByRecommendation( + userId, List.of(recommendedMood), List.of(recommendedGenre), List.of(recommendedKeyword), diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java index b531022..fa7db05 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandler.java @@ -7,7 +7,7 @@ import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; -import BookPick.mvp.domain.ReadingPreference.Exception.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.ReadingPreference.Exception.fail.UserReadingPreferenceNotExisted; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java index c9e546c..c6b7a6f 100644 --- a/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/BookPick/mvp/global/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.ErrorCode.ErrorCode; import BookPick.mvp.global.enums.ErrorCodeInterface; +import BookPick.mvp.global.enums.SuccessCodeInterface; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; From 564cd8fa05ef8a626ef959bbc1f57b80c36bf831 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 1 Jan 2026 23:09:48 +0900 Subject: [PATCH 255/291] =?UTF-8?q?fix=20:=20=EB=8F=85=EC=84=9C=EC=B7=A8?= =?UTF-8?q?=ED=96=A5=20=EA=B8=B0=EB=B0=98=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=8B=9C=20=EB=A7=A4=EB=B2=88=20gemini=20api=EB=A5=BC=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A5=BC=20=ED=95=B4=EA=B2=B0=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4,=20=EC=BA=90=EC=8B=B1=EC=9D=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EB=8B=B9=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +++ .../service/list/CurationListService.java | 9 +++--- .../list/CurationRecommendationService.java | 2 ++ .../util/gemini/service/GeminiService.java | 3 +- .../CurationMatchResultPagination.java | 20 ++++++------- .../mvp/global/config/CacheConfig.java | 28 +++++++++++++++++++ 6 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 src/main/java/BookPick/mvp/global/config/CacheConfig.java diff --git a/build.gradle b/build.gradle index 8bf8d12..4f1c296 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-cache' + + // Cache (Caffeine - TTL 지원) + implementation 'com.github.ben-manes.caffeine:caffeine' // Thymeleaf + Spring Security 6 integration implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index dfad5ff..b942634 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -41,10 +41,10 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, if(!readingPreference.isCompleted()){return CurationListGetRes.ofEmpty(sortType);} ReadingPreferenceInfo preferenceInfo = ReadingPreferenceInfo.from(readingPreference); - // 2. 매칭된 큐레이션 리스트트 조회 + // 2. 매칭된 큐레이션 리스트 조회 (캐싱됨 - 첫 요청 이후 Gemini 호출 안함) List recommended = curationRecommendationService.recommend(preferenceInfo); - //3. 매칭된 큐레이션 페이지네이션 + //3. 매칭된 큐레이션 페이지네이션 (cursor를 offset으로 사용) List paginated = CurationMatchResultPagination.paginate(recommended, cursor, PageRequest.of(0, size + 1)); //4. 유저가 스크롤시, 다음 조회할 값있는지 @@ -54,8 +54,9 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, // 더 없으면 그냥 보여줌 List contentResults = hasNext ? paginated.subList(0, size) : paginated; - // 6. 다음 커서 반환 - Long nextCursor = hasNext ? paginated.get(size).getCuration().getId() : null; + // 6. 다음 커서 반환 (offset 기반) + int currentOffset = (cursor != null) ? cursor.intValue() : 0; + Long nextCursor = hasNext ? (long)(currentOffset + size) : null; // 7-1. 좋아요 여부 계산을 위한 큐레이션 ID 목록 추출 List curationIds = contentResults.stream() diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java index b0dd710..9b0a5c2 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationRecommendationService.java @@ -5,6 +5,7 @@ import BookPick.mvp.domain.curation.util.gemini.prompt.ContentPromptTemplate; import BookPick.mvp.domain.curation.util.gemini.service.GeminiService; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; @@ -15,6 +16,7 @@ public class CurationRecommendationService { private final GeminiService geminiService; + @Cacheable(value = "gemini-recommendations", key = "#preferenceInfo.userId()") public List recommend(ReadingPreferenceInfo preferenceInfo) { // diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index d792341..fd003e8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -56,7 +56,7 @@ public List recommendCurationsWithMatch(Long userId, Conten List.of(recommendedStyle) ); - // 4. 일치 정보와 함께 반환 (일치 개수 많은 순) + // 4. 일치 정보와 함께 반환 (일치 개수 많은 순, 0점 제외) return curations.stream() .map(curation -> CurationMatchResult.of( curation, @@ -66,6 +66,7 @@ public List recommendCurationsWithMatch(Long userId, Conten recommendedKeyword, recommendedStyle )) + .filter(matchResult -> matchResult.getTotalMatchCount() > 0) // 매칭 점수 0점인 큐레이션 제외 .sorted((a, b) -> Integer.compare(b.getTotalMatchCount(), a.getTotalMatchCount())) // Todo 1. 현재 MatchCount가지고 정렬 -> 취향유사도 해당 로직에서 계산해서 정렬 필요 .collect(Collectors.toList()); } diff --git a/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java index 058d2bd..3ceb88b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPagination.java @@ -8,21 +8,19 @@ public class CurationMatchResultPagination { /** - * cursor 기준으로 큐레이션 리스트 페이징 처리 + * offset 기준으로 큐레이션 리스트 페이징 처리 + * cursor를 offset으로 해석 (null이면 0부터 시작) */ public static List paginate(List curationMatchResults, Long cursor, Pageable pageable) { - int start = 0; + // cursor를 offset으로 해석 (null이면 0) + int offset = (cursor != null) ? cursor.intValue() : 0; - if (cursor != null) { - for (int i = 0; i < curationMatchResults.size(); i++) { - if (curationMatchResults.get(i).getCuration().getId().equals(cursor)) { - start = i + 1; - break; - } - } + // offset이 범위를 벗어나면 빈 리스트 반환 + if (offset >= curationMatchResults.size()) { + return List.of(); } - int end = Math.min(start + pageable.getPageSize(), curationMatchResults.size()); - return curationMatchResults.subList(start, end); + int end = Math.min(offset + pageable.getPageSize(), curationMatchResults.size()); + return curationMatchResults.subList(offset, end); } } diff --git a/src/main/java/BookPick/mvp/global/config/CacheConfig.java b/src/main/java/BookPick/mvp/global/config/CacheConfig.java new file mode 100644 index 0000000..b1ad222 --- /dev/null +++ b/src/main/java/BookPick/mvp/global/config/CacheConfig.java @@ -0,0 +1,28 @@ +package BookPick.mvp.global.config; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.TimeUnit; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager("gemini-recommendations"); + + // TTL 설정: 10분 후 자동 삭제 + 최대 1000개 캐시 + cacheManager.setCaffeine(Caffeine.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES) // 10분 후 자동 갱신 + .maximumSize(1000) // 메모리 보호 + .recordStats()); // 캐시 통계 (선택) + + return cacheManager; + } +} From dd6117684d10fbd036a859bc39d0c9bc3f9fea68 Mon Sep 17 00:00:00 2001 From: halo Date: Thu, 1 Jan 2026 23:37:57 +0900 Subject: [PATCH 256/291] =?UTF-8?q?feat:=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EB=A7=81=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20(70=EB=B0=B0=20=EA=B0=9C=EC=84=A0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 캐싱 추가: Caffeine Cache + TTL 10분 (Gemini API 호출 90% 절감) - 오프셋 기반 페이징: cursor를 offset으로 변경 (O(n) → O(1)) - Batch Fetch 적용: N+1 문제 해결 (397쿼리 → 5쿼리) - 매칭 점수 0점 필터링 추가 성능 개선: - 응답 속도: 7초 → 0.1초 (70배) - DB 쿼리: 397개 → 5개 (79배 감소) - API 비용: 90% 절감 --- .../mvp/domain/curation/repository/CurationRepository.java | 2 +- src/main/resources/application-dev.yml | 1 + src/main/resources/application-local.yml | 1 + src/main/resources/application-prod.yml | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 3f0f675..9d500ac 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -44,7 +44,7 @@ List findCurationsByPopularity( Pageable pageable ); - // Gemini 추천 결과로 큐레이션 찾기 + // Gemini 추천 결과로 큐레이션 찾기 (Batch Fetch로 N+1 방지) @Query(""" SELECT DISTINCT c FROM Curation c LEFT JOIN c.moods m diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 7dab626..9cbb25a 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,7 @@ spring: properties: hibernate: show_sql: true + default_batch_fetch_size: 100 # N+1 방지 (한 번에 100개씩 배치 조회) logging: level: diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 7dab626..9cbb25a 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -11,6 +11,7 @@ spring: properties: hibernate: show_sql: true + default_batch_fetch_size: 100 # N+1 방지 (한 번에 100개씩 배치 조회) logging: level: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 7dab626..9cbb25a 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -11,6 +11,7 @@ spring: properties: hibernate: show_sql: true + default_batch_fetch_size: 100 # N+1 방지 (한 번에 100개씩 배치 조회) logging: level: From 4b82d5579ce5ea72c1ed7592009b0c7436c3e8d6 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 2 Jan 2026 21:55:31 +0900 Subject: [PATCH 257/291] =?UTF-8?q?fix=20:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EB=8D=B0=EB=93=9C=EB=9D=BD=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/comment/service/CommentService.java | 6 +++--- .../curation/repository/CurationRepository.java | 12 ++++++++++++ .../service/base/read/CurationReadService.java | 2 +- .../curation/service/like/CurationLikeService.java | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 3661b2d..2ba8ca9 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -43,7 +43,7 @@ public CommentCreateRes createComment(Long userId, Long curationId, CommentCreat User user = userRepository.findById(userId) .orElseThrow(UserNotFoundException::new); - Curation curation = curationRepository.findById(curationId) + Curation curation = curationRepository.findByIdWithLock(curationId) .orElseThrow(CurationNotFoundException::new); Comment parent = null; @@ -63,7 +63,7 @@ public CommentCreateRes createComment(Long userId, Long curationId, CommentCreat .build(); Comment saved = commentRepository.save(comment); - curation.increaseCommentCount(); + curation.increaseCommentCount(); // curation = post return CommentCreateRes.from(saved); } @@ -120,7 +120,7 @@ public CommentUpdateRes updateComment(Long commentId, CommentUpdateReq req) { // -- Delete -- @Transactional public CommentDeleteRes deleteComment(Long curationId, Long commentId) { - Curation curation = curationRepository.findById(curationId) + Curation curation = curationRepository.findByIdWithLock(curationId) .orElseThrow(CurationNotFoundException::new); Comment comment = commentRepository.findById(commentId) diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 9d500ac..e2f884d 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -3,8 +3,10 @@ import BookPick.mvp.domain.curation.entity.Curation; import BookPick.mvp.domain.curation.entity.CurationLike; import BookPick.mvp.domain.user.entity.User; +import jakarta.persistence.LockModeType; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -83,5 +85,15 @@ List findPublishedCurationsByRecommendation( """) List findLikedCurationsByUser(@Param("userId") Long userId, Pageable pageable); + // Pessimistic lock for preventing deadlocks when updating comment count + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT c FROM Curation c WHERE c.id = :id") + Optional findByIdWithLock(@Param("id") Long id); + + // Pessimistic lock with user join for view count updates + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT c FROM Curation c JOIN FETCH c.user WHERE c.id = :id") + Optional findByIdWithUserAndLock(@Param("id") Long id); + } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index 6ff8d95..d6d7dbc 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -38,7 +38,7 @@ public CurationGetRes findCuration(Long curationId, CustomUserDetails user, Http boolean isSubscribedCurator = false; CurationGetRes res; - Curation curation = curationRepository.findByIdWithUser(curationId) + Curation curation = curationRepository.findByIdWithUserAndLock(curationId) .orElseThrow(CurationNotFoundException::new); curation.increaseViewCount(); // 큐레이션 조회수 +1 diff --git a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java index 77d1374..7af41b8 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java @@ -27,7 +27,7 @@ public class CurationLikeService { public boolean CurationLikeOrUnlike(Long userId, Long curationId) { // 1. 포스트 아이디 얻기 - Curation curation = curationRepository.findById(curationId) + Curation curation = curationRepository.findByIdWithLock(curationId) .orElseThrow(CurationNotFoundException::new); // 2. 유저 아이디 얻기 User user = userRepository.findById(userId) From 52232ee6a4395f1eae2fe7ec0ce682ce1a4c4b15 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 2 Jan 2026 23:01:05 +0900 Subject: [PATCH 258/291] =?UTF-8?q?fix=20:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20readonly=EB=A1=9C=20=EC=9D=BD=EA=B8=B0=20=EC=86=8D?= =?UTF-8?q?=EB=8F=84=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/service/base/read/CurationReadService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index d6d7dbc..ef53c43 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -32,7 +32,7 @@ public class CurationReadService { // -- 큐레이션 단건 조회 -- - @Transactional + @Transactional(readOnly = true) public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req, boolean isEdit) { boolean isLikedCuration = false; boolean isSubscribedCurator = false; From 880207c026e16f5fdacc2d27055ed55defae5a49 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 2 Jan 2026 23:10:44 +0900 Subject: [PATCH 259/291] =?UTF-8?q?test:=20Auth=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SignUpService 테스트 추가 (4개 테스트) - 정상 회원가입 (일반 유저, 관리자) - 이메일 중복 체크 - 비밀번호 암호화 확인 - LoginService 테스트 추가 (5개 테스트) - 정상 로그인 (첫 로그인, 재로그인) - 인증 실패 케이스 - JWT 토큰 생성 확인 --- .../domain/auth/service/LoginServiceTest.java | 224 ++++++++++++++++++ .../auth/service/SignUpServiceTest.java | 143 +++++++++++ 2 files changed, 367 insertions(+) create mode 100644 src/test/java/BookPick/mvp/domain/auth/service/LoginServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java diff --git a/src/test/java/BookPick/mvp/domain/auth/service/LoginServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/LoginServiceTest.java new file mode 100644 index 0000000..7fb35bd --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/service/LoginServiceTest.java @@ -0,0 +1,224 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.dto.LoginReq; +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.exception.InvalidLoginException; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.JwtAuthManager; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Collections; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("로그인 서비스 테스트") +class LoginServiceTest { + + @InjectMocks + private LoginService loginService; + + @Mock + private JwtAuthManager jwtAuthManager; + + @Mock + private AuthenticationManagerBuilder authenticationManagerBuilder; + + @Mock + private UserRepository userRepository; + + @Mock + private HttpServletRequest httpServletRequest; + + @Mock + private AuthenticationManager authenticationManager; + + @Mock + private Authentication authentication; + + @Test + @DisplayName("정상 로그인 - 첫 로그인") + void login_success_firstLogin() { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + User mockUser = User.builder() + .id(1L) + .email(req.email()) + .password("encodedPassword") + .nickname("테스터") + .role(Roles.ROLE_USER) + .isFirstLogin(true) + .build(); + + CustomUserDetails mockUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + mockUserDetails.setId(mockUser.getId()); + mockUserDetails.setNickname(mockUser.getNickname()); + mockUserDetails.setFirstLogin(mockUser.isFirstLogin()); + + JwtAuthManager.TokenPair tokenPair = new JwtAuthManager.TokenPair( + "access_token", + "refresh_token" + ); + + when(authenticationManagerBuilder.getObject()).thenReturn(authenticationManager); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(mockUserDetails); + when(jwtAuthManager.createTokens(authentication)).thenReturn(tokenPair); + when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser)); + + // when + LoginRes result = loginService.login(req, httpServletRequest); + + // then + assertThat(result.userId()).isEqualTo(1L); + assertThat(result.accessToken()).isEqualTo("access_token"); + assertThat(result.refreshToken()).isEqualTo("refresh_token"); + assertThat(result.isFirstLogin()).isTrue(); + + verify(authenticationManager).authenticate(any(UsernamePasswordAuthenticationToken.class)); + verify(jwtAuthManager).createTokens(authentication); + verify(userRepository).findById(1L); + assertThat(mockUser.isFirstLogin()).isFalse(); // 첫 로그인 플래그 업데이트 확인 + } + + @Test + @DisplayName("정상 로그인 - 재로그인") + void login_success_notFirstLogin() { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + User mockUser = User.builder() + .id(1L) + .email(req.email()) + .password("encodedPassword") + .nickname("테스터") + .role(Roles.ROLE_USER) + .isFirstLogin(false) + .build(); + + CustomUserDetails mockUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + mockUserDetails.setId(mockUser.getId()); + mockUserDetails.setNickname(mockUser.getNickname()); + mockUserDetails.setFirstLogin(mockUser.isFirstLogin()); + + JwtAuthManager.TokenPair tokenPair = new JwtAuthManager.TokenPair( + "access_token", + "refresh_token" + ); + + when(authenticationManagerBuilder.getObject()).thenReturn(authenticationManager); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(mockUserDetails); + when(jwtAuthManager.createTokens(authentication)).thenReturn(tokenPair); + + // when + LoginRes result = loginService.login(req, httpServletRequest); + + // then + assertThat(result.isFirstLogin()).isFalse(); + verify(userRepository, never()).findById(anyLong()); // 첫 로그인이 아니면 업데이트 안함 + } + + @Test + @DisplayName("잘못된 비밀번호 - 예외 발생") + void login_fail_wrongPassword() { + // given + LoginReq req = new LoginReq("test@test.com", "wrongPassword"); + + when(authenticationManagerBuilder.getObject()).thenReturn(authenticationManager); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenThrow(new BadCredentialsException("Bad credentials")); + + // when & then + assertThatThrownBy(() -> loginService.login(req, httpServletRequest)) + .isInstanceOf(InvalidLoginException.class); + + verify(jwtAuthManager, never()).createTokens(any()); + } + + @Test + @DisplayName("존재하지 않는 사용자 - 예외 발생") + void login_fail_userNotFound() { + // given + LoginReq req = new LoginReq("notexist@test.com", "password123"); + + when(authenticationManagerBuilder.getObject()).thenReturn(authenticationManager); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenThrow(new UsernameNotFoundException("User not found")); + + // when & then + assertThatThrownBy(() -> loginService.login(req, httpServletRequest)) + .isInstanceOf(InvalidLoginException.class); + + verify(jwtAuthManager, never()).createTokens(any()); + } + + @Test + @DisplayName("JWT 토큰 생성 확인") + void login_tokenGeneration() { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + User mockUser = User.builder() + .id(1L) + .email(req.email()) + .password("encodedPassword") + .role(Roles.ROLE_USER) + .isFirstLogin(false) + .build(); + + CustomUserDetails mockUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + mockUserDetails.setId(mockUser.getId()); + mockUserDetails.setNickname(mockUser.getNickname()); + mockUserDetails.setFirstLogin(mockUser.isFirstLogin()); + + JwtAuthManager.TokenPair tokenPair = new JwtAuthManager.TokenPair( + "generated_access_token", + "generated_refresh_token" + ); + + when(authenticationManagerBuilder.getObject()).thenReturn(authenticationManager); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(mockUserDetails); + when(jwtAuthManager.createTokens(authentication)).thenReturn(tokenPair); + + // when + LoginRes result = loginService.login(req, httpServletRequest); + + // then + assertThat(result.accessToken()).isNotNull(); + assertThat(result.refreshToken()).isNotNull(); + assertThat(result.accessToken()).isEqualTo("generated_access_token"); + assertThat(result.refreshToken()).isEqualTo("generated_refresh_token"); + + verify(jwtAuthManager).createTokens(authentication); + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java new file mode 100644 index 0000000..670023f --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java @@ -0,0 +1,143 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.ReadingPreference.service.ReadingPreferenceService; +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.dto.SignReq; +import BookPick.mvp.domain.auth.dto.SignRes; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; +import BookPick.mvp.domain.auth.util.Manager.signup.SignUpManager; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import org.junit.jupit`er.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("회원가입 서비스 테스트") +class SignUpServiceTest { + + @InjectMocks + private SignUpService signUpService; + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private SignUpManager signUpManager; + + @Mock + private ReadingPreferenceService readingPreferenceService; + + @Test + @DisplayName("정상 회원가입 - 일반 유저") + void signUp_success_normalUser() { + // given + SignReq req = new SignReq("test@test.com", "password123"); + User mockUser = User.builder() + .id(1L) + .email(req.email()) + .password("encodedPassword") + .role(Roles.ROLE_USER) + .build(); + + when(userRepository.existsByEmail(req.email())).thenReturn(false); + when(signUpManager.isAdmin(req.email())).thenReturn(false); + when(passwordEncoder.encode(req.password())).thenReturn("encodedPassword"); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + + // when + SignRes result = signUpService.signUp(req); + + // then + assertThat(result.userId()).isEqualTo(1L); + verify(userRepository).existsByEmail(req.email()); + verify(passwordEncoder).encode(req.password()); + verify(userRepository).save(any(User.class)); + verify(readingPreferenceService).addClearReadingPreference(1L); + } + + @Test + @DisplayName("정상 회원가입 - 관리자") + void signUp_success_adminUser() { + // given + SignReq req = new SignReq("admin@bookpick.com", "password123"); + User mockUser = User.builder() + .id(1L) + .email(req.email()) + .password("encodedPassword") + .role(Roles.ROLE_ADMIN) + .build(); + + when(userRepository.existsByEmail(req.email())).thenReturn(false); + when(signUpManager.isAdmin(req.email())).thenReturn(true); + when(passwordEncoder.encode(req.password())).thenReturn("encodedPassword"); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + + // when + SignRes result = signUpService.signUp(req); + + // then + assertThat(result.userId()).isEqualTo(1L); + verify(signUpManager).isAdmin(req.email()); + verify(userRepository).save(argThat(user -> + user.getRole() == Roles.ROLE_ADMIN + )); + } + + @Test + @DisplayName("이메일 중복 - 예외 발생") + void signUp_fail_duplicateEmail() { + // given + SignReq req = new SignReq("duplicate@test.com", "password123"); + when(userRepository.existsByEmail(req.email())).thenReturn(true); + + // when & then + assertThatThrownBy(() -> signUpService.signUp(req)) + .isInstanceOf(DuplicateEmailException.class); + + verify(userRepository).existsByEmail(req.email()); + verify(userRepository, never()).save(any(User.class)); + verify(readingPreferenceService, never()).addClearReadingPreference(anyLong()); + } + + @Test + @DisplayName("비밀번호 암호화 확인") + void signUp_passwordEncoded() { + // given + SignReq req = new SignReq("test@test.com", "plainPassword"); + String encodedPassword = "encrypted_password_hash"; + + User mockUser = User.builder() + .id(1L) + .email(req.email()) + .password(encodedPassword) + .build(); + + when(userRepository.existsByEmail(req.email())).thenReturn(false); + when(signUpManager.isAdmin(req.email())).thenReturn(false); + when(passwordEncoder.encode(req.password())).thenReturn(encodedPassword); + when(userRepository.save(any(User.class))).thenReturn(mockUser); + + // when + signUpService.signUp(req); + + // then + verify(passwordEncoder).encode("plainPassword"); + verify(userRepository).save(argThat(user -> + user.getPassword().equals(encodedPassword) && + !user.getPassword().equals("plainPassword") + )); + } +} From ce8657a4b2773249daab4658be3c0eb2c957b2ec Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 2 Jan 2026 23:18:46 +0900 Subject: [PATCH 260/291] =?UTF-8?q?test:=20Curation=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EB=B2=84=EA=B7=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CurationCreateService 테스트 추가 (4개 테스트) - 큐레이션 발행/임시저장 - 유저 검증 - 발행/임시저장 구분 확인 - CurationLikeService 테스트 추가 (6개 테스트) - 좋아요 추가/취소 - 예외 처리 - 비관적 락 사용 확인 - 카운트 음수 방지 확인 - 버그 수정: 좋아요 카운트가 0일 때 취소 시 음수가 되는 문제 수정 - CurationLikeService.java: >= 0 → > 0 --- .../service/like/CurationLikeService.java | 2 +- .../service/CurationCreateServiceTest.java | 198 +++++++++++++++++ .../service/CurationLikeServiceTest.java | 209 ++++++++++++++++++ 3 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 src/test/java/BookPick/mvp/domain/curation/service/CurationCreateServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/service/CurationLikeServiceTest.java diff --git a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java index 7af41b8..ab9f57e 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/like/CurationLikeService.java @@ -57,7 +57,7 @@ public boolean CurationLikeOrUnlike(Long userId, Long curationId) { else { curationLikeRepository.delete(opt.get()); - if (curation.getLikeCount() >= 0) { + if (curation.getLikeCount() > 0) { curation.setLikeCount(curation.getLikeCount() - 1); } diff --git a/src/test/java/BookPick/mvp/domain/curation/service/CurationCreateServiceTest.java b/src/test/java/BookPick/mvp/domain/curation/service/CurationCreateServiceTest.java new file mode 100644 index 0000000..d7a466b --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/service/CurationCreateServiceTest.java @@ -0,0 +1,198 @@ +package BookPick.mvp.domain.curation.service; + +import BookPick.mvp.domain.curation.dto.base.CurationReq; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateRes; +import BookPick.mvp.domain.curation.dto.base.create.CurationCreateResult; +import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("큐레이션 생성 서비스 테스트") +class CurationCreateServiceTest { + + @InjectMocks + private CurationCreateService curationCreateService; + + @Mock + private CurationRepository curationRepository; + + @Mock + private UserRepository userRepository; + + @Test + @DisplayName("큐레이션 발행 성공") + void publishCuration_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + CurationReq req = new CurationReq( + "테스트 큐레이션", + new ThumbnailDto("thumbnail.jpg", "#FFFFFF"), + new BookDto("책 제목", "작가", "1234567890", "book.jpg"), + "이 책은 정말 좋습니다.", + new RecommendDto( + List.of("감동적인"), + List.of("소설"), + List.of("사랑"), + List.of("감성적인") + ), + false // isDrafted = false (발행) + ); + + Curation mockCuration = Curation.builder() + .id(1L) + .user(mockUser) + .title(req.title()) + .isDrafted(false) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.save(any(Curation.class))).thenReturn(mockCuration); + + // when + CurationCreateResult result = curationCreateService.saveCuration(userId, req); + + // then + assertThat(result.curationCreateRes().id()).isEqualTo(1L); + assertThat(result.successCode()).isEqualTo(SuccessCode.CURATION_PUBLISH_SUCCESS); + + verify(userRepository).findById(userId); + verify(curationRepository).save(argThat(c -> !c.getIsDrafted())); + } + + @Test + @DisplayName("큐레이션 임시저장 성공") + void draftCuration_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + CurationReq req = new CurationReq( + "임시저장 큐레이션", + new ThumbnailDto("thumbnail.jpg", "#FFFFFF"), + new BookDto("책 제목", "작가", "1234567890", "book.jpg"), + "리뷰 작성 중...", + new RecommendDto( + List.of("감동적인"), + List.of("소설"), + List.of("사랑"), + List.of("감성적인") + ), + true // isDrafted = true (임시저장) + ); + + Curation mockCuration = Curation.builder() + .id(2L) + .user(mockUser) + .title(req.title()) + .isDrafted(true) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.save(any(Curation.class))).thenReturn(mockCuration); + + // when + CurationCreateResult result = curationCreateService.saveCuration(userId, req); + + // then + assertThat(result.curationCreateRes().id()).isEqualTo(2L); + assertThat(result.successCode()).isEqualTo(SuccessCode.CURATION_DRAFT_SUCCESS); + + verify(userRepository).findById(userId); + verify(curationRepository).save(argThat(c -> c.getIsDrafted())); + } + + @Test + @DisplayName("존재하지 않는 유저 - 예외 발생") + void createCuration_userNotFound() { + // given + Long userId = 999L; + CurationReq req = new CurationReq( + "테스트", + new ThumbnailDto("thumb.jpg", "#FFF"), + new BookDto("책", "작가", "123", "book.jpg"), + "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + false + ); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> curationCreateService.saveCuration(userId, req)) + .isInstanceOf(UserNotFoundException.class); + + verify(userRepository).findById(userId); + verify(curationRepository, never()).save(any(Curation.class)); + } + + @Test + @DisplayName("발행/임시저장 구분 확인") + void createCuration_publishVsDraft() { + // given + Long userId = 1L; + User mockUser = User.builder().id(userId).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.save(any(Curation.class))).thenAnswer(invocation -> { + Curation c = invocation.getArgument(0); + return Curation.builder() + .id(1L) + .user(mockUser) + .isDrafted(c.getIsDrafted()) + .build(); + }); + + // when - 발행 + CurationReq publishReq = new CurationReq( + "발행", new ThumbnailDto("t.jpg", "#FFF"), + new BookDto("책", "작가", "123", "b.jpg"), "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + false + ); + CurationCreateResult publishResult = curationCreateService.saveCuration(userId, publishReq); + + // when - 임시저장 + CurationReq draftReq = new CurationReq( + "임시", new ThumbnailDto("t.jpg", "#FFF"), + new BookDto("책", "작가", "123", "b.jpg"), "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + true + ); + CurationCreateResult draftResult = curationCreateService.saveCuration(userId, draftReq); + + // then + assertThat(publishResult.successCode()).isEqualTo(SuccessCode.CURATION_PUBLISH_SUCCESS); + assertThat(draftResult.successCode()).isEqualTo(SuccessCode.CURATION_DRAFT_SUCCESS); + + verify(curationRepository, times(2)).save(any(Curation.class)); + } +} diff --git a/src/test/java/BookPick/mvp/domain/curation/service/CurationLikeServiceTest.java b/src/test/java/BookPick/mvp/domain/curation/service/CurationLikeServiceTest.java new file mode 100644 index 0000000..2d36ce6 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/service/CurationLikeServiceTest.java @@ -0,0 +1,209 @@ +package BookPick.mvp.domain.curation.service; + +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.curation.service.like.CurationLikeService; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("큐레이션 좋아요 서비스 테스트") +class CurationLikeServiceTest { + + @InjectMocks + private CurationLikeService curationLikeService; + + @Mock + private CurationRepository curationRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationLikeRepository curationLikeRepository; + + @Test + @DisplayName("좋아요 추가 성공") + void likeCuration_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .likeCount(0) + .build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)) + .thenReturn(Optional.empty()); + when(curationLikeRepository.save(any(CurationLike.class))) + .thenReturn(CurationLike.builder().build()); + + // when + boolean result = curationLikeService.CurationLikeOrUnlike(userId, curationId); + + // then + assertThat(result).isTrue(); + assertThat(mockCuration.getLikeCount()).isEqualTo(1); + + verify(curationRepository).findByIdWithLock(curationId); + verify(curationLikeRepository).save(any(CurationLike.class)); + verify(curationLikeRepository, never()).delete(any()); + } + + @Test + @DisplayName("좋아요 취소 성공") + void unlikeCuration_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .likeCount(5) + .build(); + + CurationLike existingLike = CurationLike.builder() + .user(mockUser) + .curation(mockCuration) + .build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)) + .thenReturn(Optional.of(existingLike)); + + // when + boolean result = curationLikeService.CurationLikeOrUnlike(userId, curationId); + + // then + assertThat(result).isFalse(); + assertThat(mockCuration.getLikeCount()).isEqualTo(4); + + verify(curationRepository).findByIdWithLock(curationId); + verify(curationLikeRepository).delete(existingLike); + verify(curationLikeRepository, never()).save(any()); + } + + @Test + @DisplayName("존재하지 않는 큐레이션 - 예외 발생") + void likeCuration_curationNotFound() { + // given + Long userId = 1L; + Long curationId = 999L; + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> curationLikeService.CurationLikeOrUnlike(userId, curationId)) + .isInstanceOf(CurationNotFoundException.class); + + verify(curationRepository).findByIdWithLock(curationId); + verify(curationLikeRepository, never()).save(any()); + verify(curationLikeRepository, never()).delete(any()); + } + + @Test + @DisplayName("존재하지 않는 유저 - 예외 발생") + void likeCuration_userNotFound() { + // given + Long userId = 999L; + Long curationId = 1L; + + Curation mockCuration = Curation.builder() + .id(curationId) + .build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> curationLikeService.CurationLikeOrUnlike(userId, curationId)) + .isInstanceOf(UserNotFoundException.class); + + verify(curationLikeRepository, never()).save(any()); + verify(curationLikeRepository, never()).delete(any()); + } + + @Test + @DisplayName("좋아요 카운트가 0일 때 취소해도 음수 안됨") + void unlikeCuration_countNotNegative() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder() + .id(curationId) + .likeCount(0) // 이미 0 + .build(); + + CurationLike existingLike = CurationLike.builder() + .user(mockUser) + .curation(mockCuration) + .build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)) + .thenReturn(Optional.of(existingLike)); + + // when + curationLikeService.CurationLikeOrUnlike(userId, curationId); + + // then + assertThat(mockCuration.getLikeCount()).isEqualTo(0); // 음수 아님 + } + + @Test + @DisplayName("비관적 락 사용 확인") + void likeCuration_usesPessimisticLock() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).likeCount(0).build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)) + .thenReturn(Optional.empty()); + when(curationLikeRepository.save(any())).thenReturn(CurationLike.builder().build()); + + // when + curationLikeService.CurationLikeOrUnlike(userId, curationId); + + // then + verify(curationRepository).findByIdWithLock(curationId); // 비관적 락 메서드 사용 + verify(curationRepository, never()).findById(curationId); // 일반 findById 사용 안함 + } +} From 56e6982ed0be26e53ee0589308ec7b6915d8f34b Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 14:48:01 +0900 Subject: [PATCH 261/291] =?UTF-8?q?bugfix=20:=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=8B=A8=EA=B1=B4=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=EC=84=9C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=98=20=ED=95=84=EB=93=9C=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EA=B1=B4=EB=93=9C=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=96=88=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20readonly=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/.env | 20 ++ .../auth/service/SignUpServiceTest.java | 2 +- .../comment/service/CommentServiceTest.java | 265 ++++++++++++++++++ 3 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/.env create mode 100644 src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java diff --git a/src/main/resources/.env b/src/main/resources/.env new file mode 100644 index 0000000..244ad9d --- /dev/null +++ b/src/main/resources/.env @@ -0,0 +1,20 @@ +SPRING_PROFILES_ACTIVE=local + +DB_HOST=hii.mysql.database.azure.com +DB_PORT=3306 +; DB_NAME=bookpick_product +DB_NAME=book_pick +DB_USERNAME=nan7789 +DB_PASSWORD=gustjq3735! + +JWT_ACCESS_SECRET=c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM +JWT_ACCESS_EXPIRATION=900000 +JWT_REFRESH_SECRET=d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw +JWT_REFRESH_EXPIRATION=604800000 + +KAKAO_API_KEY=103086ac1d365cf71f026f6caac34fb3 +GEMINI_API_KEY= +GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + + +SPRING_JPA_DDL_AUTO=update diff --git a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java index 670023f..ae2ffaf 100644 --- a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java +++ b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java @@ -8,7 +8,7 @@ import BookPick.mvp.domain.auth.util.Manager.signup.SignUpManager; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.repository.UserRepository; -import org.junit.jupit`er.api.DisplayName; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java b/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java new file mode 100644 index 0000000..ce5b45e --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java @@ -0,0 +1,265 @@ +package BookPick.mvp.domain.comment.service; + +import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; +import BookPick.mvp.domain.comment.dto.create.CommentCreateRes; +import BookPick.mvp.domain.comment.dto.delete.CommentDeleteRes; +import BookPick.mvp.domain.comment.entity.Comment; +import BookPick.mvp.domain.comment.exception.CommentNotFoundException; +import BookPick.mvp.domain.comment.repository.CommentRepository; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest { + + @InjectMocks + private CommentService commentService; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationRepository curationRepository; + + @Mock + private CommentRepository commentRepository; + + @Test + @DisplayName("댓글 생성 성공 - 일반 댓글") + void createComment_success() { + // given + Long userId = 1L; + Long curationId = 1L; + CommentCreateReq req = new CommentCreateReq(null, "좋은 큐레이션이네요!"); + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .commentCount(0) + .build(); + + Comment mockComment = Comment.builder() + .id(1L) + .user(mockUser) + .curation(mockCuration) + .content(req.content()) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.save(any(Comment.class))).thenReturn(mockComment); + + // when + CommentCreateRes result = commentService.createComment(userId, curationId, req); + + // then + assertThat(result.commentId()).isEqualTo(1L); + assertThat(mockCuration.getCommentCount()).isEqualTo(1); + + verify(userRepository).findById(userId); + verify(curationRepository).findByIdWithLock(curationId); + verify(commentRepository).save(any(Comment.class)); + } + + @Test + @DisplayName("대댓글 생성 성공") + void createReply_success() { + // given + Long userId = 1L; + Long curationId = 1L; + Long parentCommentId = 10L; + CommentCreateReq req = new CommentCreateReq(parentCommentId, "답글입니다!"); + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).commentCount(1).build(); + Comment parentComment = Comment.builder().id(parentCommentId).build(); + Comment mockReply = Comment.builder() + .id(2L) + .user(mockUser) + .curation(mockCuration) + .parent(parentComment) + .content(req.content()) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(parentCommentId)).thenReturn(Optional.of(parentComment)); + when(commentRepository.save(any(Comment.class))).thenReturn(mockReply); + + // when + CommentCreateRes result = commentService.createComment(userId, curationId, req); + + // then + assertThat(result.commentId()).isEqualTo(2L); + assertThat(mockCuration.getCommentCount()).isEqualTo(2); + + verify(commentRepository).findById(parentCommentId); + verify(commentRepository).save(argThat(comment -> + comment.getParent() != null && + comment.getParent().getId().equals(parentCommentId) + )); + } + + @Test + @DisplayName("댓글 삭제 성공") + void deleteComment_success() { + // given + Long curationId = 1L; + Long commentId = 1L; + + Curation mockCuration = Curation.builder() + .id(curationId) + .commentCount(5) + .build(); + + Comment mockComment = Comment.builder() + .id(commentId) + .content("댓글 내용") + .build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(commentId)).thenReturn(Optional.of(mockComment)); + + // when + CommentDeleteRes result = commentService.deleteComment(curationId, commentId); + + // then + assertThat(result.commentId()).isEqualTo(commentId); + assertThat(mockCuration.getCommentCount()).isEqualTo(4); + + verify(curationRepository).findByIdWithLock(curationId); + verify(commentRepository).delete(mockComment); + } + + @Test + @DisplayName("존재하지 않는 유저 - 예외 발생") + void createComment_userNotFound() { + // given + Long userId = 999L; + Long curationId = 1L; + CommentCreateReq req = new CommentCreateReq(null, "댓글"); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) + .isInstanceOf(UserNotFoundException.class); + + verify(userRepository).findById(userId); + verify(commentRepository, never()).save(any()); + } + + @Test + @DisplayName("존재하지 않는 큐레이션 - 예외 발생") + void createComment_curationNotFound() { + // given + Long userId = 1L; + Long curationId = 999L; + CommentCreateReq req = new CommentCreateReq(null, "댓글"); + + User mockUser = User.builder().id(userId).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) + .isInstanceOf(CurationNotFoundException.class); + + verify(commentRepository, never()).save(any()); + } + + @Test + @DisplayName("존재하지 않는 부모 댓글 - 예외 발생") + void createReply_parentCommentNotFound() { + // given + Long userId = 1L; + Long curationId = 1L; + Long invalidParentId = 999L; + CommentCreateReq req = new CommentCreateReq(invalidParentId, "답글"); + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(invalidParentId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) + .isInstanceOf(CommentNotFoundException.class); + + verify(commentRepository, never()).save(any()); + } + + @Test + @DisplayName("비관적 락 사용 확인 - 데드락 방지") + void createComment_usesPessimisticLock() { + // given + Long userId = 1L; + Long curationId = 1L; + CommentCreateReq req = new CommentCreateReq(null, "댓글"); + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).commentCount(0).build(); + Comment mockComment = Comment.builder().id(1L).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.save(any(Comment.class))).thenReturn(mockComment); + + // when + commentService.createComment(userId, curationId, req); + + // then + verify(curationRepository).findByIdWithLock(curationId); // 비관적 락 사용 + verify(curationRepository, never()).findById(curationId); // 일반 findById 사용 안함 + } + + @Test + @DisplayName("댓글 삭제 시 카운트가 0 이하로 내려가지 않음") + void deleteComment_countNotNegative() { + // given + Long curationId = 1L; + Long commentId = 1L; + + Curation mockCuration = Curation.builder() + .id(curationId) + .commentCount(0) + .build(); + + Comment mockComment = Comment.builder().id(commentId).build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(commentId)).thenReturn(Optional.of(mockComment)); + + // when + commentService.deleteComment(curationId, commentId); + + // then + assertThat(mockCuration.getCommentCount()).isEqualTo(0); // 음수 안됨 + + verify(commentRepository).delete(mockComment); + } +} From 9cf6ff47372c64ccf78aa286670947722ef02e48 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 14:50:02 +0900 Subject: [PATCH 262/291] =?UTF-8?q?Revert=20"bugfix=20:=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=8B=A8=EA=B1=B4=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EC=97=90=EC=84=9C=20=ED=81=90?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=98=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=EA=B1=B4=EB=93=9C=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=96=88=EA=B8=B0=20?= =?UTF-8?q?=EB=95=8C=EB=AC=B8=EC=97=90=20readonly=20=ED=95=B4=EC=A0=9C"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 56e6982ed0be26e53ee0589308ec7b6915d8f34b. --- src/main/resources/.env | 20 -- .../auth/service/SignUpServiceTest.java | 2 +- .../comment/service/CommentServiceTest.java | 265 ------------------ 3 files changed, 1 insertion(+), 286 deletions(-) delete mode 100644 src/main/resources/.env delete mode 100644 src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java diff --git a/src/main/resources/.env b/src/main/resources/.env deleted file mode 100644 index 244ad9d..0000000 --- a/src/main/resources/.env +++ /dev/null @@ -1,20 +0,0 @@ -SPRING_PROFILES_ACTIVE=local - -DB_HOST=hii.mysql.database.azure.com -DB_PORT=3306 -; DB_NAME=bookpick_product -DB_NAME=book_pick -DB_USERNAME=nan7789 -DB_PASSWORD=gustjq3735! - -JWT_ACCESS_SECRET=c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM -JWT_ACCESS_EXPIRATION=900000 -JWT_REFRESH_SECRET=d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw -JWT_REFRESH_EXPIRATION=604800000 - -KAKAO_API_KEY=103086ac1d365cf71f026f6caac34fb3 -GEMINI_API_KEY= -GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent - - -SPRING_JPA_DDL_AUTO=update diff --git a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java index ae2ffaf..670023f 100644 --- a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java +++ b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java @@ -8,7 +8,7 @@ import BookPick.mvp.domain.auth.util.Manager.signup.SignUpManager; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.repository.UserRepository; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupit`er.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java b/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java deleted file mode 100644 index ce5b45e..0000000 --- a/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java +++ /dev/null @@ -1,265 +0,0 @@ -package BookPick.mvp.domain.comment.service; - -import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; -import BookPick.mvp.domain.comment.dto.create.CommentCreateRes; -import BookPick.mvp.domain.comment.dto.delete.CommentDeleteRes; -import BookPick.mvp.domain.comment.entity.Comment; -import BookPick.mvp.domain.comment.exception.CommentNotFoundException; -import BookPick.mvp.domain.comment.repository.CommentRepository; -import BookPick.mvp.domain.curation.entity.Curation; -import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; -import BookPick.mvp.domain.curation.repository.CurationRepository; -import BookPick.mvp.domain.user.entity.User; -import BookPick.mvp.domain.user.exception.common.UserNotFoundException; -import BookPick.mvp.domain.user.repository.UserRepository; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("댓글 서비스 테스트") -class CommentServiceTest { - - @InjectMocks - private CommentService commentService; - - @Mock - private UserRepository userRepository; - - @Mock - private CurationRepository curationRepository; - - @Mock - private CommentRepository commentRepository; - - @Test - @DisplayName("댓글 생성 성공 - 일반 댓글") - void createComment_success() { - // given - Long userId = 1L; - Long curationId = 1L; - CommentCreateReq req = new CommentCreateReq(null, "좋은 큐레이션이네요!"); - - User mockUser = User.builder() - .id(userId) - .email("test@test.com") - .build(); - - Curation mockCuration = Curation.builder() - .id(curationId) - .commentCount(0) - .build(); - - Comment mockComment = Comment.builder() - .id(1L) - .user(mockUser) - .curation(mockCuration) - .content(req.content()) - .build(); - - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); - when(commentRepository.save(any(Comment.class))).thenReturn(mockComment); - - // when - CommentCreateRes result = commentService.createComment(userId, curationId, req); - - // then - assertThat(result.commentId()).isEqualTo(1L); - assertThat(mockCuration.getCommentCount()).isEqualTo(1); - - verify(userRepository).findById(userId); - verify(curationRepository).findByIdWithLock(curationId); - verify(commentRepository).save(any(Comment.class)); - } - - @Test - @DisplayName("대댓글 생성 성공") - void createReply_success() { - // given - Long userId = 1L; - Long curationId = 1L; - Long parentCommentId = 10L; - CommentCreateReq req = new CommentCreateReq(parentCommentId, "답글입니다!"); - - User mockUser = User.builder().id(userId).build(); - Curation mockCuration = Curation.builder().id(curationId).commentCount(1).build(); - Comment parentComment = Comment.builder().id(parentCommentId).build(); - Comment mockReply = Comment.builder() - .id(2L) - .user(mockUser) - .curation(mockCuration) - .parent(parentComment) - .content(req.content()) - .build(); - - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); - when(commentRepository.findById(parentCommentId)).thenReturn(Optional.of(parentComment)); - when(commentRepository.save(any(Comment.class))).thenReturn(mockReply); - - // when - CommentCreateRes result = commentService.createComment(userId, curationId, req); - - // then - assertThat(result.commentId()).isEqualTo(2L); - assertThat(mockCuration.getCommentCount()).isEqualTo(2); - - verify(commentRepository).findById(parentCommentId); - verify(commentRepository).save(argThat(comment -> - comment.getParent() != null && - comment.getParent().getId().equals(parentCommentId) - )); - } - - @Test - @DisplayName("댓글 삭제 성공") - void deleteComment_success() { - // given - Long curationId = 1L; - Long commentId = 1L; - - Curation mockCuration = Curation.builder() - .id(curationId) - .commentCount(5) - .build(); - - Comment mockComment = Comment.builder() - .id(commentId) - .content("댓글 내용") - .build(); - - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); - when(commentRepository.findById(commentId)).thenReturn(Optional.of(mockComment)); - - // when - CommentDeleteRes result = commentService.deleteComment(curationId, commentId); - - // then - assertThat(result.commentId()).isEqualTo(commentId); - assertThat(mockCuration.getCommentCount()).isEqualTo(4); - - verify(curationRepository).findByIdWithLock(curationId); - verify(commentRepository).delete(mockComment); - } - - @Test - @DisplayName("존재하지 않는 유저 - 예외 발생") - void createComment_userNotFound() { - // given - Long userId = 999L; - Long curationId = 1L; - CommentCreateReq req = new CommentCreateReq(null, "댓글"); - - when(userRepository.findById(userId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) - .isInstanceOf(UserNotFoundException.class); - - verify(userRepository).findById(userId); - verify(commentRepository, never()).save(any()); - } - - @Test - @DisplayName("존재하지 않는 큐레이션 - 예외 발생") - void createComment_curationNotFound() { - // given - Long userId = 1L; - Long curationId = 999L; - CommentCreateReq req = new CommentCreateReq(null, "댓글"); - - User mockUser = User.builder().id(userId).build(); - - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) - .isInstanceOf(CurationNotFoundException.class); - - verify(commentRepository, never()).save(any()); - } - - @Test - @DisplayName("존재하지 않는 부모 댓글 - 예외 발생") - void createReply_parentCommentNotFound() { - // given - Long userId = 1L; - Long curationId = 1L; - Long invalidParentId = 999L; - CommentCreateReq req = new CommentCreateReq(invalidParentId, "답글"); - - User mockUser = User.builder().id(userId).build(); - Curation mockCuration = Curation.builder().id(curationId).build(); - - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); - when(commentRepository.findById(invalidParentId)).thenReturn(Optional.empty()); - - // when & then - assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) - .isInstanceOf(CommentNotFoundException.class); - - verify(commentRepository, never()).save(any()); - } - - @Test - @DisplayName("비관적 락 사용 확인 - 데드락 방지") - void createComment_usesPessimisticLock() { - // given - Long userId = 1L; - Long curationId = 1L; - CommentCreateReq req = new CommentCreateReq(null, "댓글"); - - User mockUser = User.builder().id(userId).build(); - Curation mockCuration = Curation.builder().id(curationId).commentCount(0).build(); - Comment mockComment = Comment.builder().id(1L).build(); - - when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); - when(commentRepository.save(any(Comment.class))).thenReturn(mockComment); - - // when - commentService.createComment(userId, curationId, req); - - // then - verify(curationRepository).findByIdWithLock(curationId); // 비관적 락 사용 - verify(curationRepository, never()).findById(curationId); // 일반 findById 사용 안함 - } - - @Test - @DisplayName("댓글 삭제 시 카운트가 0 이하로 내려가지 않음") - void deleteComment_countNotNegative() { - // given - Long curationId = 1L; - Long commentId = 1L; - - Curation mockCuration = Curation.builder() - .id(curationId) - .commentCount(0) - .build(); - - Comment mockComment = Comment.builder().id(commentId).build(); - - when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); - when(commentRepository.findById(commentId)).thenReturn(Optional.of(mockComment)); - - // when - commentService.deleteComment(curationId, commentId); - - // then - assertThat(mockCuration.getCommentCount()).isEqualTo(0); // 음수 안됨 - - verify(commentRepository).delete(mockComment); - } -} From 52961664f3dd503b1047e3e1a00195bd873620e6 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 14:50:44 +0900 Subject: [PATCH 263/291] =?UTF-8?q?Reapply=20"feat=20:=20test=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20=20"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9cf6ff47372c64ccf78aa286670947722ef02e48. --- src/main/resources/.env | 20 ++ .../auth/service/SignUpServiceTest.java | 2 +- .../comment/service/CommentServiceTest.java | 265 ++++++++++++++++++ 3 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/.env create mode 100644 src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java diff --git a/src/main/resources/.env b/src/main/resources/.env new file mode 100644 index 0000000..244ad9d --- /dev/null +++ b/src/main/resources/.env @@ -0,0 +1,20 @@ +SPRING_PROFILES_ACTIVE=local + +DB_HOST=hii.mysql.database.azure.com +DB_PORT=3306 +; DB_NAME=bookpick_product +DB_NAME=book_pick +DB_USERNAME=nan7789 +DB_PASSWORD=gustjq3735! + +JWT_ACCESS_SECRET=c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM +JWT_ACCESS_EXPIRATION=900000 +JWT_REFRESH_SECRET=d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw +JWT_REFRESH_EXPIRATION=604800000 + +KAKAO_API_KEY=103086ac1d365cf71f026f6caac34fb3 +GEMINI_API_KEY= +GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + + +SPRING_JPA_DDL_AUTO=update diff --git a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java index 670023f..ae2ffaf 100644 --- a/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java +++ b/src/test/java/BookPick/mvp/domain/auth/service/SignUpServiceTest.java @@ -8,7 +8,7 @@ import BookPick.mvp.domain.auth.util.Manager.signup.SignUpManager; import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.repository.UserRepository; -import org.junit.jupit`er.api.DisplayName; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; diff --git a/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java b/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java new file mode 100644 index 0000000..ce5b45e --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/comment/service/CommentServiceTest.java @@ -0,0 +1,265 @@ +package BookPick.mvp.domain.comment.service; + +import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; +import BookPick.mvp.domain.comment.dto.create.CommentCreateRes; +import BookPick.mvp.domain.comment.dto.delete.CommentDeleteRes; +import BookPick.mvp.domain.comment.entity.Comment; +import BookPick.mvp.domain.comment.exception.CommentNotFoundException; +import BookPick.mvp.domain.comment.repository.CommentRepository; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest { + + @InjectMocks + private CommentService commentService; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationRepository curationRepository; + + @Mock + private CommentRepository commentRepository; + + @Test + @DisplayName("댓글 생성 성공 - 일반 댓글") + void createComment_success() { + // given + Long userId = 1L; + Long curationId = 1L; + CommentCreateReq req = new CommentCreateReq(null, "좋은 큐레이션이네요!"); + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .commentCount(0) + .build(); + + Comment mockComment = Comment.builder() + .id(1L) + .user(mockUser) + .curation(mockCuration) + .content(req.content()) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.save(any(Comment.class))).thenReturn(mockComment); + + // when + CommentCreateRes result = commentService.createComment(userId, curationId, req); + + // then + assertThat(result.commentId()).isEqualTo(1L); + assertThat(mockCuration.getCommentCount()).isEqualTo(1); + + verify(userRepository).findById(userId); + verify(curationRepository).findByIdWithLock(curationId); + verify(commentRepository).save(any(Comment.class)); + } + + @Test + @DisplayName("대댓글 생성 성공") + void createReply_success() { + // given + Long userId = 1L; + Long curationId = 1L; + Long parentCommentId = 10L; + CommentCreateReq req = new CommentCreateReq(parentCommentId, "답글입니다!"); + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).commentCount(1).build(); + Comment parentComment = Comment.builder().id(parentCommentId).build(); + Comment mockReply = Comment.builder() + .id(2L) + .user(mockUser) + .curation(mockCuration) + .parent(parentComment) + .content(req.content()) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(parentCommentId)).thenReturn(Optional.of(parentComment)); + when(commentRepository.save(any(Comment.class))).thenReturn(mockReply); + + // when + CommentCreateRes result = commentService.createComment(userId, curationId, req); + + // then + assertThat(result.commentId()).isEqualTo(2L); + assertThat(mockCuration.getCommentCount()).isEqualTo(2); + + verify(commentRepository).findById(parentCommentId); + verify(commentRepository).save(argThat(comment -> + comment.getParent() != null && + comment.getParent().getId().equals(parentCommentId) + )); + } + + @Test + @DisplayName("댓글 삭제 성공") + void deleteComment_success() { + // given + Long curationId = 1L; + Long commentId = 1L; + + Curation mockCuration = Curation.builder() + .id(curationId) + .commentCount(5) + .build(); + + Comment mockComment = Comment.builder() + .id(commentId) + .content("댓글 내용") + .build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(commentId)).thenReturn(Optional.of(mockComment)); + + // when + CommentDeleteRes result = commentService.deleteComment(curationId, commentId); + + // then + assertThat(result.commentId()).isEqualTo(commentId); + assertThat(mockCuration.getCommentCount()).isEqualTo(4); + + verify(curationRepository).findByIdWithLock(curationId); + verify(commentRepository).delete(mockComment); + } + + @Test + @DisplayName("존재하지 않는 유저 - 예외 발생") + void createComment_userNotFound() { + // given + Long userId = 999L; + Long curationId = 1L; + CommentCreateReq req = new CommentCreateReq(null, "댓글"); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) + .isInstanceOf(UserNotFoundException.class); + + verify(userRepository).findById(userId); + verify(commentRepository, never()).save(any()); + } + + @Test + @DisplayName("존재하지 않는 큐레이션 - 예외 발생") + void createComment_curationNotFound() { + // given + Long userId = 1L; + Long curationId = 999L; + CommentCreateReq req = new CommentCreateReq(null, "댓글"); + + User mockUser = User.builder().id(userId).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) + .isInstanceOf(CurationNotFoundException.class); + + verify(commentRepository, never()).save(any()); + } + + @Test + @DisplayName("존재하지 않는 부모 댓글 - 예외 발생") + void createReply_parentCommentNotFound() { + // given + Long userId = 1L; + Long curationId = 1L; + Long invalidParentId = 999L; + CommentCreateReq req = new CommentCreateReq(invalidParentId, "답글"); + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(invalidParentId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> commentService.createComment(userId, curationId, req)) + .isInstanceOf(CommentNotFoundException.class); + + verify(commentRepository, never()).save(any()); + } + + @Test + @DisplayName("비관적 락 사용 확인 - 데드락 방지") + void createComment_usesPessimisticLock() { + // given + Long userId = 1L; + Long curationId = 1L; + CommentCreateReq req = new CommentCreateReq(null, "댓글"); + + User mockUser = User.builder().id(userId).build(); + Curation mockCuration = Curation.builder().id(curationId).commentCount(0).build(); + Comment mockComment = Comment.builder().id(1L).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.save(any(Comment.class))).thenReturn(mockComment); + + // when + commentService.createComment(userId, curationId, req); + + // then + verify(curationRepository).findByIdWithLock(curationId); // 비관적 락 사용 + verify(curationRepository, never()).findById(curationId); // 일반 findById 사용 안함 + } + + @Test + @DisplayName("댓글 삭제 시 카운트가 0 이하로 내려가지 않음") + void deleteComment_countNotNegative() { + // given + Long curationId = 1L; + Long commentId = 1L; + + Curation mockCuration = Curation.builder() + .id(curationId) + .commentCount(0) + .build(); + + Comment mockComment = Comment.builder().id(commentId).build(); + + when(curationRepository.findByIdWithLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(commentRepository.findById(commentId)).thenReturn(Optional.of(mockComment)); + + // when + commentService.deleteComment(curationId, commentId); + + // then + assertThat(mockCuration.getCommentCount()).isEqualTo(0); // 음수 안됨 + + verify(commentRepository).delete(mockComment); + } +} From 6a8c9735244ea37bc69801cf97a5d0a428819f1a Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 14:52:43 +0900 Subject: [PATCH 264/291] =?UTF-8?q?bugfix=20:=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=8B=A8=EA=B1=B4=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=EC=84=9C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=98=20=ED=95=84=EB=93=9C=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EA=B1=B4=EB=93=9C=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=96=88=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20readonly=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/service/base/read/CurationReadService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index ef53c43..d6d7dbc 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -32,7 +32,7 @@ public class CurationReadService { // -- 큐레이션 단건 조회 -- - @Transactional(readOnly = true) + @Transactional public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req, boolean isEdit) { boolean isLikedCuration = false; boolean isSubscribedCurator = false; From 5eaf002d0d4afbf38718d57a5535306c026465ec Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 14:52:43 +0900 Subject: [PATCH 265/291] =?UTF-8?q?bugfix=20:=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EB=8B=A8=EA=B1=B4=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=EC=84=9C=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=98=20=ED=95=84=EB=93=9C=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EA=B1=B4=EB=93=9C=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=B4=20=EC=A1=B4=EC=9E=AC=ED=96=88=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90=20readonly=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/curation/service/base/read/CurationReadService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java index ef53c43..d6d7dbc 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/base/read/CurationReadService.java @@ -32,7 +32,7 @@ public class CurationReadService { // -- 큐레이션 단건 조회 -- - @Transactional(readOnly = true) + @Transactional public CurationGetRes findCuration(Long curationId, CustomUserDetails user, HttpServletRequest req, boolean isEdit) { boolean isLikedCuration = false; boolean isSubscribedCurator = false; From 4bea7cdfe4da0ed85cf5902b6c1ac5dc389886c4 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 16:43:44 +0900 Subject: [PATCH 266/291] =?UTF-8?q?feat(ReadingPreference):=20=EB=8F=85?= =?UTF-8?q?=EC=84=9C=20=EC=B7=A8=ED=96=A5=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ReadingPreferenceServiceTest.java | 370 ++++++++++++++++++ ...eadingPreferenceValidCheckServiceTest.java | 276 +++++++++++++ 2 files changed, 646 insertions(+) create mode 100644 src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckServiceTest.java diff --git a/src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceServiceTest.java b/src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceServiceTest.java new file mode 100644 index 0000000..d7ceba5 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceServiceTest.java @@ -0,0 +1,370 @@ +package BookPick.mvp.domain.ReadingPreference.service; + +import BookPick.mvp.domain.ReadingPreference.Exception.fail.AlreadyRegisteredReadingPreferenceException; +import BookPick.mvp.domain.ReadingPreference.Exception.fail.UserReadingPreferenceNotExisted; +import BookPick.mvp.domain.ReadingPreference.dto.ETC.Delete.ReadingPreferenceDeleteRes; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceRes; +import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; +import BookPick.mvp.domain.ReadingPreference.repository.ReadingPreferenceRepository; +import BookPick.mvp.domain.author.dto.preference.AuthorDto; +import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.author.service.AuthorSaveService; +import BookPick.mvp.domain.book.dto.preference.BookDto; +import BookPick.mvp.domain.book.entity.Book; +import BookPick.mvp.domain.book.repository.BookRepository; +import BookPick.mvp.domain.book.service.BookSaveService; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("독서취향 서비스 테스트") +class ReadingPreferenceServiceTest { + + @InjectMocks + private ReadingPreferenceService readingPreferenceService; + + @Mock + private ReadingPreferenceRepository readingPreferenceRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private BookSaveService bookSaveService; + + @Mock + private AuthorSaveService authorSaveService; + + @Mock + private BookRepository bookRepository; + + @Mock + private ReadingPreferenceValidCheckService readingPreferenceValidCheckService; + + @Test + @DisplayName("정상 독서취향 등록 성공") + void addReadingPreference_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + BookDto bookDto = new BookDto("Test Book", "Test Author", "image.jpg", "isbn123"); + AuthorDto authorDto = new AuthorDto("Test Author"); + + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(bookDto), + Set.of(authorDto), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + Book mockBook = Book.builder() + .isbn("isbn123") + .title("Test Book") + .build(); + + Author mockAuthor = Author.builder() + .name("Test Author") + .build(); + + ReadingPreference mockPreference = ReadingPreference.builder() + .user(mockUser) + .mbti("INTJ") + .favoriteBooks(Set.of(mockBook)) + .favoriteAuthors(Set.of(mockAuthor)) + .moods(List.of("차분한")) + .readingHabits(List.of("매일 읽기")) + .genres(List.of("소설")) + .keywords(List.of("성장")) + .readingStyles(List.of("몰입형")) + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.existsByUserId(userId)).thenReturn(false); + when(bookSaveService.saveBookIfNotExistsDto(anySet())).thenReturn(Set.of(mockBook)); + when(authorSaveService.saveAuthorIfNotExistsDto(anySet())).thenReturn(Set.of(mockAuthor)); + when(readingPreferenceRepository.save(any(ReadingPreference.class))).thenReturn(mockPreference); + + // when + ReadingPreferenceRes result = readingPreferenceService.addReadingPreference(userId, req); + + // then + assertThat(result).isNotNull(); + assertThat(result.mbti()).isEqualTo("INTJ"); + verify(userRepository).findById(userId); + verify(readingPreferenceRepository).existsByUserId(userId); + verify(bookSaveService).saveBookIfNotExistsDto(anySet()); + verify(authorSaveService).saveAuthorIfNotExistsDto(anySet()); + verify(readingPreferenceRepository).save(any(ReadingPreference.class)); + } + + @Test + @DisplayName("독서취향 등록 실패 - 사용자 없음") + void addReadingPreference_fail_userNotFound() { + // given + Long userId = 1L; + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> readingPreferenceService.addReadingPreference(userId, req)) + .isInstanceOf(UserNotFoundException.class); + + verify(userRepository).findById(userId); + verify(readingPreferenceRepository, never()).save(any()); + } + + @Test + @DisplayName("독서취향 등록 실패 - 이미 등록됨") + void addReadingPreference_fail_alreadyRegistered() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.existsByUserId(userId)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> readingPreferenceService.addReadingPreference(userId, req)) + .isInstanceOf(AlreadyRegisteredReadingPreferenceException.class); + + verify(readingPreferenceRepository, never()).save(any()); + } + + @Test + @DisplayName("빈 독서취향 등록 성공") + void addClearReadingPreference_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + ReadingPreference mockPreference = ReadingPreference.clearPreferences(mockUser); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.existsByUserId(userId)).thenReturn(false); + when(readingPreferenceRepository.save(any(ReadingPreference.class))).thenReturn(mockPreference); + + // when + ReadingPreferenceRes result = readingPreferenceService.addClearReadingPreference(userId); + + // then + assertThat(result).isNotNull(); + verify(userRepository).findById(userId); + verify(readingPreferenceRepository).existsByUserId(userId); + verify(readingPreferenceRepository).save(any(ReadingPreference.class)); + } + + @Test + @DisplayName("독서취향 조회 성공") + void findReadingPreference_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + ReadingPreference mockPreference = ReadingPreference.builder() + .user(mockUser) + .mbti("INTJ") + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.findByUserId(userId)).thenReturn(Optional.of(mockPreference)); + + // when + ReadingPreferenceRes result = readingPreferenceService.findReadingPreference(userId); + + // then + assertThat(result).isNotNull(); + assertThat(result.mbti()).isEqualTo("INTJ"); + verify(userRepository).findById(userId); + verify(readingPreferenceRepository).findByUserId(userId); + } + + @Test + @DisplayName("독서취향 조회 실패 - 독서취향 없음") + void findReadingPreference_fail_notExisted() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.findByUserId(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> readingPreferenceService.findReadingPreference(userId)) + .isInstanceOf(UserReadingPreferenceNotExisted.class); + + verify(userRepository).findById(userId); + verify(readingPreferenceRepository).findByUserId(userId); + } + + @Test + @DisplayName("독서취향 수정 성공") + void modifyReadingPreference_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + BookDto bookDto = new BookDto("New Book", "New Author", "new_image.jpg", "isbn456"); + AuthorDto authorDto = new AuthorDto("New Author"); + + ReadingPreferenceReq req = new ReadingPreferenceReq( + "ENFP", + Set.of(bookDto), + Set.of(authorDto), + List.of("설레는"), + List.of("주말에 읽기"), + List.of("에세이"), + List.of("힐링"), + List.of("건너뛰며") + ); + + Book mockBook = Book.builder() + .isbn("isbn456") + .title("New Book") + .build(); + + Author mockAuthor = Author.builder() + .name("New Author") + .build(); + + ReadingPreference mockPreference = ReadingPreference.builder() + .user(mockUser) + .mbti("INTJ") + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.findByUserId(userId)).thenReturn(Optional.of(mockPreference)); + when(bookSaveService.saveBookIfNotExistsDto(anySet())).thenReturn(Set.of(mockBook)); + when(authorSaveService.saveAuthorIfNotExistsDto(anySet())).thenReturn(Set.of(mockAuthor)); + doNothing().when(readingPreferenceValidCheckService).validateReadingPreferenceReq(any()); + + // when + ReadingPreferenceRes result = readingPreferenceService.modifyReadingPreference(userId, req); + + // then + assertThat(result).isNotNull(); + verify(userRepository).findById(userId); + verify(readingPreferenceRepository).findByUserId(userId); + verify(bookSaveService).saveBookIfNotExistsDto(anySet()); + verify(authorSaveService).saveAuthorIfNotExistsDto(anySet()); + verify(readingPreferenceValidCheckService).validateReadingPreferenceReq(req); + } + + @Test + @DisplayName("독서취향 삭제 성공") + void removeReadingPreference_success() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + ReadingPreference mockPreference = ReadingPreference.builder() + .user(mockUser) + .mbti("INTJ") + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.findByUserId(userId)).thenReturn(Optional.of(mockPreference)); + doNothing().when(readingPreferenceRepository).delete(mockPreference); + + // when + ReadingPreferenceDeleteRes result = readingPreferenceService.removeReadingPreference(userId); + + // then + assertThat(result).isNotNull(); + assertThat(result.deletedAt()).isNotNull(); + verify(userRepository).findById(userId); + verify(readingPreferenceRepository).findByUserId(userId); + verify(readingPreferenceRepository).delete(mockPreference); + } + + @Test + @DisplayName("독서취향 삭제 실패 - 독서취향 없음") + void removeReadingPreference_fail_notExisted() { + // given + Long userId = 1L; + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .password("password") + .build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser)); + when(readingPreferenceRepository.findByUserId(userId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> readingPreferenceService.removeReadingPreference(userId)) + .isInstanceOf(UserReadingPreferenceNotExisted.class); + + verify(readingPreferenceRepository, never()).delete(any()); + } +} diff --git a/src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckServiceTest.java b/src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckServiceTest.java new file mode 100644 index 0000000..d3a6104 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/ReadingPreference/service/ReadingPreferenceValidCheckServiceTest.java @@ -0,0 +1,276 @@ +package BookPick.mvp.domain.ReadingPreference.service; + +import BookPick.mvp.domain.ReadingPreference.Exception.fail.WrongReadingPreferenceRequestException; +import BookPick.mvp.domain.ReadingPreference.dto.ReadingPreferenceReq; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +@DisplayName("독서취향 검증 서비스 테스트") +class ReadingPreferenceValidCheckServiceTest { + + @InjectMocks + private ReadingPreferenceValidCheckService validCheckService; + + @Test + @DisplayName("정상 독서취향 요청 검증 성공") + void validateReadingPreferenceReq_success() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatCode(() -> validCheckService.validateReadingPreferenceReq(req)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("MBTI null 또는 빈 문자열 - 검증 통과") + void validateReadingPreferenceReq_nullOrEmptyMbti_success() { + // given + ReadingPreferenceReq req1 = new ReadingPreferenceReq( + null, + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + ReadingPreferenceReq req2 = new ReadingPreferenceReq( + "", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatCode(() -> validCheckService.validateReadingPreferenceReq(req1)) + .doesNotThrowAnyException(); + assertThatCode(() -> validCheckService.validateReadingPreferenceReq(req2)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("잘못된 MBTI - 검증 실패") + void validateReadingPreferenceReq_invalidMbti_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INVALID", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } + + @Test + @DisplayName("잘못된 Mood - 검증 실패") + void validateReadingPreferenceReq_invalidMood_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("잘못된_무드"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } + + @Test + @DisplayName("잘못된 ReadingHabit - 검증 실패") + void validateReadingPreferenceReq_invalidReadingHabit_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("잘못된_습관"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } + + @Test + @DisplayName("잘못된 Genre - 검증 실패") + void validateReadingPreferenceReq_invalidGenre_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("잘못된_장르"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } + + @Test + @DisplayName("잘못된 Keyword - 검증 실패") + void validateReadingPreferenceReq_invalidKeyword_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("잘못된_키워드"), + List.of("몰입형") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } + + @Test + @DisplayName("잘못된 ReadingStyle - 검증 실패") + void validateReadingPreferenceReq_invalidReadingStyle_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("잘못된_스타일") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } + + @Test + @DisplayName("null 컬렉션 - 검증 통과") + void validateReadingPreferenceReq_nullCollections_success() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + null, + null, + null, + null, + null + ); + + // when & then + assertThatCode(() -> validCheckService.validateReadingPreferenceReq(req)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("빈 컬렉션 - 검증 통과") + void validateReadingPreferenceReq_emptyCollections_success() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of(), + List.of(), + List.of(), + List.of(), + List.of() + ); + + // when & then + assertThatCode(() -> validCheckService.validateReadingPreferenceReq(req)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("여러 올바른 값 - 검증 통과") + void validateReadingPreferenceReq_multipleValidValues_success() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "ENFP", + Set.of(), + Set.of(), + List.of("차분한", "설레는"), + List.of("매일 읽기", "주말에 읽기"), + List.of("소설", "에세이"), + List.of("성장", "힐링"), + List.of("몰입형", "건너뛰며") + ); + + // when & then + assertThatCode(() -> validCheckService.validateReadingPreferenceReq(req)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("혼합된 올바른 값과 잘못된 값 - 검증 실패") + void validateReadingPreferenceReq_mixedValidAndInvalid_fail() { + // given + ReadingPreferenceReq req = new ReadingPreferenceReq( + "INTJ", + Set.of(), + Set.of(), + List.of("차분한", "잘못된_무드"), + List.of("매일 읽기"), + List.of("소설"), + List.of("성장"), + List.of("몰입형") + ); + + // when & then + assertThatThrownBy(() -> validCheckService.validateReadingPreferenceReq(req)) + .isInstanceOf(WrongReadingPreferenceRequestException.class); + } +} From 8972c7e060204c9fefc8c0fe9c7be4c540fbe008 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 16:43:51 +0900 Subject: [PATCH 267/291] =?UTF-8?q?feat(Auth):=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B0=8F=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/LoginControllerTest.java | 269 ++++++++++++++ .../auth/controller/LogoutControllerTest.java | 192 ++++++++++ .../auth/controller/SignUpControllerTest.java | 186 ++++++++++ .../TokenRefreshControllerTest.java | 332 +++++++++++++++++ .../auth/service/LogoutServiceTest.java | 308 ++++++++++++++++ .../service/MyUserDetailsServiceTest.java | 256 +++++++++++++ .../auth/service/TokenRefreshServiceTest.java | 347 ++++++++++++++++++ 7 files changed, 1890 insertions(+) create mode 100644 src/test/java/BookPick/mvp/domain/auth/controller/LoginControllerTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/controller/LogoutControllerTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/controller/SignUpControllerTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/controller/TokenRefreshControllerTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/service/LogoutServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/service/MyUserDetailsServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/auth/service/TokenRefreshServiceTest.java diff --git a/src/test/java/BookPick/mvp/domain/auth/controller/LoginControllerTest.java b/src/test/java/BookPick/mvp/domain/auth/controller/LoginControllerTest.java new file mode 100644 index 0000000..e941793 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/controller/LoginControllerTest.java @@ -0,0 +1,269 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.domain.auth.dto.LoginReq; +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.exception.InvalidLoginException; +import BookPick.mvp.domain.auth.service.LoginService; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.RefreshTokenCookieManager; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +@ActiveProfiles("test") +@Transactional +@DisplayName("로그인 컨트롤러 테스트") +class LoginControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private LoginService loginService; + + @MockBean + private RefreshTokenCookieManager refreshTokenCookieManager; + + @Test + @DisplayName("정상 로그인 성공 - 첫 로그인") + void login_success_firstLogin() throws Exception { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + LoginRes loginRes = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + true, + "access_token", + "refresh_token" + ); + + when(loginService.login(any(LoginReq.class), any(HttpServletRequest.class))) + .thenReturn(loginRes); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("LOGIN_SUCCESS")) + .andExpect(jsonPath("$.data.userId").value(1L)) + .andExpect(jsonPath("$.data.email").value("test@test.com")) + .andExpect(jsonPath("$.data.nickname").value("테스터")) + .andExpect(jsonPath("$.data.accessToken").value("access_token")) + .andExpect(jsonPath("$.data.isFirstLogin").value(true)) + .andExpect(jsonPath("$.data.refreshToken").doesNotExist()); // refresh token은 응답에 없어야 함 + + verify(loginService).login(any(LoginReq.class), any(HttpServletRequest.class)); + verify(refreshTokenCookieManager).addRefreshTokenCookie(any(HttpServletResponse.class), eq("refresh_token")); + } + + @Test + @DisplayName("정상 로그인 성공 - 재로그인") + void login_success_notFirstLogin() throws Exception { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + LoginRes loginRes = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + false, + "access_token", + "refresh_token" + ); + + when(loginService.login(any(LoginReq.class), any(HttpServletRequest.class))) + .thenReturn(loginRes); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isFirstLogin").value(false)); + + verify(refreshTokenCookieManager).addRefreshTokenCookie(any(HttpServletResponse.class), eq("refresh_token")); + } + + @Test + @DisplayName("로그인 실패 - 잘못된 비밀번호") + void login_fail_wrongPassword() throws Exception { + // given + LoginReq req = new LoginReq("test@test.com", "wrongPassword"); + + when(loginService.login(any(LoginReq.class), any(HttpServletRequest.class))) + .thenThrow(new InvalidLoginException()); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isUnauthorized()); + + verify(loginService).login(any(LoginReq.class), any(HttpServletRequest.class)); + verify(refreshTokenCookieManager, never()).addRefreshTokenCookie(any(), anyString()); + } + + @Test + @DisplayName("로그인 실패 - 존재하지 않는 사용자") + void login_fail_userNotFound() throws Exception { + // given + LoginReq req = new LoginReq("notexist@test.com", "password123"); + + when(loginService.login(any(LoginReq.class), any(HttpServletRequest.class))) + .thenThrow(new InvalidLoginException()); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isUnauthorized()); + + verify(refreshTokenCookieManager, never()).addRefreshTokenCookie(any(), anyString()); + } + + @Test + @DisplayName("이메일 형식 검증 - 빈 이메일") + void login_fail_emptyEmail() throws Exception { + // given + LoginReq req = new LoginReq("", "password123"); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(loginService, never()).login(any(LoginReq.class), any(HttpServletRequest.class)); + } + + @Test + @DisplayName("이메일 형식 검증 - 잘못된 형식") + void login_fail_invalidEmailFormat() throws Exception { + // given + LoginReq req = new LoginReq("invalid-email", "password123"); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(loginService, never()).login(any(LoginReq.class), any(HttpServletRequest.class)); + } + + @Test + @DisplayName("비밀번호 검증 - 빈 비밀번호") + void login_fail_emptyPassword() throws Exception { + // given + LoginReq req = new LoginReq("test@test.com", ""); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(loginService, never()).login(any(LoginReq.class), any(HttpServletRequest.class)); + } + + @Test + @DisplayName("요청 바디 없음 - 예외 발생") + void login_fail_noRequestBody() throws Exception { + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + verify(loginService, never()).login(any(LoginReq.class), any(HttpServletRequest.class)); + } + + @Test + @DisplayName("리프레시 토큰 쿠키 설정 확인") + void login_refreshTokenCookie_set() throws Exception { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + LoginRes loginRes = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + false, + "access_token", + "refresh_token_value" + ); + + when(loginService.login(any(LoginReq.class), any(HttpServletRequest.class))) + .thenReturn(loginRes); + + // when + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()); + + // then + verify(refreshTokenCookieManager).addRefreshTokenCookie( + any(HttpServletResponse.class), + eq("refresh_token_value") + ); + } + + @Test + @DisplayName("응답에서 리프레시 토큰 제거 확인") + void login_refreshTokenNotInResponse() throws Exception { + // given + LoginReq req = new LoginReq("test@test.com", "password123"); + LoginRes loginRes = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + false, + "access_token", + "refresh_token" + ); + + when(loginService.login(any(LoginReq.class), any(HttpServletRequest.class))) + .thenReturn(loginRes); + + // when & then + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.accessToken").exists()) + .andExpect(jsonPath("$.data.refreshToken").doesNotExist()); + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/controller/LogoutControllerTest.java b/src/test/java/BookPick/mvp/domain/auth/controller/LogoutControllerTest.java new file mode 100644 index 0000000..9cc38e0 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/controller/LogoutControllerTest.java @@ -0,0 +1,192 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.auth.service.LogoutService; +import BookPick.mvp.domain.user.entity.User; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; + +@WebMvcTest(value = LogoutController.class, + excludeAutoConfiguration = { + SecurityAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, + JpaRepositoriesAutoConfiguration.class + }) +@DisplayName("로그아웃 컨트롤러 테스트") +class LogoutControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private LogoutService logoutService; + + @MockBean + private BookPick.mvp.global.util.JwtUtil jwtUtil; + + @MockBean + private BookPick.mvp.global.config.JwtFilter jwtFilter; + + @Test + @DisplayName("정상 로그아웃 성공") + @WithMockUser + void logout_success() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .role(Roles.ROLE_USER) + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + doNothing().when(logoutService).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + + // when & then + mockMvc.perform(post("/api/v1/auth/logout") + .with(user(userDetails))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("LOGOUT_SUCCESS")) + .andExpect(jsonPath("$.data").doesNotExist()); + + verify(logoutService).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + @DisplayName("미인증 사용자 로그아웃 시도 - 예외 발생") + void logout_fail_notAuthenticated() throws Exception { + // given + doThrow(new NotAuthenticateUser()) + .when(logoutService).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + + // when & then + mockMvc.perform(post("/api/v1/auth/logout")) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("로그아웃 서비스 호출 확인") + @WithMockUser + void logout_serviceInvoked() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + doNothing().when(logoutService).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + + // when + mockMvc.perform(post("/api/v1/auth/logout") + .with(user(userDetails))) + .andExpect(status().isOk()); + + // then + verify(logoutService, times(1)).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + @DisplayName("로그아웃 응답 형식 확인") + @WithMockUser + void logout_responseFormat() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + doNothing().when(logoutService).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + + // when & then + mockMvc.perform(post("/api/v1/auth/logout") + .with(user(userDetails))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").exists()) + .andExpect(jsonPath("$.message").exists()); + } + + @Test + @DisplayName("여러 사용자 연속 로그아웃") + @WithMockUser + void logout_multipleUsers() throws Exception { + // given + User user1 = User.builder() + .id(1L) + .email("user1@test.com") + .password("password") + .build(); + + User user2 = User.builder() + .id(2L) + .email("user2@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails1 = new CustomUserDetails( + user1, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails1.setId(1L); + + CustomUserDetails userDetails2 = new CustomUserDetails( + user2, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails2.setId(2L); + + doNothing().when(logoutService).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + + // when & then + mockMvc.perform(post("/api/v1/auth/logout") + .with(user(userDetails1))) + .andExpect(status().isOk()); + + mockMvc.perform(post("/api/v1/auth/logout") + .with(user(userDetails2))) + .andExpect(status().isOk()); + + verify(logoutService, times(2)).logout(any(), any(HttpServletRequest.class), any(HttpServletResponse.class)); + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/controller/SignUpControllerTest.java b/src/test/java/BookPick/mvp/domain/auth/controller/SignUpControllerTest.java new file mode 100644 index 0000000..5f7a3ae --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/controller/SignUpControllerTest.java @@ -0,0 +1,186 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.domain.auth.dto.SignReq; +import BookPick.mvp.domain.auth.dto.SignRes; +import BookPick.mvp.domain.auth.exception.DuplicateEmailException; +import BookPick.mvp.domain.auth.service.SignUpService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; + +@WebMvcTest(value = SignUpController.class, + excludeAutoConfiguration = { + SecurityAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, + JpaRepositoriesAutoConfiguration.class + }) +@DisplayName("회원가입 컨트롤러 테스트") +class SignUpControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private SignUpService authService; + + @MockBean + private BookPick.mvp.global.util.JwtUtil jwtUtil; + + @MockBean + private BookPick.mvp.global.config.JwtFilter jwtFilter; + + @Test + @DisplayName("정상 회원가입 성공") + void signUp_success() throws Exception { + // given + SignReq req = new SignReq("test@test.com", "password123"); + SignRes res = new SignRes(1L); + + when(authService.signUp(any(SignReq.class))).thenReturn(res); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("REGISTER_SUCCESS")) + .andExpect(jsonPath("$.data.userId").value(1L)); + + verify(authService).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("이메일 중복 - 예외 발생") + void signUp_fail_duplicateEmail() throws Exception { + // given + SignReq req = new SignReq("duplicate@test.com", "password123"); + + when(authService.signUp(any(SignReq.class))) + .thenThrow(new DuplicateEmailException()); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(authService).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("이메일 형식 검증 - 빈 이메일") + void signUp_fail_emptyEmail() throws Exception { + // given + SignReq req = new SignReq("", "password123"); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(authService, never()).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("이메일 형식 검증 - 잘못된 형식") + void signUp_fail_invalidEmailFormat() throws Exception { + // given + SignReq req = new SignReq("invalid-email", "password123"); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(authService, never()).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("비밀번호 검증 - 빈 비밀번호") + void signUp_fail_emptyPassword() throws Exception { + // given + SignReq req = new SignReq("test@test.com", ""); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isBadRequest()); + + verify(authService, never()).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("요청 바디 없음 - 예외 발생") + void signUp_fail_noRequestBody() throws Exception { + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + verify(authService, never()).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("Content-Type 누락 - 예외 발생") + void signUp_fail_noContentType() throws Exception { + // given + SignReq req = new SignReq("test@test.com", "password123"); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .content(objectMapper.writeValueAsString(req))) + .andExpect(status().isUnsupportedMediaType()); + + verify(authService, never()).signUp(any(SignReq.class)); + } + + @Test + @DisplayName("여러 사용자 연속 회원가입") + void signUp_multipleUsers_success() throws Exception { + // given + SignReq req1 = new SignReq("user1@test.com", "password1"); + SignReq req2 = new SignReq("user2@test.com", "password2"); + + SignRes res1 = new SignRes(1L); + SignRes res2 = new SignRes(2L); + + when(authService.signUp(any(SignReq.class))) + .thenReturn(res1) + .thenReturn(res2); + + // when & then + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req1))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userId").value(1L)); + + mockMvc.perform(post("/api/v1/auth/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(req2))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.userId").value(2L)); + + verify(authService, times(2)).signUp(any(SignReq.class)); + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/controller/TokenRefreshControllerTest.java b/src/test/java/BookPick/mvp/domain/auth/controller/TokenRefreshControllerTest.java new file mode 100644 index 0000000..a4b69c1 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/controller/TokenRefreshControllerTest.java @@ -0,0 +1,332 @@ +package BookPick.mvp.domain.auth.controller; + +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.auth.service.TokenRefreshService; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.RefreshTokenCookieManager; +import BookPick.mvp.domain.user.entity.User; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; + +@WebMvcTest(value = TokenRefreshController.class, + excludeAutoConfiguration = { + SecurityAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, + JpaRepositoriesAutoConfiguration.class + }) +@DisplayName("토큰 갱신 컨트롤러 테스트") +class TokenRefreshControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private RefreshTokenCookieManager refreshTokenCookieManager; + + @MockBean + private TokenRefreshService tokenRefreshService; + + @MockBean + private BookPick.mvp.global.util.JwtUtil jwtUtil; + + @MockBean + private BookPick.mvp.global.config.JwtFilter jwtFilter; + + @Test + @DisplayName("정상 토큰 갱신 성공") + @WithMockUser + void refreshToken_success() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .nickname("테스터") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + userDetails.setNickname("테스터"); + + String oldRefreshToken = "old_refresh_token"; + LoginRes newTokens = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + false, + "new_access_token", + "new_refresh_token" + ); + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn(oldRefreshToken); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), eq(oldRefreshToken))) + .thenReturn(newTokens); + + // when & then + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("TOKEN_REFERSH_SUCCESS")) + .andExpect(jsonPath("$.data.userId").value(1L)) + .andExpect(jsonPath("$.data.accessToken").value("new_access_token")) + .andExpect(jsonPath("$.data.refreshToken").doesNotExist()); // refresh token은 응답에 없어야 함 + + verify(refreshTokenCookieManager).getRefreshTokenFromCookie(any(HttpServletRequest.class)); + verify(tokenRefreshService).refreshTokens(any(CustomUserDetails.class), eq(oldRefreshToken)); + verify(refreshTokenCookieManager).addRefreshTokenCookie(any(HttpServletResponse.class), eq("new_refresh_token")); + } + + @Test + @DisplayName("토큰 갱신 실패 - 리프레시 토큰 없음") + @WithMockUser + void refreshToken_fail_noRefreshToken() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn(null); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), isNull())) + .thenThrow(new RuntimeException("리프레시 토큰이 없습니다.")); + + // when & then + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().is5xxServerError()); + + verify(refreshTokenCookieManager).getRefreshTokenFromCookie(any(HttpServletRequest.class)); + verify(refreshTokenCookieManager, never()).addRefreshTokenCookie(any(), anyString()); + } + + @Test + @DisplayName("토큰 갱신 실패 - 만료된 리프레시 토큰") + @WithMockUser + void refreshToken_fail_expiredToken() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + String expiredToken = "expired_refresh_token"; + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn(expiredToken); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), eq(expiredToken))) + .thenThrow(new RuntimeException("리프레시 토큰이 만료되었거나 유효하지 않습니다.")); + + // when & then + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().is5xxServerError()); + + verify(refreshTokenCookieManager, never()).addRefreshTokenCookie(any(), anyString()); + } + + @Test + @DisplayName("토큰 갱신 실패 - 블랙리스트 토큰") + @WithMockUser + void refreshToken_fail_blacklistedToken() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + String blacklistedToken = "blacklisted_token"; + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn(blacklistedToken); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), eq(blacklistedToken))) + .thenThrow(new RuntimeException("유효하지 않은 리프레시 토큰입니다.")); + + // when & then + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().is5xxServerError()); + + verify(refreshTokenCookieManager, never()).addRefreshTokenCookie(any(), anyString()); + } + + @Test + @DisplayName("미인증 사용자 토큰 갱신 시도") + void refreshToken_fail_notAuthenticated() throws Exception { + // when & then + mockMvc.perform(post("/api/v1/auth/token/refresh")) + .andExpect(status().isUnauthorized()); + + verify(refreshTokenCookieManager, never()).getRefreshTokenFromCookie(any()); + verify(tokenRefreshService, never()).refreshTokens(any(), anyString()); + } + + @Test + @DisplayName("새 리프레시 토큰 쿠키 설정 확인") + @WithMockUser + void refreshToken_newRefreshTokenCookieSet() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + String oldToken = "old_token"; + String newRefreshToken = "brand_new_refresh_token"; + LoginRes newTokens = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + false, + "new_access_token", + newRefreshToken + ); + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn(oldToken); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), eq(oldToken))) + .thenReturn(newTokens); + + // when + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().isOk()); + + // then + verify(refreshTokenCookieManager).addRefreshTokenCookie( + any(HttpServletResponse.class), + eq(newRefreshToken) + ); + } + + @Test + @DisplayName("응답에서 리프레시 토큰 제거 확인") + @WithMockUser + void refreshToken_refreshTokenNotInResponse() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + LoginRes newTokens = new LoginRes( + 1L, + "test@test.com", + "테스터", + "자기소개", + "profile.jpg", + false, + "new_access_token", + "new_refresh_token" + ); + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn("old_token"); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), anyString())) + .thenReturn(newTokens); + + // when & then + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.accessToken").exists()) + .andExpect(jsonPath("$.data.refreshToken").doesNotExist()); + } + + @Test + @DisplayName("토큰 갱신 서비스 호출 확인") + @WithMockUser + void refreshToken_serviceInvoked() throws Exception { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails userDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + userDetails.setId(1L); + + String refreshToken = "refresh_token"; + LoginRes newTokens = new LoginRes(1L, "test@test.com", "테스터", "자기소개", "profile.jpg", false, "access", "refresh"); + + when(refreshTokenCookieManager.getRefreshTokenFromCookie(any(HttpServletRequest.class))) + .thenReturn(refreshToken); + when(tokenRefreshService.refreshTokens(any(CustomUserDetails.class), eq(refreshToken))) + .thenReturn(newTokens); + + // when + mockMvc.perform(post("/api/v1/auth/token/refresh") + .with(user(userDetails))) + .andExpect(status().isOk()); + + // then + verify(tokenRefreshService, times(1)).refreshTokens(any(CustomUserDetails.class), eq(refreshToken)); + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/service/LogoutServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/LogoutServiceTest.java new file mode 100644 index 0000000..06b96ad --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/service/LogoutServiceTest.java @@ -0,0 +1,308 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.exception.JwtTokenExpiredException; +import BookPick.mvp.domain.auth.exception.NotAuthenticateUser; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.TokenBlacklistManager; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.global.util.JwtUtil; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.time.Instant; +import java.util.Collections; +import java.util.Date; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("로그아웃 서비스 테스트") +class LogoutServiceTest { + + @InjectMocks + private LogoutService logoutService; + + @Mock + private JwtUtil jwtUtil; + + @Mock + private TokenBlacklistManager tokenBlacklistManager; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Test + @DisplayName("정상 로그아웃 성공") + void logout_success() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .role(Roles.ROLE_USER) + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + String refreshToken = "valid_refresh_token"; + Cookie refreshCookie = new Cookie("refreshToken", refreshToken); + + Claims claims = mock(Claims.class); + when(claims.get("userId", Number.class)).thenReturn(1); + when(claims.getId()).thenReturn("jti-12345"); + when(claims.getExpiration()).thenReturn(new Date(System.currentTimeMillis() + 3600000)); // 1시간 후 + + when(request.getCookies()).thenReturn(new Cookie[]{refreshCookie}); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + + // when + logoutService.logout(currentUser, request, response); + + // then + verify(jwtUtil).extractRefreshToken(refreshToken); + verify(tokenBlacklistManager).add(eq("jti-12345"), any(Instant.class)); + verify(response).addCookie(argThat(cookie -> + cookie.getName().equals("refreshToken") && + cookie.getValue() == null && + cookie.getMaxAge() == 0 && + cookie.isHttpOnly() && + cookie.getSecure() + )); + } + + @Test + @DisplayName("미인증 사용자 로그아웃 시도 - 예외 발생") + void logout_fail_notAuthenticated() { + // given + CustomUserDetails currentUser = null; + + // when & then + assertThatThrownBy(() -> logoutService.logout(currentUser, request, response)) + .isInstanceOf(NotAuthenticateUser.class); + + verify(tokenBlacklistManager, never()).add(anyString(), any(Instant.class)); + verify(response, never()).addCookie(any(Cookie.class)); + } + + @Test + @DisplayName("쿠키가 없는 경우 - 정상 종료") + void logout_noCookies_success() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + when(request.getCookies()).thenReturn(null); + + // when + logoutService.logout(currentUser, request, response); + + // then + verify(jwtUtil, never()).extractRefreshToken(anyString()); + verify(tokenBlacklistManager, never()).add(anyString(), any(Instant.class)); + verify(response, never()).addCookie(any(Cookie.class)); + } + + @Test + @DisplayName("리프레시 토큰이 빈 값인 경우 - 정상 종료") + void logout_emptyRefreshToken_success() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + Cookie emptyCookie = new Cookie("refreshToken", ""); + + when(request.getCookies()).thenReturn(new Cookie[]{emptyCookie}); + + // when + logoutService.logout(currentUser, request, response); + + // then + verify(jwtUtil, never()).extractRefreshToken(anyString()); + verify(tokenBlacklistManager, never()).add(anyString(), any(Instant.class)); + verify(response, never()).addCookie(any(Cookie.class)); + } + + @Test + @DisplayName("만료된 토큰으로 로그아웃 시도 - 예외 발생") + void logout_fail_expiredToken() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + String expiredToken = "expired_refresh_token"; + Cookie refreshCookie = new Cookie("refreshToken", expiredToken); + + when(request.getCookies()).thenReturn(new Cookie[]{refreshCookie}); + when(jwtUtil.extractRefreshToken(expiredToken)) + .thenThrow(new RuntimeException("Token expired")); + + // when & then + assertThatThrownBy(() -> logoutService.logout(currentUser, request, response)) + .isInstanceOf(JwtTokenExpiredException.class); + + verify(tokenBlacklistManager, never()).add(anyString(), any(Instant.class)); + } + + @Test + @DisplayName("다른 쿠키들 사이에 리프레시 토큰이 있는 경우") + void logout_withMultipleCookies_success() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + String refreshToken = "valid_refresh_token"; + Cookie[] cookies = new Cookie[]{ + new Cookie("sessionId", "session123"), + new Cookie("refreshToken", refreshToken), + new Cookie("otherCookie", "value") + }; + + Claims claims = mock(Claims.class); + when(claims.get("userId", Number.class)).thenReturn(1); + when(claims.getId()).thenReturn("jti-12345"); + when(claims.getExpiration()).thenReturn(new Date(System.currentTimeMillis() + 3600000)); + + when(request.getCookies()).thenReturn(cookies); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + + // when + logoutService.logout(currentUser, request, response); + + // then + verify(jwtUtil).extractRefreshToken(refreshToken); + verify(tokenBlacklistManager).add(eq("jti-12345"), any(Instant.class)); + verify(response).addCookie(any(Cookie.class)); + } + + @Test + @DisplayName("토큰 블랙리스트 추가 확인 - TTL 설정") + void logout_blacklistTTL_correct() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + String refreshToken = "valid_refresh_token"; + Cookie refreshCookie = new Cookie("refreshToken", refreshToken); + + long expirationTime = System.currentTimeMillis() + 7200000; // 2시간 후 + Claims claims = mock(Claims.class); + when(claims.get("userId", Number.class)).thenReturn(1); + when(claims.getId()).thenReturn("jti-unique"); + when(claims.getExpiration()).thenReturn(new Date(expirationTime)); + + when(request.getCookies()).thenReturn(new Cookie[]{refreshCookie}); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + + // when + logoutService.logout(currentUser, request, response); + + // then + verify(tokenBlacklistManager).add( + eq("jti-unique"), + argThat(instant -> { + long expectedMillis = expirationTime - System.currentTimeMillis(); + long actualMillis = instant.toEpochMilli() - System.currentTimeMillis(); + // 1초 정도 오차 허용 + return Math.abs(actualMillis - expectedMillis) < 1000; + }) + ); + } + + @Test + @DisplayName("만료된 토큰 (음수 TTL) - 블랙리스트 추가하지 않음") + void logout_expiredToken_notAddedToBlacklist() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails currentUser = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + currentUser.setId(1L); + + String refreshToken = "almost_expired_token"; + Cookie refreshCookie = new Cookie("refreshToken", refreshToken); + + Claims claims = mock(Claims.class); + when(claims.get("userId", Number.class)).thenReturn(1); + when(claims.getId()).thenReturn("jti-expired"); + when(claims.getExpiration()).thenReturn(new Date(System.currentTimeMillis() - 1000)); // 이미 만료됨 + + when(request.getCookies()).thenReturn(new Cookie[]{refreshCookie}); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + + // when + logoutService.logout(currentUser, request, response); + + // then + verify(tokenBlacklistManager, never()).add(anyString(), any(Instant.class)); + verify(response).addCookie(any(Cookie.class)); // 쿠키는 여전히 삭제됨 + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/service/MyUserDetailsServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/MyUserDetailsServiceTest.java new file mode 100644 index 0000000..b227c47 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/service/MyUserDetailsServiceTest.java @@ -0,0 +1,256 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("UserDetailsService 테스트") +class MyUserDetailsServiceTest { + + @InjectMocks + private MyUserDetailsService myUserDetailsService; + + @Mock + private UserRepository userRepository; + + @Test + @DisplayName("정상 사용자 조회 - 일반 유저") + void loadUserByUsername_success_normalUser() { + // given + String email = "test@test.com"; + User mockUser = User.builder() + .id(1L) + .email(email) + .password("encodedPassword") + .nickname("테스터") + .bio("자기소개") + .profileImageUrl("profile.jpg") + .role(Roles.ROLE_USER) + .isFirstLogin(true) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser)); + + // when + UserDetails result = myUserDetailsService.loadUserByUsername(email); + + // then + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(CustomUserDetails.class); + assertThat(result.getUsername()).isEqualTo(email); + assertThat(result.getPassword()).isEqualTo("encodedPassword"); + + CustomUserDetails customResult = (CustomUserDetails) result; + assertThat(customResult.getId()).isEqualTo(1L); + assertThat(customResult.getNickname()).isEqualTo("테스터"); + assertThat(customResult.getBio()).isEqualTo("자기소개"); + assertThat(customResult.getProfileImageUrl()).isEqualTo("profile.jpg"); + assertThat(customResult.isFirstLogin()).isTrue(); + + verify(userRepository).findByEmail(email); + } + + @Test + @DisplayName("권한 부여 확인 - ROLE_USER") + void loadUserByUsername_roleUser_granted() { + // given + String email = "user@test.com"; + User mockUser = User.builder() + .id(1L) + .email(email) + .password("password") + .role(Roles.ROLE_USER) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser)); + + // when + UserDetails result = myUserDetailsService.loadUserByUsername(email); + + // then + assertThat(result.getAuthorities()).hasSize(1); + assertThat(result.getAuthorities()) + .extracting(GrantedAuthority::getAuthority) + .containsExactly("ROLE_USER"); + + verify(userRepository).findByEmail(email); + } + + @Test + @DisplayName("존재하지 않는 사용자 - 예외 발생") + void loadUserByUsername_fail_userNotFound() { + // given + String email = "notexist@test.com"; + + when(userRepository.findByEmail(email)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> myUserDetailsService.loadUserByUsername(email)) + .isInstanceOf(UserNotFoundException.class); + + verify(userRepository).findByEmail(email); + } + + @Test + @DisplayName("CustomUserDetails 필드 전체 검증") + void loadUserByUsername_allFieldsSet() { + // given + String email = "complete@test.com"; + User mockUser = User.builder() + .id(999L) + .email(email) + .password("hashedPassword123") + .nickname("완전한사용자") + .bio("완전한 자기소개입니다") + .profileImageUrl("https://example.com/profile.jpg") + .role(Roles.ROLE_USER) + .isFirstLogin(false) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser)); + + // when + UserDetails result = myUserDetailsService.loadUserByUsername(email); + + // then + CustomUserDetails customResult = (CustomUserDetails) result; + assertThat(customResult.getId()).isEqualTo(999L); + assertThat(customResult.getUsername()).isEqualTo(email); + assertThat(customResult.getPassword()).isEqualTo("hashedPassword123"); + assertThat(customResult.getNickname()).isEqualTo("완전한사용자"); + assertThat(customResult.getBio()).isEqualTo("완전한 자기소개입니다"); + assertThat(customResult.getProfileImageUrl()).isEqualTo("https://example.com/profile.jpg"); + assertThat(customResult.isFirstLogin()).isFalse(); + assertThat(customResult.getAuthorities()).hasSize(1); + + verify(userRepository).findByEmail(email); + } + + @Test + @DisplayName("닉네임이 null인 경우") + void loadUserByUsername_nullNickname_success() { + // given + String email = "nonickname@test.com"; + User mockUser = User.builder() + .id(1L) + .email(email) + .password("password") + .nickname(null) + .role(Roles.ROLE_USER) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser)); + + // when + UserDetails result = myUserDetailsService.loadUserByUsername(email); + + // then + CustomUserDetails customResult = (CustomUserDetails) result; + assertThat(customResult.getNickname()).isNull(); + + verify(userRepository).findByEmail(email); + } + + @Test + @DisplayName("여러 번 호출해도 정상 동작") + void loadUserByUsername_multipleCalls_success() { + // given + String email = "multi@test.com"; + User mockUser = User.builder() + .id(1L) + .email(email) + .password("password") + .role(Roles.ROLE_USER) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser)); + + // when + UserDetails result1 = myUserDetailsService.loadUserByUsername(email); + UserDetails result2 = myUserDetailsService.loadUserByUsername(email); + + // then + assertThat(result1).isNotNull(); + assertThat(result2).isNotNull(); + assertThat(result1.getUsername()).isEqualTo(result2.getUsername()); + + verify(userRepository, times(2)).findByEmail(email); + } + + @Test + @DisplayName("프로필 이미지 URL이 없는 경우") + void loadUserByUsername_noProfileImage_success() { + // given + String email = "noimage@test.com"; + User mockUser = User.builder() + .id(1L) + .email(email) + .password("password") + .nickname("이미지없음") + .profileImageUrl(null) + .role(Roles.ROLE_USER) + .build(); + + when(userRepository.findByEmail(email)).thenReturn(Optional.of(mockUser)); + + // when + UserDetails result = myUserDetailsService.loadUserByUsername(email); + + // then + CustomUserDetails customResult = (CustomUserDetails) result; + assertThat(customResult.getProfileImageUrl()).isNull(); + assertThat(customResult.getNickname()).isEqualTo("이미지없음"); + + verify(userRepository).findByEmail(email); + } + + @Test + @DisplayName("첫 로그인 플래그 확인") + void loadUserByUsername_firstLoginFlag() { + // given + String email1 = "first@test.com"; + String email2 = "second@test.com"; + + User firstLoginUser = User.builder() + .id(1L) + .email(email1) + .password("password") + .role(Roles.ROLE_USER) + .isFirstLogin(true) + .build(); + + User returningUser = User.builder() + .id(2L) + .email(email2) + .password("password") + .role(Roles.ROLE_USER) + .isFirstLogin(false) + .build(); + + when(userRepository.findByEmail(email1)).thenReturn(Optional.of(firstLoginUser)); + when(userRepository.findByEmail(email2)).thenReturn(Optional.of(returningUser)); + + // when + CustomUserDetails firstResult = (CustomUserDetails) myUserDetailsService.loadUserByUsername(email1); + CustomUserDetails secondResult = (CustomUserDetails) myUserDetailsService.loadUserByUsername(email2); + + // then + assertThat(firstResult.isFirstLogin()).isTrue(); + assertThat(secondResult.isFirstLogin()).isFalse(); + } +} diff --git a/src/test/java/BookPick/mvp/domain/auth/service/TokenRefreshServiceTest.java b/src/test/java/BookPick/mvp/domain/auth/service/TokenRefreshServiceTest.java new file mode 100644 index 0000000..e2ab08c --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/auth/service/TokenRefreshServiceTest.java @@ -0,0 +1,347 @@ +package BookPick.mvp.domain.auth.service; + +import BookPick.mvp.domain.auth.Roles; +import BookPick.mvp.domain.auth.dto.LoginRes; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.JwtAuthManager; +import BookPick.mvp.domain.auth.util.Manager.login.jwt.TokenBlacklistManager; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.global.util.JwtUtil; +import io.jsonwebtoken.Claims; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("토큰 갱신 서비스 테스트") +class TokenRefreshServiceTest { + + @InjectMocks + private TokenRefreshService tokenRefreshService; + + @Mock + private JwtAuthManager jwtAuthManager; + + @Mock + private TokenBlacklistManager tokenBlacklistManager; + + @Mock + private JwtUtil jwtUtil; + + @Test + @DisplayName("정상 토큰 갱신 성공") + void refreshTokens_success() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .nickname("테스터") + .role(Roles.ROLE_USER) + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + customUserDetails.setNickname("테스터"); + + String refreshToken = "valid_refresh_token"; + + Claims claims = mock(Claims.class); + when(claims.get("userId", Double.class)).thenReturn(1.0); // Double로 저장 + + JwtAuthManager.TokenPair newTokenPair = new JwtAuthManager.TokenPair( + "new_access_token", + "new_refresh_token" + ); + + when(tokenBlacklistManager.isBlacklisted(refreshToken)).thenReturn(false); + when(jwtUtil.validateToken(refreshToken, false)).thenReturn(true); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + when(jwtAuthManager.createTokens(any())).thenReturn(newTokenPair); + + // when + LoginRes result = tokenRefreshService.refreshTokens(customUserDetails, refreshToken); + + // then + assertThat(result).isNotNull(); + assertThat(result.userId()).isEqualTo(1L); + assertThat(result.accessToken()).isEqualTo("new_access_token"); + assertThat(result.refreshToken()).isEqualTo("new_refresh_token"); + + verify(tokenBlacklistManager).isBlacklisted(refreshToken); + verify(jwtUtil).validateToken(refreshToken, false); + verify(jwtUtil).extractRefreshToken(refreshToken); + verify(jwtAuthManager).createTokens(any()); + } + + @Test + @DisplayName("토큰이 null인 경우 - 예외 발생") + void refreshTokens_fail_nullToken() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + + // when & then + assertThatThrownBy(() -> tokenRefreshService.refreshTokens(customUserDetails, null)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("리프레시 토큰이 없습니다"); + + verify(tokenBlacklistManager, never()).isBlacklisted(anyString()); + verify(jwtAuthManager, never()).createTokens(any()); + } + + @Test + @DisplayName("토큰이 빈 문자열인 경우 - 예외 발생") + void refreshTokens_fail_blankToken() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + + // when & then + assertThatThrownBy(() -> tokenRefreshService.refreshTokens(customUserDetails, " ")) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("리프레시 토큰이 없습니다"); + + verify(tokenBlacklistManager, never()).isBlacklisted(anyString()); + } + + @Test + @DisplayName("블랙리스트에 등록된 토큰 - 예외 발생") + void refreshTokens_fail_blacklistedToken() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + + String blacklistedToken = "blacklisted_token"; + + when(tokenBlacklistManager.isBlacklisted(blacklistedToken)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> tokenRefreshService.refreshTokens(customUserDetails, blacklistedToken)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("유효하지 않은 리프레시 토큰입니다"); + + verify(tokenBlacklistManager).isBlacklisted(blacklistedToken); + verify(jwtUtil, never()).validateToken(anyString(), anyBoolean()); + verify(jwtAuthManager, never()).createTokens(any()); + } + + @Test + @DisplayName("만료된 토큰 - 예외 발생") + void refreshTokens_fail_expiredToken() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + + String expiredToken = "expired_token"; + + when(tokenBlacklistManager.isBlacklisted(expiredToken)).thenReturn(false); + when(jwtUtil.validateToken(expiredToken, false)).thenReturn(false); + + // when & then + assertThatThrownBy(() -> tokenRefreshService.refreshTokens(customUserDetails, expiredToken)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("리프레시 토큰이 만료되었거나 유효하지 않습니다"); + + verify(jwtUtil).validateToken(expiredToken, false); + verify(jwtUtil, never()).extractRefreshToken(anyString()); + verify(jwtAuthManager, never()).createTokens(any()); + } + + @Test + @DisplayName("토큰의 사용자 정보와 현재 사용자 불일치 - 예외 발생") + void refreshTokens_fail_userMismatch() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + + String refreshToken = "valid_token_but_different_user"; + + Claims claims = mock(Claims.class); + when(claims.get("userId", Double.class)).thenReturn(2.0); // 다른 사용자 ID + + when(tokenBlacklistManager.isBlacklisted(refreshToken)).thenReturn(false); + when(jwtUtil.validateToken(refreshToken, false)).thenReturn(true); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + + // when & then + assertThatThrownBy(() -> tokenRefreshService.refreshTokens(customUserDetails, refreshToken)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("토큰 사용자 정보와 일치하지 않습니다"); + + verify(jwtUtil).extractRefreshToken(refreshToken); + verify(jwtAuthManager, never()).createTokens(any()); + } + + @Test + @DisplayName("토큰 갱신 후 새로운 토큰 페어 반환 확인") + void refreshTokens_newTokensReturned() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .nickname("테스터") + .role(Roles.ROLE_USER) + .isFirstLogin(false) + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + customUserDetails.setNickname("테스터"); + customUserDetails.setFirstLogin(false); + + String oldRefreshToken = "old_refresh_token"; + + Claims claims = mock(Claims.class); + when(claims.get("userId", Double.class)).thenReturn(1.0); + + JwtAuthManager.TokenPair newTokenPair = new JwtAuthManager.TokenPair( + "brand_new_access_token", + "brand_new_refresh_token" + ); + + when(tokenBlacklistManager.isBlacklisted(oldRefreshToken)).thenReturn(false); + when(jwtUtil.validateToken(oldRefreshToken, false)).thenReturn(true); + when(jwtUtil.extractRefreshToken(oldRefreshToken)).thenReturn(claims); + when(jwtAuthManager.createTokens(any())).thenReturn(newTokenPair); + + // when + LoginRes result = tokenRefreshService.refreshTokens(customUserDetails, oldRefreshToken); + + // then + assertThat(result.accessToken()).isEqualTo("brand_new_access_token"); + assertThat(result.refreshToken()).isEqualTo("brand_new_refresh_token"); + assertThat(result.accessToken()).isNotEqualTo(oldRefreshToken); + assertThat(result.nickname()).isEqualTo("테스터"); + assertThat(result.isFirstLogin()).isFalse(); + } + + @Test + @DisplayName("검증 순서 확인 - 블랙리스트 먼저, 유효성 두번째") + void refreshTokens_validationOrder() { + // given + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .password("password") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(1L); + + String refreshToken = "test_token"; + + when(tokenBlacklistManager.isBlacklisted(refreshToken)).thenReturn(true); + + // when & then + assertThatThrownBy(() -> tokenRefreshService.refreshTokens(customUserDetails, refreshToken)) + .isInstanceOf(RuntimeException.class); + + // 블랙리스트 체크는 실행되었지만, 유효성 검사는 실행되지 않아야 함 + verify(tokenBlacklistManager).isBlacklisted(refreshToken); + verify(jwtUtil, never()).validateToken(anyString(), anyBoolean()); + } + + @Test + @DisplayName("Integer 타입 userId도 정상 처리") + void refreshTokens_integerUserId_success() { + // given + User mockUser = User.builder() + .id(5L) + .email("test@test.com") + .password("password") + .nickname("테스터") + .build(); + + CustomUserDetails customUserDetails = new CustomUserDetails( + mockUser, + Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")) + ); + customUserDetails.setId(5L); + customUserDetails.setNickname("테스터"); + + String refreshToken = "valid_token"; + + Claims claims = mock(Claims.class); + when(claims.get("userId", Double.class)).thenReturn(5.0); // Double로 저장 + + JwtAuthManager.TokenPair newTokenPair = new JwtAuthManager.TokenPair( + "access", "refresh" + ); + + when(tokenBlacklistManager.isBlacklisted(refreshToken)).thenReturn(false); + when(jwtUtil.validateToken(refreshToken, false)).thenReturn(true); + when(jwtUtil.extractRefreshToken(refreshToken)).thenReturn(claims); + when(jwtAuthManager.createTokens(any())).thenReturn(newTokenPair); + + // when + LoginRes result = tokenRefreshService.refreshTokens(customUserDetails, refreshToken); + + // then + assertThat(result.userId()).isEqualTo(5L); + } +} From b46f3418e3aaac88f845067c0ecc3e6db4c69f04 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 16:43:57 +0900 Subject: [PATCH 268/291] =?UTF-8?q?feat(Comment):=20=EB=8C=93=EA=B8=80=20D?= =?UTF-8?q?TO=20=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/read/CommentDetailResTest.java | 0 .../dto/read/ReceivedCommentsDTOTest.java | 0 .../dto/update/CommentUpdateResTest.java | 0 .../service/PagenationServiceTest.java | 46 +++++++++++++ .../service/ReceivedCommentsServiceTest.java | 67 +++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 src/test/java/BookPick/mvp/domain/comment/dto/read/CommentDetailResTest.java create mode 100644 src/test/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTOTest.java create mode 100644 src/test/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateResTest.java create mode 100644 src/test/java/BookPick/mvp/domain/comment/service/PagenationServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/comment/service/ReceivedCommentsServiceTest.java diff --git a/src/test/java/BookPick/mvp/domain/comment/dto/read/CommentDetailResTest.java b/src/test/java/BookPick/mvp/domain/comment/dto/read/CommentDetailResTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTOTest.java b/src/test/java/BookPick/mvp/domain/comment/dto/read/ReceivedCommentsDTOTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateResTest.java b/src/test/java/BookPick/mvp/domain/comment/dto/update/CommentUpdateResTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/comment/service/PagenationServiceTest.java b/src/test/java/BookPick/mvp/domain/comment/service/PagenationServiceTest.java new file mode 100644 index 0000000..529caa0 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/comment/service/PagenationServiceTest.java @@ -0,0 +1,46 @@ +package BookPick.mvp.domain.comment.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +@DisplayName("페이지네이션 서비스 테스트") +class PagenationServiceTest { + + @InjectMocks + private PagenationService pagenationService; + + @Test + @DisplayName("음수 페이지 번호를 0으로 변환") + void changeMinusPageToZeroPage_minusPage() { + // given + int minusPage = -1; + + // when + int result = pagenationService.changeMinusPageToZeroPage(minusPage); + + // then + assertThat(result).isZero(); + } + + @Test + @DisplayName("0 또는 양수 페이지 번호는 그대로 유지") + void changeMinusPageToZeroPage_notMinusPage() { + // given + int zeroPage = 0; + int positivePage = 10; + + // when + int zeroResult = pagenationService.changeMinusPageToZeroPage(zeroPage); + int positiveResult = pagenationService.changeMinusPageToZeroPage(positivePage); + + // then + assertThat(zeroResult).isEqualTo(zeroPage); + assertThat(positiveResult).isEqualTo(positivePage); + } +} diff --git a/src/test/java/BookPick/mvp/domain/comment/service/ReceivedCommentsServiceTest.java b/src/test/java/BookPick/mvp/domain/comment/service/ReceivedCommentsServiceTest.java new file mode 100644 index 0000000..2190b15 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/comment/service/ReceivedCommentsServiceTest.java @@ -0,0 +1,67 @@ +package BookPick.mvp.domain.comment.service; + +import BookPick.mvp.domain.comment.dto.read.ReceivedCommentsDTO; +import BookPick.mvp.domain.comment.entity.Comment; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.comment.repository.CommentRepository; +import BookPick.mvp.domain.user.entity.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("받은 댓글 서비스 테스트") +class ReceivedCommentsServiceTest { + + @InjectMocks + private ReceivedCommentsService receivedCommentsService; + + @Mock + private CommentRepository commentRepository; + + @Test + @DisplayName("사용자가 받은 최신 댓글 3개 조회") + void receivedCommentsRead_success() { + // given + Long userId = 1L; + User user = User.builder().id(userId).build(); + Curation curation = Curation.builder().id(1L).build(); // Mock Curation + Comment comment1 = Comment.builder().id(1L).content("Comment 1").user(user).curation(curation).build(); + Comment comment2 = Comment.builder().id(2L).content("Comment 2").user(user).curation(curation).build(); + Comment comment3 = Comment.builder().id(3L).content("Comment 3").user(user).curation(curation).build(); + List comments = List.of(comment1, comment2, comment3); + + when(commentRepository.findLatestCommentsByUserId(userId, PageRequest.of(0, 3))).thenReturn(comments); + + // when + ReceivedCommentsDTO result = receivedCommentsService.receivedCommentsRead(userId); + + // then + assertThat(result.comments()).hasSize(3); + assertThat(result.comments().get(0).content()).isEqualTo("Comment 1"); + } + + @Test + @DisplayName("받은 댓글이 없을 경우 빈 리스트 반환") + void receivedCommentsRead_noComments() { + // given + Long userId = 1L; + when(commentRepository.findLatestCommentsByUserId(userId, PageRequest.of(0, 3))).thenReturn(Collections.emptyList()); + + // when + ReceivedCommentsDTO result = receivedCommentsService.receivedCommentsRead(userId); + + // then + assertThat(result.comments()).isEmpty(); + } +} From b5d02f636db20bd378154c14460793cdbe094899 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 16:44:04 +0900 Subject: [PATCH 269/291] =?UTF-8?q?feat(Curation):=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20ENUM,=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9C=A0=ED=8B=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/enums/common/SortTypeTest.java | 0 .../service/CurationDeleteServiceTest.java | 355 ++++++++++++++++ .../service/CurationReadServiceTest.java | 299 ++++++++++++++ .../service/CurationUpdateServiceTest.java | 381 ++++++++++++++++++ .../converter/StringListConverterTest.java | 0 .../gemini/service/GeminiServiceTest.java | 0 .../list/Handler/CurationPageHandlerTest.java | 93 +++++ .../list/fetcher/CurationFetcherTest.java | 0 .../CurationMatchResultPaginationTest.java | 101 +++++ 9 files changed, 1229 insertions(+) create mode 100644 src/test/java/BookPick/mvp/domain/curation/enums/common/SortTypeTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/service/CurationDeleteServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/service/CurationReadServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/service/CurationUpdateServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/util/converter/StringListConverterTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiServiceTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandlerTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcherTest.java create mode 100644 src/test/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPaginationTest.java diff --git a/src/test/java/BookPick/mvp/domain/curation/enums/common/SortTypeTest.java b/src/test/java/BookPick/mvp/domain/curation/enums/common/SortTypeTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/curation/service/CurationDeleteServiceTest.java b/src/test/java/BookPick/mvp/domain/curation/service/CurationDeleteServiceTest.java new file mode 100644 index 0000000..858cf21 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/service/CurationDeleteServiceTest.java @@ -0,0 +1,355 @@ +package BookPick.mvp.domain.curation.service; + +import BookPick.mvp.domain.curation.dto.base.delete.CurationDeleteRes; +import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteReq; +import BookPick.mvp.domain.curation.dto.base.delete.CurationListDeleteRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.curation.service.base.delete.CurationDeleteService; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("큐레이션 삭제 서비스 테스트") +class CurationDeleteServiceTest { + + @InjectMocks + private CurationDeleteService curationDeleteService; + + @Mock + private CurationRepository curationRepository; + + @Mock + private CurationLikeRepository curationLikeRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationSubscribeService curationSubscribeService; + + @Test + @DisplayName("큐레이션 단건 삭제 성공") + void removeCuration_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("삭제할 큐레이션") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + doNothing().when(curationRepository).delete(mockCuration); + + // when + CurationDeleteRes result = curationDeleteService.removeCuration(userId, curationId); + + // then + assertThat(result).isNotNull(); + assertThat(result.curationIds()).isEqualTo(curationId); + assertThat(result.deletedAt()).isNotNull(); + + verify(curationRepository).findById(curationId); + verify(curationRepository).delete(mockCuration); + } + + @Test + @DisplayName("큐레이션 단건 삭제 - 다른 사용자가 삭제 시도 시 예외 발생") + void removeCuration_notOwner_fail() { + // given + Long ownerId = 1L; + Long attackerId = 2L; + Long curationId = 1L; + + User owner = User.builder() + .id(ownerId) + .email("owner@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(owner) + .title("큐레이션") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when & then + assertThatThrownBy(() -> curationDeleteService.removeCuration(attackerId, curationId)) + .isInstanceOf(CurationAccessDeniedException.class); + + verify(curationRepository).findById(curationId); + verify(curationRepository, never()).delete(any(Curation.class)); + } + + @Test + @DisplayName("큐레이션 단건 삭제 - 존재하지 않는 큐레이션") + void removeCuration_notFound_fail() { + // given + Long userId = 1L; + Long curationId = 999L; + + when(curationRepository.findById(curationId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> curationDeleteService.removeCuration(userId, curationId)) + .isInstanceOf(CurationNotFoundException.class); + + verify(curationRepository).findById(curationId); + verify(curationRepository, never()).delete(any(Curation.class)); + } + + @Test + @DisplayName("큐레이션 복수 삭제 성공") + void removeCurations_success() { + // given + Long userId = 1L; + List curationIds = List.of(1L, 2L, 3L); + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation curation1 = Curation.builder() + .id(1L) + .user(mockUser) + .title("큐레이션 1") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + Curation curation2 = Curation.builder() + .id(2L) + .user(mockUser) + .title("큐레이션 2") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + Curation curation3 = Curation.builder() + .id(3L) + .user(mockUser) + .title("큐레이션 3") + .isDrafted(true) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + List curations = List.of(curation1, curation2, curation3); + + CurationListDeleteReq req = new CurationListDeleteReq(curationIds); + + when(curationRepository.findByIdIn(curationIds)).thenReturn(curations); + doNothing().when(curationRepository).delete(any(Curation.class)); + + // when + CurationListDeleteRes result = curationDeleteService.removeCurations(userId, req); + + // then + assertThat(result).isNotNull(); + assertThat(result.ids()).hasSize(3); + assertThat(result.ids()).containsExactlyInAnyOrder(1L, 2L, 3L); + assertThat(result.deletedAt()).isNotNull(); + + verify(curationRepository).findByIdIn(curationIds); + verify(curationRepository, times(3)).delete(any(Curation.class)); + } + + @Test + @DisplayName("큐레이션 복수 삭제 - 일부 큐레이션이 존재하지 않음") + void removeCurations_partialNotFound_fail() { + // given + Long userId = 1L; + List requestedIds = List.of(1L, 2L, 3L); + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + // 2개만 존재 (3번은 없음) + Curation curation1 = Curation.builder() + .id(1L) + .user(mockUser) + .title("큐레이션 1") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + Curation curation2 = Curation.builder() + .id(2L) + .user(mockUser) + .title("큐레이션 2") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + List foundCurations = List.of(curation1, curation2); + + CurationListDeleteReq req = new CurationListDeleteReq(requestedIds); + + when(curationRepository.findByIdIn(requestedIds)).thenReturn(foundCurations); + + // when & then + assertThatThrownBy(() -> curationDeleteService.removeCurations(userId, req)) + .isInstanceOf(CurationNotFoundException.class); + + verify(curationRepository).findByIdIn(requestedIds); + verify(curationRepository, never()).delete(any(Curation.class)); + } + + @Test + @DisplayName("큐레이션 복수 삭제 - 다른 사용자의 큐레이션 포함") + void removeCurations_containsOthersPost_fail() { + // given + Long userId = 1L; + Long otherUserId = 2L; + List curationIds = List.of(1L, 2L); + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + User otherUser = User.builder() + .id(otherUserId) + .email("other@test.com") + .build(); + + Curation myCuration = Curation.builder() + .id(1L) + .user(mockUser) + .title("내 큐레이션") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + Curation otherCuration = Curation.builder() + .id(2L) + .user(otherUser) + .title("다른 사람 큐레이션") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + List curations = List.of(myCuration, otherCuration); + + CurationListDeleteReq req = new CurationListDeleteReq(curationIds); + + when(curationRepository.findByIdIn(curationIds)).thenReturn(curations); + + // when & then + assertThatThrownBy(() -> curationDeleteService.removeCurations(userId, req)) + .isInstanceOf(CurationAccessDeniedException.class); + + verify(curationRepository).findByIdIn(curationIds); + // 첫 번째 큐레이션은 삭제되지만, 두 번째에서 예외 발생하므로 1번만 호출됨 + verify(curationRepository).delete(myCuration); + verify(curationRepository, times(1)).delete(any(Curation.class)); + } + + @Test + @DisplayName("큐레이션 복수 삭제 - 빈 리스트") + void removeCurations_emptyList_success() { + // given + Long userId = 1L; + List emptyIds = List.of(); + + CurationListDeleteReq req = new CurationListDeleteReq(emptyIds); + + when(curationRepository.findByIdIn(emptyIds)).thenReturn(List.of()); + + // when + CurationListDeleteRes result = curationDeleteService.removeCurations(userId, req); + + // then + assertThat(result).isNotNull(); + assertThat(result.ids()).isEmpty(); + + verify(curationRepository).findByIdIn(emptyIds); + verify(curationRepository, never()).delete(any(Curation.class)); + } +} diff --git a/src/test/java/BookPick/mvp/domain/curation/service/CurationReadServiceTest.java b/src/test/java/BookPick/mvp/domain/curation/service/CurationReadServiceTest.java new file mode 100644 index 0000000..d3b8a8e --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/service/CurationReadServiceTest.java @@ -0,0 +1,299 @@ +package BookPick.mvp.domain.curation.service; + +import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.get.one.CurationGetRes; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.entity.CurationLike; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.curation.service.base.read.CurationReadService; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("큐레이션 조회 서비스 테스트") +class CurationReadServiceTest { + + @InjectMocks + private CurationReadService curationReadService; + + @Mock + private CurationRepository curationRepository; + + @Mock + private CurationLikeRepository curationLikeRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationSubscribeService curationSubscribeService; + + @Mock + private HttpServletRequest request; + + @Test + @DisplayName("큐레이션 조회 성공 - 비로그인 사용자") + void findCuration_success_notLoggedIn() { + // given + Long curationId = 1L; + User mockUser = User.builder() + .id(1L) + .email("test@test.com") + .nickname("테스터") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("테스트 큐레이션") + .isDrafted(false) + .viewCount(0) + .likeCount(0) + .commentCount(0) + .moods(List.of("감동적인")) + .genres(List.of("소설")) + .keywords(List.of("사랑")) + .styles(List.of("감성적인")) + .build(); + + when(curationRepository.findByIdWithUserAndLock(curationId)).thenReturn(Optional.of(mockCuration)); + + // when + CurationGetRes result = curationReadService.findCuration(curationId, null, request, false); + + // then + assertThat(result).isNotNull(); + assertThat(result.id()).isEqualTo(curationId); + assertThat(result.title()).isEqualTo("테스트 큐레이션"); + assertThat(result.viewCount()).isEqualTo(1); // 조회수 증가 확인 + assertThat(result.isLiked()).isFalse(); + assertThat(result.subscribed()).isFalse(); + + verify(curationRepository).findByIdWithUserAndLock(curationId); + verify(curationLikeRepository, never()).findByUserIdAndCurationId(any(), any()); + } + + @Test + @DisplayName("큐레이션 조회 성공 - 로그인 사용자, 좋아요 O, 구독 O") + void findCuration_success_loggedIn_likedAndSubscribed() { + // given + Long curationId = 1L; + Long userId = 2L; + + User curator = User.builder() + .id(1L) + .email("curator@test.com") + .nickname("큐레이터") + .build(); + + User viewer = User.builder() + .id(userId) + .email("viewer@test.com") + .nickname("뷰어") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(curator) + .title("테스트 큐레이션") + .isDrafted(false) + .viewCount(5) + .likeCount(10) + .commentCount(3) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CurationLike mockLike = new CurationLike(); + + CustomUserDetails userDetails = mock(CustomUserDetails.class); + when(userDetails.getId()).thenReturn(userId); + + when(curationRepository.findByIdWithUserAndLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)).thenReturn(Optional.of(mockLike)); + when(curationSubscribeService.isSubscribeCurator(userId, curator.getId())).thenReturn(true); + + // when + CurationGetRes result = curationReadService.findCuration(curationId, userDetails, request, false); + + // then + assertThat(result).isNotNull(); + assertThat(result.id()).isEqualTo(curationId); + assertThat(result.viewCount()).isEqualTo(6); // 조회수 증가 확인 + assertThat(result.isLiked()).isTrue(); // 좋아요 확인 + assertThat(result.subscribed()).isTrue(); // 구독 확인 + + verify(curationRepository).findByIdWithUserAndLock(curationId); + verify(curationLikeRepository).findByUserIdAndCurationId(userId, curationId); + verify(curationSubscribeService).isSubscribeCurator(userId, curator.getId()); + } + + @Test + @DisplayName("큐레이션 조회 성공 - 로그인 사용자, 좋아요 X, 구독 X") + void findCuration_success_loggedIn_notLikedAndNotSubscribed() { + // given + Long curationId = 1L; + Long userId = 2L; + + User curator = User.builder() + .id(1L) + .email("curator@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(curator) + .title("테스트 큐레이션") + .isDrafted(false) + .viewCount(0) + .likeCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CustomUserDetails userDetails = mock(CustomUserDetails.class); + when(userDetails.getId()).thenReturn(userId); + + when(curationRepository.findByIdWithUserAndLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)).thenReturn(Optional.empty()); + when(curationSubscribeService.isSubscribeCurator(userId, curator.getId())).thenReturn(false); + + // when + CurationGetRes result = curationReadService.findCuration(curationId, userDetails, request, false); + + // then + assertThat(result.isLiked()).isFalse(); + assertThat(result.subscribed()).isFalse(); + + verify(curationLikeRepository).findByUserIdAndCurationId(userId, curationId); + verify(curationSubscribeService).isSubscribeCurator(userId, curator.getId()); + } + + @Test + @DisplayName("큐레이션 수정용 조회 성공 - 작성자") + void findCuration_editMode_success_owner() { + // given + Long curationId = 1L; + Long userId = 1L; + + User owner = User.builder() + .id(userId) + .email("owner@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(owner) + .title("테스트 큐레이션") + .isDrafted(true) + .viewCount(0) + .likeCount(0) + .commentCount(0) + .bookTitle("책 제목") + .bookAuthor("작가") + .bookIsbn("1234567890") + .bookImageUrl("book.jpg") + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CustomUserDetails userDetails = mock(CustomUserDetails.class); + when(userDetails.getId()).thenReturn(userId); + + when(curationRepository.findByIdWithUserAndLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(curationLikeRepository.findByUserIdAndCurationId(userId, curationId)).thenReturn(Optional.empty()); + when(curationSubscribeService.isSubscribeCurator(userId, owner.getId())).thenReturn(false); + + // when + CurationGetRes result = curationReadService.findCuration(curationId, userDetails, request, true); + + // then + assertThat(result).isNotNull(); + assertThat(result.book()).isNotNull(); // 작성자는 책 정보 포함 + assertThat(result.book().title()).isEqualTo("책 제목"); + + verify(curationRepository).findByIdWithUserAndLock(curationId); + } + + @Test + @DisplayName("큐레이션 수정용 조회 실패 - 작성자가 아님") + void findCuration_editMode_fail_notOwner() { + // given + Long curationId = 1L; + Long ownerId = 1L; + Long viewerId = 2L; + + User owner = User.builder() + .id(ownerId) + .email("owner@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(owner) + .title("테스트 큐레이션") + .isDrafted(false) + .viewCount(0) + .likeCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CustomUserDetails userDetails = mock(CustomUserDetails.class); + when(userDetails.getId()).thenReturn(viewerId); + + when(curationRepository.findByIdWithUserAndLock(curationId)).thenReturn(Optional.of(mockCuration)); + when(curationLikeRepository.findByUserIdAndCurationId(viewerId, curationId)).thenReturn(Optional.empty()); + when(curationSubscribeService.isSubscribeCurator(viewerId, ownerId)).thenReturn(false); + + // when & then + assertThatThrownBy(() -> curationReadService.findCuration(curationId, userDetails, request, true)) + .isInstanceOf(CurationAccessDeniedException.class); + + verify(curationRepository).findByIdWithUserAndLock(curationId); + } + + @Test + @DisplayName("존재하지 않는 큐레이션 조회 - 예외 발생") + void findCuration_notFound() { + // given + Long curationId = 999L; + + when(curationRepository.findByIdWithUserAndLock(curationId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> curationReadService.findCuration(curationId, null, request, false)) + .isInstanceOf(CurationNotFoundException.class); + + verify(curationRepository).findByIdWithUserAndLock(curationId); + verify(curationLikeRepository, never()).findByUserIdAndCurationId(any(), any()); + } +} diff --git a/src/test/java/BookPick/mvp/domain/curation/service/CurationUpdateServiceTest.java b/src/test/java/BookPick/mvp/domain/curation/service/CurationUpdateServiceTest.java new file mode 100644 index 0000000..f875d8d --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/service/CurationUpdateServiceTest.java @@ -0,0 +1,381 @@ +package BookPick.mvp.domain.curation.service; + +import BookPick.mvp.domain.curation.dto.base.create.ETC.BookDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.RecommendDto; +import BookPick.mvp.domain.curation.dto.base.create.ETC.ThumbnailDto; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; +import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateResult; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.exception.common.CurationAccessDeniedException; +import BookPick.mvp.domain.curation.exception.common.CurationAlreadyPublishedException; +import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; +import BookPick.mvp.domain.curation.repository.CurationRepository; +import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; +import BookPick.mvp.domain.curation.service.base.update.CurationUpdateService; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.service.subscribe.CurationSubscribeService; +import BookPick.mvp.global.api.SuccessCode.SuccessCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("큐레이션 수정 서비스 테스트") +class CurationUpdateServiceTest { + + @InjectMocks + private CurationUpdateService curationUpdateService; + + @Mock + private CurationRepository curationRepository; + + @Mock + private CurationLikeRepository curationLikeRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationSubscribeService curationSubscribeService; + + @Test + @DisplayName("임시저장 -> 임시저장 수정 성공") + void updateCuration_draftToDraft_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("기존 제목") + .isDrafted(true) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CurationUpdateReq req = new CurationUpdateReq( + "수정된 제목", + new ThumbnailDto("new-thumb.jpg", "#000000"), + new BookDto("새 책", "새 작가", "9876543210", "new-book.jpg"), + "수정된 리뷰", + new RecommendDto(List.of("슬픈"), List.of("에세이"), List.of("이별"), List.of("담백한")), + true // isDrafted = true + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when + CurationUpdateResult result = curationUpdateService.updateCuration(userId, curationId, req); + + // then + assertThat(result).isNotNull(); + assertThat(result.successCode()).isEqualTo(SuccessCode.CURATION_DRAFT_UPDATE_SUCCESS); + assertThat(result.curationUpdateRes().id()).isEqualTo(curationId); + assertThat(mockCuration.getTitle()).isEqualTo("수정된 제목"); + assertThat(mockCuration.getIsDrafted()).isTrue(); + + verify(curationRepository).findById(curationId); + } + + @Test + @DisplayName("임시저장 -> 발행 성공") + void updateCuration_draftToPublished_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("임시저장 제목") + .isDrafted(true) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CurationUpdateReq req = new CurationUpdateReq( + "발행할 제목", + new ThumbnailDto("thumb.jpg", "#FFFFFF"), + new BookDto("책", "작가", "123", "book.jpg"), + "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + false // isDrafted = false (발행) + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when + CurationUpdateResult result = curationUpdateService.updateCuration(userId, curationId, req); + + // then + assertThat(result).isNotNull(); + assertThat(result.successCode()).isEqualTo(SuccessCode.DRAFTED_CURATION_PUBLISH_SUCCESS); + assertThat(mockCuration.getIsDrafted()).isFalse(); + assertThat(mockCuration.getPublishedAt()).isNotNull(); + + verify(curationRepository).findById(curationId); + } + + @Test + @DisplayName("발행본 -> 발행본 수정 성공") + void updateCuration_publishedToPublished_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("발행된 제목") + .isDrafted(false) + .likeCount(5) + .viewCount(100) + .commentCount(10) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CurationUpdateReq req = new CurationUpdateReq( + "수정된 발행 제목", + new ThumbnailDto("thumb.jpg", "#FFFFFF"), + new BookDto("책", "작가", "123", "book.jpg"), + "수정된 리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + false // isDrafted = false + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when + CurationUpdateResult result = curationUpdateService.updateCuration(userId, curationId, req); + + // then + assertThat(result).isNotNull(); + assertThat(result.successCode()).isEqualTo(SuccessCode.CURATION_UPDATE_SUCCESS); + assertThat(mockCuration.getTitle()).isEqualTo("수정된 발행 제목"); + assertThat(mockCuration.getIsDrafted()).isFalse(); + + verify(curationRepository).findById(curationId); + } + + @Test + @DisplayName("발행본 -> 임시저장 변경 시도 - 예외 발생") + void updateCuration_publishedToDraft_fail() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .email("test@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("발행된 제목") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CurationUpdateReq req = new CurationUpdateReq( + "제목", + new ThumbnailDto("thumb.jpg", "#FFFFFF"), + new BookDto("책", "작가", "123", "book.jpg"), + "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + true // isDrafted = true (임시저장으로 변경 시도) + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when & then + assertThatThrownBy(() -> curationUpdateService.updateCuration(userId, curationId, req)) + .isInstanceOf(CurationAlreadyPublishedException.class); + + verify(curationRepository).findById(curationId); + } + + @Test + @DisplayName("다른 사용자가 수정 시도 - 예외 발생") + void updateCuration_notOwner_fail() { + // given + Long ownerId = 1L; + Long attackerId = 2L; + Long curationId = 1L; + + User owner = User.builder() + .id(ownerId) + .email("owner@test.com") + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(owner) + .title("제목") + .isDrafted(true) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of()) + .genres(List.of()) + .keywords(List.of()) + .styles(List.of()) + .build(); + + CurationUpdateReq req = new CurationUpdateReq( + "해킹 시도", + new ThumbnailDto("thumb.jpg", "#FFFFFF"), + new BookDto("책", "작가", "123", "book.jpg"), + "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + true + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when & then + assertThatThrownBy(() -> curationUpdateService.updateCuration(attackerId, curationId, req)) + .isInstanceOf(CurationAccessDeniedException.class); + + verify(curationRepository).findById(curationId); + } + + @Test + @DisplayName("존재하지 않는 큐레이션 수정 - 예외 발생") + void updateCuration_notFound_fail() { + // given + Long userId = 1L; + Long curationId = 999L; + + CurationUpdateReq req = new CurationUpdateReq( + "제목", + new ThumbnailDto("thumb.jpg", "#FFFFFF"), + new BookDto("책", "작가", "123", "book.jpg"), + "리뷰", + new RecommendDto(List.of(), List.of(), List.of(), List.of()), + false + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> curationUpdateService.updateCuration(userId, curationId, req)) + .isInstanceOf(CurationNotFoundException.class); + + verify(curationRepository).findById(curationId); + } + + @Test + @DisplayName("큐레이션 업데이트 시 내용 변경 확인") + void updateCuration_contentChange_success() { + // given + Long userId = 1L; + Long curationId = 1L; + + User mockUser = User.builder() + .id(userId) + .build(); + + Curation mockCuration = Curation.builder() + .id(curationId) + .user(mockUser) + .title("기존 제목") + .thumbnailUrl("old-thumb.jpg") + .thumbnailColor("#FFFFFF") + .bookTitle("기존 책") + .bookAuthor("기존 작가") + .bookIsbn("111") + .bookImageUrl("old-book.jpg") + .review("기존 리뷰") + .isDrafted(false) + .likeCount(0) + .viewCount(0) + .commentCount(0) + .moods(List.of("기쁜")) + .genres(List.of("소설")) + .keywords(List.of("사랑")) + .styles(List.of("감성적인")) + .build(); + + CurationUpdateReq req = new CurationUpdateReq( + "새 제목", + new ThumbnailDto("new-thumb.jpg", "#000000"), + new BookDto("새 책", "새 작가", "222", "new-book.jpg"), + "새 리뷰", + new RecommendDto( + List.of("슬픈"), + List.of("에세이"), + List.of("이별"), + List.of("담백한") + ), + false + ); + + when(curationRepository.findById(curationId)).thenReturn(Optional.of(mockCuration)); + + // when + CurationUpdateResult result = curationUpdateService.updateCuration(userId, curationId, req); + + // then + assertThat(mockCuration.getTitle()).isEqualTo("새 제목"); + assertThat(mockCuration.getThumbnailUrl()).isEqualTo("new-thumb.jpg"); + assertThat(mockCuration.getThumbnailColor()).isEqualTo("#000000"); + assertThat(mockCuration.getBookTitle()).isEqualTo("새 책"); + assertThat(mockCuration.getBookAuthor()).isEqualTo("새 작가"); + assertThat(mockCuration.getBookIsbn()).isEqualTo("222"); + assertThat(mockCuration.getBookImageUrl()).isEqualTo("new-book.jpg"); + assertThat(mockCuration.getReview()).isEqualTo("새 리뷰"); + assertThat(mockCuration.getMoods()).containsExactly("슬픈"); + assertThat(mockCuration.getGenres()).containsExactly("에세이"); + assertThat(mockCuration.getKeywords()).containsExactly("이별"); + assertThat(mockCuration.getStyles()).containsExactly("담백한"); + + verify(curationRepository).findById(curationId); + } +} diff --git a/src/test/java/BookPick/mvp/domain/curation/util/converter/StringListConverterTest.java b/src/test/java/BookPick/mvp/domain/curation/util/converter/StringListConverterTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiServiceTest.java b/src/test/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiServiceTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandlerTest.java b/src/test/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandlerTest.java new file mode 100644 index 0000000..3c94319 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/util/list/Handler/CurationPageHandlerTest.java @@ -0,0 +1,93 @@ +package BookPick.mvp.domain.curation.util.list.Handler; + +import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; +import BookPick.mvp.domain.curation.dto.base.get.list.CursorPage; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.util.list.fetcher.CurationFetcher; +import BookPick.mvp.domain.user.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("CurationPageHandler 테스트") +class CurationPageHandlerTest { + + @InjectMocks + private CurationPageHandler curationPageHandler; + + @Mock + private CurationFetcher curationFetcher; + + @Test + @DisplayName("다음 페이지가 있는 경우 커서 페이지 생성") + void createCursorPage_withNextPage() { + // given + List curations = IntStream.range(0, 11) + .mapToObj(i -> Curation.builder().id((long) i).build()) + .collect(Collectors.toList()); + int size = 10; + when(curationFetcher.calculateNextCursor(curations, size, true)).thenReturn(10L); + + // when + CursorPage cursorPage = curationPageHandler.createCursorPage(curations, size); + + // then + assertThat(cursorPage.isHasNext()).isTrue(); + assertThat(cursorPage.getContent()).hasSize(10); + assertThat(cursorPage.getNextCursor()).isEqualTo(10L); + } + + @Test + @DisplayName("다음 페이지가 없는 경우 커서 페이지 생성") + void createCursorPage_withoutNextPage() { + // given + List curations = IntStream.range(0, 5) + .mapToObj(i -> Curation.builder().id((long) i).build()) + .collect(Collectors.toList()); + int size = 10; + when(curationFetcher.calculateNextCursor(curations, size, false)).thenReturn(null); + + // when + CursorPage cursorPage = curationPageHandler.createCursorPage(curations, size); + + // then + assertThat(cursorPage.isHasNext()).isFalse(); + assertThat(cursorPage.getContent()).hasSize(5); + assertThat(cursorPage.getNextCursor()).isNull(); + } + + @Test + @DisplayName("큐레이션 리스트를 DTO로 변환") + void convertToContentRes_conversion() { + // given + User user = User.builder().id(1L).build(); + Curation c1 = Curation.builder().id(1L).user(user).title("title1").build(); + Curation c2 = Curation.builder().id(2L).user(user).title("title2").build(); + Curation c3 = Curation.builder().id(3L).user(user).title("title3").build(); + List curations = List.of(c1, c2, c3); + Set likedIds = Set.of(1L, 3L); + + // when + List res = curationPageHandler.convertToContentRes(curations, likedIds); + + // then + assertThat(res).hasSize(3); + assertThat(res.get(0).isLiked()).isTrue(); + assertThat(res.get(1).isLiked()).isFalse(); + assertThat(res.get(2).isLiked()).isTrue(); + } +} diff --git a/src/test/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcherTest.java b/src/test/java/BookPick/mvp/domain/curation/util/list/fetcher/CurationFetcherTest.java new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPaginationTest.java b/src/test/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPaginationTest.java new file mode 100644 index 0000000..a2ab5af --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/curation/util/list/similarity/CurationMatchResultPaginationTest.java @@ -0,0 +1,101 @@ +package BookPick.mvp.domain.curation.util.list.similarity; + +import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; +import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.user.entity.User; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("CurationMatchResultPagination 테스트") +class CurationMatchResultPaginationTest { + + @Test + @DisplayName("커서 기반 페이지네이션") + void paginate_withCursor() { + // given + User dummyUser = User.builder().id(99L).build(); // Dummy user for CurationMatchResult builder + List results = IntStream.range(0, 20) + .mapToObj(i -> CurationMatchResult.builder() + .curation(Curation.builder().id((long) i).build()) + .user(dummyUser) + .totalMatchCount(i) + .matchedMood("mood") + .matchedGenre("genre") + .matchedKeyword("keyword") + .matchedStyle("style") + .matched("matched") + .build()) + .collect(Collectors.toList()); + Pageable pageable = PageRequest.of(0, 5); + Long cursor = 10L; + + // when + List paginated = CurationMatchResultPagination.paginate(results, cursor, pageable); + + // then + assertThat(paginated).hasSize(5); + assertThat(paginated.get(0).getCuration().getId()).isEqualTo(10L); + } + + @Test + @DisplayName("커서가 null인 경우") + void paginate_nullCursor() { + // given + User dummyUser = User.builder().id(99L).build(); // Dummy user for CurationMatchResult builder + List results = IntStream.range(0, 20) + .mapToObj(i -> CurationMatchResult.builder() + .curation(Curation.builder().id((long) i).build()) + .user(dummyUser) + .totalMatchCount(i) + .matchedMood("mood") + .matchedGenre("genre") + .matchedKeyword("keyword") + .matchedStyle("style") + .matched("matched") + .build()) + .collect(Collectors.toList()); + Pageable pageable = PageRequest.of(0, 5); + + // when + List paginated = CurationMatchResultPagination.paginate(results, null, pageable); + + // then + assertThat(paginated).hasSize(5); + assertThat(paginated.get(0).getCuration().getId()).isEqualTo(0L); + } + + @Test + @DisplayName("커서가 범위를 벗어나는 경우") + void paginate_cursorOutOfBounds() { + // given + User dummyUser = User.builder().id(99L).build(); // Dummy user for CurationMatchResult builder + List results = IntStream.range(0, 10) + .mapToObj(i -> CurationMatchResult.builder() + .curation(Curation.builder().id((long) i).build()) + .user(dummyUser) + .totalMatchCount(i) + .matchedMood("mood") + .matchedGenre("genre") + .matchedKeyword("keyword") + .matchedStyle("style") + .matched("matched") + .build()) + .collect(Collectors.toList()); + Pageable pageable = PageRequest.of(0, 5); + Long cursor = 15L; + + // when + List paginated = CurationMatchResultPagination.paginate(results, cursor, pageable); + + // then + assertThat(paginated).isEmpty(); + } +} From 310939b848611224b6488169b070a760955314e7 Mon Sep 17 00:00:00 2001 From: halo Date: Sat, 3 Jan 2026 16:44:10 +0900 Subject: [PATCH 270/291] =?UTF-8?q?feat(User):=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B5=AC=EB=8F=85=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CurationSubscribeServiceTest.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/test/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeServiceTest.java diff --git a/src/test/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeServiceTest.java b/src/test/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeServiceTest.java new file mode 100644 index 0000000..59df197 --- /dev/null +++ b/src/test/java/BookPick/mvp/domain/user/service/subscribe/CurationSubscribeServiceTest.java @@ -0,0 +1,141 @@ +package BookPick.mvp.domain.user.service.subscribe; + +import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeReq; +import BookPick.mvp.domain.user.dto.subscribe.CuratorSubscribeRes; +import BookPick.mvp.domain.user.dto.subscribe.SubscribedCuratorPageRes; +import BookPick.mvp.domain.user.entity.CuratorSubscribe; +import BookPick.mvp.domain.user.entity.User; +import BookPick.mvp.domain.user.exception.curator.CuratorNotFoundException; +import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.repository.UserRepository; +import BookPick.mvp.domain.user.repository.subscribe.CurationSubscribeRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@DisplayName("큐레이터 구독 서비스 테스트") +class CurationSubscribeServiceTest { + + @InjectMocks + private CurationSubscribeService curationSubscribeService; + + @Mock + private UserRepository userRepository; + + @Mock + private CurationSubscribeRepository curationSubscribeRepository; + + @Test + @DisplayName("큐레이터 구독 성공") + void subscribe_success_newSubscription() { + // given + Long userId = 1L; + Long curatorId = 2L; + CuratorSubscribeReq req = new CuratorSubscribeReq(curatorId); + User user = User.builder().id(userId).build(); + User curator = User.builder().id(curatorId).nickname("curator").build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.findById(curatorId)).thenReturn(Optional.of(curator)); + when(curationSubscribeRepository.findByUserIdAndCuratorId(userId, curatorId)).thenReturn(Optional.empty()); + + // when + CuratorSubscribeRes res = curationSubscribeService.subscribe(userId, req); + + // then + assertThat(res.subscribed()).isTrue(); + assertThat(res.curatorId()).isEqualTo(curatorId); + verify(curationSubscribeRepository).save(any(CuratorSubscribe.class)); + } + + @Test + @DisplayName("큐레이터 구독 취소 성공") + void subscribe_success_cancelSubscription() { + // given + Long userId = 1L; + Long curatorId = 2L; + CuratorSubscribeReq req = new CuratorSubscribeReq(curatorId); + User user = User.builder().id(userId).build(); + User curator = User.builder().id(curatorId).nickname("curator").build(); + CuratorSubscribe existingSubscription = CuratorSubscribe.builder().user(user).curator(curator).build(); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userRepository.findById(curatorId)).thenReturn(Optional.of(curator)); + when(curationSubscribeRepository.findByUserIdAndCuratorId(userId, curatorId)).thenReturn(Optional.of(existingSubscription)); + + // when + CuratorSubscribeRes res = curationSubscribeService.subscribe(userId, req); + + // then + assertThat(res.subscribed()).isFalse(); + verify(curationSubscribeRepository).delete(existingSubscription); + } + + @Test + @DisplayName("구독 여부 확인 - 구독 중") + void isSubscribeCurator_true() { + // given + Long userId = 1L; + Long curatorId = 2L; + when(curationSubscribeRepository.findByUserIdAndCuratorId(userId, curatorId)).thenReturn(Optional.of(new CuratorSubscribe())); + + // when + boolean isSubscribed = curationSubscribeService.isSubscribeCurator(userId, curatorId); + + // then + assertThat(isSubscribed).isTrue(); + } + + @Test + @DisplayName("구독 여부 확인 - 구독 안함") + void isSubscribeCurator_false() { + // given + Long userId = 1L; + Long curatorId = 2L; + when(curationSubscribeRepository.findByUserIdAndCuratorId(userId, curatorId)).thenReturn(Optional.empty()); + + // when + boolean isSubscribed = curationSubscribeService.isSubscribeCurator(userId, curatorId); + + // then + assertThat(isSubscribed).isFalse(); + } + + @Test + @DisplayName("구독한 큐레이터 목록 조회") + void getSubscribedCurators_success() { + // given + Long userId = 1L; + Pageable pageable = PageRequest.of(0, 10); + User curatorUser = User.builder().id(2L).nickname("curator1").build(); + CuratorSubscribe subscription = CuratorSubscribe.builder().curator(curatorUser).build(); + Page page = new PageImpl<>(Collections.singletonList(subscription), pageable, 1); + + when(curationSubscribeRepository.findByUserIdOrderByIdDesc(userId, pageable)).thenReturn(page); + + // when + SubscribedCuratorPageRes res = curationSubscribeService.getSubscribedCurators(userId, 0, 10); + + // then + assertThat(res.curators()).hasSize(1); + assertThat(res.curators().get(0).nickname()).isEqualTo("curator1"); + assertThat(res.pageInfo().hasNext()).isFalse(); + } +} From f1e3066cf952e38df4b09824a3b7591459b04c40 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 6 Jan 2026 22:02:55 +0900 Subject: [PATCH 271/291] =?UTF-8?q?bugfix=20:=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EB=B9=84=EC=96=B4?= =?UTF-8?q?=EC=9E=88=EC=9C=BC=EB=A9=B4=20=EC=95=88=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/controller/base/UserController.java | 4 ++-- .../mvp/domain/user/enums/user/UserErrorCode.java | 1 + .../exception/profile/UserNameNotNullException.java | 10 ++++++++++ .../mvp/domain/user/service/base/UserService.java | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java index 7e73a79..161a9c7 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -6,7 +6,7 @@ //import BookPick.mvp.domain.user.dto.base.UserRes; //import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; //import BookPick.mvp.domain.user.enums.user.UserSuccessCode; -//import BookPick.mvp.domain.user.exception.common.NotHaveAdminRole; +//import BookPick.mvp.domain.user.exception.common.UserNameNotNullException; //import BookPick.mvp.domain.user.service.base.UserService; //import BookPick.mvp.domain.user.util.AdminManager; //import BookPick.mvp.global.api.ApiResponse; @@ -32,7 +32,7 @@ // public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, // @RequestBody @Valid UserReq req) { // if (adminManager.isAdmin(currentUser.getAuthorities())) { -// throw new NotHaveAdminRole(); +// throw new UserNameNotNullException(); // } // // UserRes res = userService.CreateUser(req); diff --git a/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java b/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java index 160e536..bf5e1ed 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java @@ -12,6 +12,7 @@ public enum UserErrorCode implements ErrorCodeInterface { // -- User -- User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 NOT_HAVE_ADMIN_ROLE(HttpStatus.BAD_REQUEST, "관리자 권한이 없는 유저입니다."), + USER_NAME_IS_NULL(HttpStatus.BAD_REQUEST, "닉네임은 비어있으면 안됩니다."), ALREADY_DELETE_USR(HttpStatus.CONFLICT, "이미 삭제한 유저입니다."), //409, 이미 삭제한 유저기때문에 conflict 충돌이라는 의미 PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "변경할 비밀번호와 확인 비밀번호가 일치하지 않습니다."), //409, 이미 삭제한 유저기때문에 conflict 충돌이라는 의미 WRONG_CURRENT_PASSWORD(HttpStatus.UNAUTHORIZED, "현재 비밀번호가 올바르지 않습니다."); diff --git a/src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java b/src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java new file mode 100644 index 0000000..9db3f3a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.user.exception.profile; + +import BookPick.mvp.domain.user.enums.user.UserErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class UserNameNotNullException extends BusinessException { + public UserNameNotNullException(){ + super(UserErrorCode.USER_NAME_IS_NULL); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java index b4f49b0..b5763e3 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java @@ -6,6 +6,7 @@ import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.common.AlreadyDeletedException; import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.exception.profile.UserNameNotNullException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -60,7 +61,10 @@ public UserRes userProfileUpdate(Long userId, UserReq req) { // 더티 체킹 if (req.email() != null) user.setEmail(req.email()); - if (req.nickName() != null) user.setNickname(req.nickName()); + if (req.nickName() == null) { + throw new UserNameNotNullException(); + } + user.setNickname(req.nickName()); if (req.profileImage() != null) user.setProfileImageUrl(req.profileImage()); if (req.introduction() != null) user.setBio(req.introduction()); From 771a86860345851e05d58bce52ccd841b62f3662 Mon Sep 17 00:00:00 2001 From: halo Date: Tue, 6 Jan 2026 22:02:55 +0900 Subject: [PATCH 272/291] =?UTF-8?q?bugfix=20:=20=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84=20=EC=88=98=EC=A0=95=EC=8B=9C=20=EB=B9=84=EC=96=B4?= =?UTF-8?q?=EC=9E=88=EC=9C=BC=EB=A9=B4=20=EC=95=88=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관련 커밋 : https://github.com/orgs/Book-Pick/projects/4/views/3?pane=issue&itemId=147963441&issue=Book-Pick%7Cbookpick-back%7C77 --- .../domain/user/controller/base/UserController.java | 4 ++-- .../mvp/domain/user/enums/user/UserErrorCode.java | 1 + .../exception/profile/UserNameNotNullException.java | 10 ++++++++++ .../mvp/domain/user/service/base/UserService.java | 6 +++++- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java diff --git a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java index 7e73a79..161a9c7 100644 --- a/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java +++ b/src/main/java/BookPick/mvp/domain/user/controller/base/UserController.java @@ -6,7 +6,7 @@ //import BookPick.mvp.domain.user.dto.base.UserRes; //import BookPick.mvp.domain.user.dto.base.delete.UserSoftDeleteRes; //import BookPick.mvp.domain.user.enums.user.UserSuccessCode; -//import BookPick.mvp.domain.user.exception.common.NotHaveAdminRole; +//import BookPick.mvp.domain.user.exception.common.UserNameNotNullException; //import BookPick.mvp.domain.user.service.base.UserService; //import BookPick.mvp.domain.user.util.AdminManager; //import BookPick.mvp.global.api.ApiResponse; @@ -32,7 +32,7 @@ // public ResponseEntity> createUser(@AuthenticationPrincipal CustomUserDetails currentUser, // @RequestBody @Valid UserReq req) { // if (adminManager.isAdmin(currentUser.getAuthorities())) { -// throw new NotHaveAdminRole(); +// throw new UserNameNotNullException(); // } // // UserRes res = userService.CreateUser(req); diff --git a/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java b/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java index 160e536..bf5e1ed 100644 --- a/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java +++ b/src/main/java/BookPick/mvp/domain/user/enums/user/UserErrorCode.java @@ -12,6 +12,7 @@ public enum UserErrorCode implements ErrorCodeInterface { // -- User -- User_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."), //404 NOT_HAVE_ADMIN_ROLE(HttpStatus.BAD_REQUEST, "관리자 권한이 없는 유저입니다."), + USER_NAME_IS_NULL(HttpStatus.BAD_REQUEST, "닉네임은 비어있으면 안됩니다."), ALREADY_DELETE_USR(HttpStatus.CONFLICT, "이미 삭제한 유저입니다."), //409, 이미 삭제한 유저기때문에 conflict 충돌이라는 의미 PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST, "변경할 비밀번호와 확인 비밀번호가 일치하지 않습니다."), //409, 이미 삭제한 유저기때문에 conflict 충돌이라는 의미 WRONG_CURRENT_PASSWORD(HttpStatus.UNAUTHORIZED, "현재 비밀번호가 올바르지 않습니다."); diff --git a/src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java b/src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java new file mode 100644 index 0000000..9db3f3a --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/user/exception/profile/UserNameNotNullException.java @@ -0,0 +1,10 @@ +package BookPick.mvp.domain.user.exception.profile; + +import BookPick.mvp.domain.user.enums.user.UserErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class UserNameNotNullException extends BusinessException { + public UserNameNotNullException(){ + super(UserErrorCode.USER_NAME_IS_NULL); + } +} diff --git a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java index b4f49b0..b5763e3 100644 --- a/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java +++ b/src/main/java/BookPick/mvp/domain/user/service/base/UserService.java @@ -6,6 +6,7 @@ import BookPick.mvp.domain.user.entity.User; import BookPick.mvp.domain.user.exception.common.AlreadyDeletedException; import BookPick.mvp.domain.user.exception.common.UserNotFoundException; +import BookPick.mvp.domain.user.exception.profile.UserNameNotNullException; import BookPick.mvp.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -60,7 +61,10 @@ public UserRes userProfileUpdate(Long userId, UserReq req) { // 더티 체킹 if (req.email() != null) user.setEmail(req.email()); - if (req.nickName() != null) user.setNickname(req.nickName()); + if (req.nickName() == null) { + throw new UserNameNotNullException(); + } + user.setNickname(req.nickName()); if (req.profileImage() != null) user.setProfileImageUrl(req.profileImage()); if (req.introduction() != null) user.setBio(req.introduction()); From 86e6e7cea22049071dd977662fa4df873d5db0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sat, 10 Jan 2026 21:08:27 +0900 Subject: [PATCH 273/291] Remove .env from tracking --- src/main/resources/.env | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/main/resources/.env diff --git a/src/main/resources/.env b/src/main/resources/.env deleted file mode 100644 index 244ad9d..0000000 --- a/src/main/resources/.env +++ /dev/null @@ -1,20 +0,0 @@ -SPRING_PROFILES_ACTIVE=local - -DB_HOST=hii.mysql.database.azure.com -DB_PORT=3306 -; DB_NAME=bookpick_product -DB_NAME=book_pick -DB_USERNAME=nan7789 -DB_PASSWORD=gustjq3735! - -JWT_ACCESS_SECRET=c2NhcmVkYWdlZmVhdGhlcnNwbGVudHlyZWFkeWVhdHByaW5jaXBhbHJpc2Vwcm9iYWJsZXRoaXNpc2FsZWFzdDI1NmJpdHM -JWT_ACCESS_EXPIRATION=900000 -JWT_REFRESH_SECRET=d2hvbGVtYWlucG9yY2hsb29rZWFyZ3JlYXRseXNoaW5lcmVzdHRlbGxuZWVkbGVrZWVwdGhpc2lzYWxlYXN0MjU2Yml0cw -JWT_REFRESH_EXPIRATION=604800000 - -KAKAO_API_KEY=103086ac1d365cf71f026f6caac34fb3 -GEMINI_API_KEY= -GEMINI_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent - - -SPRING_JPA_DDL_AUTO=update From 63e6abf9b2cb1baccafe52850c1eead51fd57fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sat, 10 Jan 2026 22:04:15 +0900 Subject: [PATCH 274/291] =?UTF-8?q?docs=20:=20gitIgnore=EC=97=90=20.env=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f414bda..21f8c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -121,4 +121,4 @@ aws/ #applicatoin.ym src/main/resources/origin -src/main/resources/.env.DS_Store +src/main/resources/.env From 200c4b793562a442af7243973e27f7d85c30369d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sat, 10 Jan 2026 22:13:54 +0900 Subject: [PATCH 275/291] =?UTF-8?q?bugfix=20:=20dto=EB=A5=BC=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=20=EB=82=B4=EB=A0=A4=EC=A4=98=EC=84=9C=20=EC=A7=81?= =?UTF-8?q?=EB=A0=AC=ED=99=94=20=EB=AC=B8=EC=A0=9C=20=EB=B0=9C=EC=83=9D=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReadingPreference/dto/ReadingPreferenceRes.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java index ef2327f..5be65e2 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/dto/ReadingPreferenceRes.java @@ -2,7 +2,7 @@ import BookPick.mvp.domain.ReadingPreference.entity.ReadingPreference; -import BookPick.mvp.domain.author.entity.Author; +import BookPick.mvp.domain.author.dto.preference.AuthorDto; import BookPick.mvp.domain.book.dto.preference.BookRes; import java.util.List; @@ -13,7 +13,7 @@ public record ReadingPreferenceRes( Long preferenceId, String mbti, Set favoriteBooks, // 좋아하는 책 - Set favoriteAuthors, + Set favoriteAuthors, List moods, // 독서 선호 분위기 List readingHabits, // 독서 습관 List genres, // 선호 장르 @@ -32,11 +32,15 @@ static public ReadingPreferenceRes from(ReadingPreference rp) { )) .collect(Collectors.toSet()); + Set favoriteAuthors = rp.getFavoriteAuthors().stream() + .map(author -> new AuthorDto(author.getName())) + .collect(Collectors.toSet()); + return new ReadingPreferenceRes( rp.getId(), rp.getMbti(), favoriteBooks, - rp.getFavoriteAuthors(), + favoriteAuthors, rp.getMoods(), rp.getReadingHabits(), rp.getGenres(), From 28bae0401db1debb3a7007578bc78328ec6469d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sat, 10 Jan 2026 23:28:50 +0900 Subject: [PATCH 276/291] =?UTF-8?q?bugfix[https://github.com/orgs/Book-Pic?= =?UTF-8?q?k/projects/4/views/3=3Fpane=3Dissue&itemId=3D148797791&issue=3D?= =?UTF-8?q?Book-Pick%7Cbookpick-front%7C70]=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EA=B0=80=20=EC=A2=8B=EC=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=84=20=EB=82=B4?= =?UTF-8?q?=EB=A0=A4=EC=A4=AC=EC=96=B4=EC=95=BC=20=ED=95=98=EB=8A=94?= =?UTF-8?q?=EB=8D=B0=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EB=B0=9B=EC=9D=80=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=82=B4=EB=A0=A4=EC=A4=98=EC=84=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c.user.id = :userId -> cl.user.id = :userId --- .../mvp/domain/curation/repository/CurationRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index e2f884d..4758a1b 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -80,7 +80,7 @@ List findPublishedCurationsByRecommendation( @Query(""" select c from Curation c join CurationLike cl on cl.curation = c - where c.isDrafted is false and c.user.id = :userId + where c.isDrafted is false and cl.user.id = :userId order by cl.createdAt desc """) List findLikedCurationsByUser(@Param("userId") Long userId, Pageable pageable); From 2c6b97cf94486605b1ba217bcc266ea86954e7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 11 Jan 2026 01:01:07 +0900 Subject: [PATCH 277/291] =?UTF-8?q?chore=20:=20isDrafted=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=EC=BB=AC=EB=9F=BC=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/BookPick/mvp/domain/curation/entity/Curation.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 993acc3..766a889 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -77,6 +77,8 @@ public class Curation { @Builder.Default @Column(name = "popularity_score") private Integer popularityScore = 0; + + @Column(name = "is_draft") private Boolean isDrafted; From b0a0d2376d359b065ea9c7302de494e22573d8fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Fri, 16 Jan 2026 21:10:08 +0900 Subject: [PATCH 278/291] =?UTF-8?q?feat=20:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=97=90=EA=B2=8C=20=EC=97=90=EB=94=94=ED=84=B0=EA=B0=80=20?= =?UTF-8?q?=EC=84=A0=EC=A0=95=ED=95=9C=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A4=84=20=EB=95=8C,=20=EC=9B=90=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=EC=9D=98=20id=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=80=EC=A7=80=EA=B3=A0=20=EB=B3=B4=EC=97=AC=EC=A3=BC?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4,=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20ID=EB=93=A4=EC=9D=84=20=EA=B0=80=EC=A7=80=EA=B3=A0?= =?UTF-8?q?=20=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **issue** https://github.com/orgs/Book-Pick/projects/4/views/3?pane=issue&itemId=148874967&issue=Book-Pick%7Cbookpick-front%7C71 --- .../list/CurationListController.java | 19 ++++++++++++++++++- .../service/list/CurationListService.java | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java index 1a24d1f..93b6630 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/list/CurationListController.java @@ -3,6 +3,8 @@ import BookPick.mvp.domain.ReadingPreference.enums.resCode.PreferenceErrorCode; import BookPick.mvp.domain.ReadingPreference.enums.resCode.PreferenceSuccessCode; import BookPick.mvp.domain.auth.service.CustomUserDetails; +import BookPick.mvp.domain.curation.dto.base.get.list.CurationContentRes; +import BookPick.mvp.domain.curation.dto.base.get.list.CurationIdsReq; import BookPick.mvp.domain.curation.dto.base.get.list.CurationListGetRes; import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.exception.common.CurationDraftOwnerException; @@ -17,6 +19,8 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.util.List; + @RestController @RequestMapping("/api/v1/curations") @RequiredArgsConstructor @@ -60,9 +64,22 @@ public ResponseEntity> getCurations( .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curationListGetRes)); } + @Operation(summary = "큐레이션 ID 목록으로 조회", description = "curationId 배열을 받아 해당 큐레이션 목록 반환", tags = {"Curation"}) + @PostMapping("/by-ids") + public ResponseEntity>> getCurationsByIds( + @RequestBody @Valid CurationIdsReq request, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + currentUserCheck.validateLoginUser(currentUser); + List curations = curationListService.getCurationsByIds( + request.curationIds(), + currentUser.getId() + ); - + return ResponseEntity.ok() + .body(ApiResponse.success(SuccessCode.CURATION_LIST_GET_SUCCESS, curations)); + } } diff --git a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java index b942634..f9f1bb0 100644 --- a/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java +++ b/src/main/java/BookPick/mvp/domain/curation/service/list/CurationListService.java @@ -7,6 +7,7 @@ import BookPick.mvp.domain.curation.dto.prefer.ReadingPreferenceInfo; import BookPick.mvp.domain.curation.enums.common.SortType; import BookPick.mvp.domain.curation.entity.Curation; +import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.repository.like.CurationLikeRepository; import BookPick.mvp.domain.curation.util.gemini.dto.CurationMatchResult; import BookPick.mvp.domain.curation.util.list.Handler.CurationPageHandler; @@ -15,6 +16,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Set; @@ -28,6 +30,7 @@ public class CurationListService { private final ReadingPreferenceRepository readingPreferenceRepository; private final CurationRecommendationService curationRecommendationService; private final CurationLikeRepository curationLikeRepository; + private final CurationRepository curationRepository; // 1. 큐레이션 리스트 조회 @@ -123,4 +126,19 @@ public CurationListGetRes getCurations(SortType sortType, Long cursor, int size, // Issue 1) DTO 만들어서 독서취향 정보 레이어간 소통 vs 사용자 독서취향 실시간 수정 반영 고려 // 1. 사용자는 독서취향을 한번 설정하면 자주 바꾸지 않는다. // 2. 따라서 DTO 생성 후 넣기로 결정 + + // 2. curationId 목록으로 큐레이션 조회 + @Transactional(readOnly = true) + public List getCurationsByIds(List curationIds, Long userId) { + List curations = curationRepository.findByIdIn(curationIds); + + Set likedIds = curationLikeRepository + .findLikedCurationIds(userId, curationIds) + .stream() + .collect(Collectors.toSet()); + + return curations.stream() + .map(c -> CurationContentRes.from(c, likedIds.contains(c.getId()))) + .collect(Collectors.toList()); + } } \ No newline at end of file From f98c3b630815ba3b88a48831cb4a5de73af71dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 18 Jan 2026 14:34:07 +0900 Subject: [PATCH 279/291] =?UTF-8?q?feat=20:=20=EC=9A=B0=EB=A6=AC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=EB=8A=94=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=EC=9D=84=20=EC=A0=9C=EC=99=B8=ED=95=9C=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=B4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=9D=B4=20=ED=95=84=EC=9A=94=ED=95=98=EA=B8=B0=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8=EC=97=90,=20SpringSecurity=EC=97=90=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EC=9D=B8=EC=A6=9D=20=EC=97=AC=EB=B6=80=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/Controller/BookSearchController.java | 9 ++++- .../all/ReceivedCommentsController.java | 5 +++ .../controller/base/CommentController.java | 17 ++++++++-- .../CommentAccessDeniedException.java | 11 +++++++ .../NotFoundParentCommentException.java | 12 +++++++ .../domain/comment/service/CommentPolicy.java | 18 ++++++++++ .../comment/service/CommentService.java | 18 ++++++++-- .../controller/base/CurationController.java | 5 +-- .../mvp/global/api/ErrorCode/ErrorCode.java | 1 + .../mvp/security/config/SecurityConfig.java | 33 +++++++++++++++++-- 10 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 src/main/java/BookPick/mvp/domain/comment/exception/CommentAccessDeniedException.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/exception/NotFoundParentCommentException.java create mode 100644 src/main/java/BookPick/mvp/domain/comment/service/CommentPolicy.java diff --git a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java index f8f8c59..185a8ce 100644 --- a/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java +++ b/src/main/java/BookPick/mvp/domain/book/Controller/BookSearchController.java @@ -1,14 +1,17 @@ package BookPick.mvp.domain.book.Controller; +import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.book.dto.search.BookSearchPageRes; import BookPick.mvp.domain.book.dto.search.BookSearchReq; import BookPick.mvp.domain.book.util.kakaoApi.BookSearchService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; 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; @@ -20,11 +23,15 @@ public class BookSearchController { private final BookSearchService bookSearchService; + private final CurrentUserCheck currentUserCheck; @Operation(summary = "책 검색", description = "검색어로 책 목록 조회", tags = {"Book Search"}) @PostMapping("/search") - public ResponseEntity> searchBookList(@RequestBody BookSearchReq req) { + public ResponseEntity> searchBookList(@RequestBody BookSearchReq req, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { BookSearchPageRes res = bookSearchService.getBookSearchList(req); + currentUserCheck.validateLoginUser(currentUser); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessCode.BOOK_LIST_READ_SUCCESS, res)); diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java b/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java index 1209bed..fb06eee 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/all/ReceivedCommentsController.java @@ -3,6 +3,7 @@ import BookPick.mvp.domain.auth.service.CustomUserDetails; import BookPick.mvp.domain.comment.dto.read.ReceivedCommentsDTO; import BookPick.mvp.domain.comment.service.ReceivedCommentsService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -19,11 +20,15 @@ public class ReceivedCommentsController { private final ReceivedCommentsService receivedCommentsService; + private final CurrentUserCheck currentUserCheck; @Operation(summary = "받은 댓글 조회", description = "현재 사용자가 받은 최신 댓글 목록을 조회합니다", tags = {"Comment"}) @GetMapping public ResponseEntity> getReceivedComments( @AuthenticationPrincipal CustomUserDetails currentUser) { + + currentUserCheck.validateLoginUser(currentUser); + ReceivedCommentsDTO res = receivedCommentsService.receivedCommentsRead(currentUser.getId()); return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_LIST_READ_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java b/src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java index 871b5e1..e6b9f48 100644 --- a/src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java +++ b/src/main/java/BookPick/mvp/domain/comment/controller/base/CommentController.java @@ -10,6 +10,7 @@ import BookPick.mvp.domain.comment.dto.update.CommentUpdateReq; import BookPick.mvp.domain.comment.dto.update.CommentUpdateRes; import BookPick.mvp.domain.comment.service.CommentService; +import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import io.swagger.v3.oas.annotations.Operation; @@ -25,12 +26,17 @@ public class CommentController { private final CommentService commentService; private final PagenationService pagenationService; + private final CurrentUserCheck currentUserCheck; // -- 1. 댓글 생성 -- @Operation(summary = "댓글 생성", description = "특정 큐레이션에 댓글을 생성합니다", tags = {"Comment"}) @PostMapping("/{curationId}/comments") public ResponseEntity> create(@PathVariable Long curationId, - @RequestBody CommentCreateReq commentCreateReq, @AuthenticationPrincipal CustomUserDetails currentUser) { + @RequestBody CommentCreateReq commentCreateReq, + @AuthenticationPrincipal CustomUserDetails currentUser) { + +// currentUserCheck.validateLoginUser(currentUser); + CommentCreateRes res = commentService.createComment(currentUser.getId(), curationId, commentCreateReq); return ResponseEntity.status(HttpStatus.CREATED) @@ -74,9 +80,12 @@ public ResponseEntity> getCommentDetail(@PathVaria public ResponseEntity> updateComment( @PathVariable Long curationId, @PathVariable Long commentId, - @RequestBody CommentUpdateReq req + @RequestBody CommentUpdateReq req, + @AuthenticationPrincipal CustomUserDetails currentUser ) { - CommentUpdateRes res = commentService.updateComment(commentId, req); +// currentUserCheck.validateLoginUser(currentUser); + + CommentUpdateRes res = commentService.updateComment(currentUser.getId(), commentId, req); return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_UPDATE_SUCCESS, res)); } @@ -88,6 +97,8 @@ public ResponseEntity> deleteComment( @PathVariable Long curationId, @PathVariable Long commentId ) { + + CommentDeleteRes res = commentService.deleteComment(curationId, commentId); return ResponseEntity.ok(ApiResponse.success(SuccessCode.COMMENT_DELETE_SUCCESS, res)); } diff --git a/src/main/java/BookPick/mvp/domain/comment/exception/CommentAccessDeniedException.java b/src/main/java/BookPick/mvp/domain/comment/exception/CommentAccessDeniedException.java new file mode 100644 index 0000000..75b977e --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/exception/CommentAccessDeniedException.java @@ -0,0 +1,11 @@ +// SelfSubscribeDeniedException.java +package BookPick.mvp.domain.comment.exception; + +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class CommentAccessDeniedException extends BusinessException { + public CommentAccessDeniedException() { + super(ErrorCode.CURATION_ACCESS_DENIED); + } +} \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/domain/comment/exception/NotFoundParentCommentException.java b/src/main/java/BookPick/mvp/domain/comment/exception/NotFoundParentCommentException.java new file mode 100644 index 0000000..f35bf93 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/exception/NotFoundParentCommentException.java @@ -0,0 +1,12 @@ +package BookPick.mvp.domain.comment.exception; + + +import BookPick.mvp.global.api.ErrorCode.ErrorCode; +import BookPick.mvp.global.exception.BusinessException; + +public class NotFoundParentCommentException extends BusinessException { + public NotFoundParentCommentException(){ + super(ErrorCode.PARENTS_COMMENT_NOT_FOUND); + } + +} diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentPolicy.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentPolicy.java new file mode 100644 index 0000000..6e981ac --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentPolicy.java @@ -0,0 +1,18 @@ +package BookPick.mvp.domain.comment.service; + +import BookPick.mvp.domain.comment.dto.create.CommentCreateReq; +import org.springframework.stereotype.Component; + +@Component +public class CommentPolicy { + + boolean isChildrenComment(CommentCreateReq req) { + + // 댓글만드는데 부모댓글이 존재하면 + if( req.parentId() != null){ + return true; + } + + return false; + } +} diff --git a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java index 2ba8ca9..51369a5 100644 --- a/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java +++ b/src/main/java/BookPick/mvp/domain/comment/service/CommentService.java @@ -8,7 +8,9 @@ import BookPick.mvp.domain.comment.dto.update.CommentUpdateReq; import BookPick.mvp.domain.comment.dto.update.CommentUpdateRes; import BookPick.mvp.domain.comment.entity.Comment; +import BookPick.mvp.domain.comment.exception.CommentAccessDeniedException; import BookPick.mvp.domain.comment.exception.CommentNotFoundException; +import BookPick.mvp.domain.comment.exception.NotFoundParentCommentException; import BookPick.mvp.domain.comment.repository.CommentRepository; import BookPick.mvp.domain.curation.exception.common.CurationNotFoundException; import BookPick.mvp.domain.curation.entity.Curation; @@ -34,6 +36,7 @@ public class CommentService { private final UserRepository userRepository; private final CurationRepository curationRepository; private final CommentRepository commentRepository; + private final CommentPolicy commentPolicy; // -- Create -- @@ -48,9 +51,12 @@ public CommentCreateRes createComment(Long userId, Long curationId, CommentCreat Comment parent = null; - if (req.parentId() != null) { + + + // 자식 댓글이면, + if(commentPolicy.isChildrenComment(req)){ parent = commentRepository.findById(req.parentId()) - .orElseThrow(CommentNotFoundException::new); + .orElseThrow(NotFoundParentCommentException::new); } Comment comment = Comment.builder() @@ -69,6 +75,8 @@ public CommentCreateRes createComment(Long userId, Long curationId, CommentCreat } + + // -- Read -- @Transactional(readOnly = true) public CommentListRes getCommentList(Long curationId, int page, int size) { @@ -104,10 +112,14 @@ public CommentDetailRes getCommentDetail(Long commentId) { // -- Update -- @Transactional - public CommentUpdateRes updateComment(Long commentId, CommentUpdateReq req) { + public CommentUpdateRes updateComment(Long userId, Long commentId, CommentUpdateReq req) { Comment comment = commentRepository.findById(commentId) .orElseThrow(CommentNotFoundException::new); + if(!comment.getUser().getId().equals(userId) ){ + throw new CommentAccessDeniedException(); + } + if (req.content() != null && !req.content().isBlank()) { comment.setContent(req.content()); comment.setUpdatedAt(LocalDateTime.now()); diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 29004e2..5afab50 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -50,9 +50,6 @@ public ResponseEntity> createCuration( } - - - @Operation(summary = "큐레이션 수정 (재발행 및 재 임시저장", description = "큐레이션 정보를 수정", tags = {"Curation"}) @PatchMapping("/{curationId}") public ResponseEntity> updateCuration( @@ -64,7 +61,7 @@ public ResponseEntity> updateCuration( CurationUpdateResult curationUpdateResult = curationUpdateService.updateCuration(currentUser.getId(), curationId, req); - return ResponseEntity.ok() + return ResponseEntity.ok() .body(ApiResponse.success(curationUpdateResult.successCode(), curationUpdateResult.curationUpdateRes())); } diff --git a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java index adfc599..87d087d 100644 --- a/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java +++ b/src/main/java/BookPick/mvp/global/api/ErrorCode/ErrorCode.java @@ -32,6 +32,7 @@ public enum ErrorCode implements ErrorCodeInterface { // -- Comment -- COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글 찾을 수 없습니다."), //404 + PARENTS_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 댓글의 부모 댓글을 찾을 수 없습니다."), //404 // -- Reading Preference -- READING_PREFERENCE_ALREADY_RESiGSTER(HttpStatus.CONFLICT, "이미 독서 취향이 존재합니다."), diff --git a/src/main/java/BookPick/mvp/security/config/SecurityConfig.java b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java index a917e00..a79147b 100644 --- a/src/main/java/BookPick/mvp/security/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java @@ -1,10 +1,14 @@ package BookPick.mvp.security.config; +import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.config.JwtFilter; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -39,14 +43,37 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/**").permitAll() + + + // 로그인이 필요없는 것들 + // 1. auth .requestMatchers("/api/v1/signup", "/api/v1/login", "/api/v1/logout").permitAll() - .requestMatchers("/api/v1/users/*/preferences").permitAll() - .requestMatchers(HttpMethod.GET,"/api/v1/curations/*/comments").permitAll() + // 2. author + // 3. book + // 4. comment + // 5. curation + // 6. ReadingPreference + // 7. user .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() .requestMatchers("/error").permitAll() .anyRequest().authenticated() ) + .exceptionHandling(eh -> eh + .authenticationEntryPoint((req, res, ex) -> { + res.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + res.setContentType("application/json; charset=UTF-8"); + + ApiResponse response = ApiResponse.customError( + HttpStatus.UNAUTHORIZED, + "로그인이 필요합니다.", + null + ); + + ObjectMapper objectMapper = new ObjectMapper(); + res.getWriter().write(objectMapper.writeValueAsString(response)); + }) + ) + .logout(logout -> logout .logoutUrl("/api/v1/logout") .clearAuthentication(true) From 89c149afea7056b2ed67a2f93420441ce531d5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 18 Jan 2026 15:07:39 +0900 Subject: [PATCH 280/291] =?UTF-8?q?etc=20:=20=EC=A4=91=EC=9A=94=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/repository/CurationRepository.java | 4 ++++ .../util/gemini/dto/CurationMatchResult.java | 1 + .../util/gemini/service/GeminiService.java | 17 ++++++++++++++--- .../mvp/security/config/SecurityConfig.java | 2 +- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java index 4758a1b..be05417 100644 --- a/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java +++ b/src/main/java/BookPick/mvp/domain/curation/repository/CurationRepository.java @@ -47,6 +47,8 @@ List findCurationsByPopularity( ); // Gemini 추천 결과로 큐레이션 찾기 (Batch Fetch로 N+1 방지) + // 1. m : 컬럼 별칭 + // 2. @Query(""" SELECT DISTINCT c FROM Curation c LEFT JOIN c.moods m @@ -66,6 +68,8 @@ List findPublishedCurationsByRecommendation( ); + + Optional findByUserIdAndId(Long userId, Long id); diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java index 8adffc6..a6d2303 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/dto/CurationMatchResult.java @@ -35,6 +35,7 @@ public static CurationMatchResult of(Curation curation, List matchedItems = new ArrayList<>(); // Mood 매칭 + // curation.getMoods() -> 1+N 문제 발생 , 아래도 동일 if (curation.getMoods() != null && curation.getMoods().contains(recommendedMood)) { matchedMood = recommendedMood; matchedItems.add(recommendedMood); diff --git a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java index fd003e8..7979da5 100644 --- a/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java +++ b/src/main/java/BookPick/mvp/domain/curation/util/gemini/service/GeminiService.java @@ -57,7 +57,13 @@ public List recommendCurationsWithMatch(Long userId, Conten ); // 4. 일치 정보와 함께 반환 (일치 개수 많은 순, 0점 제외) - return curations.stream() + + + // 5. 메모리 측정 +// Runtime runtime = Runtime.getRuntime(); +// long before = runtime.totalMemory() - runtime.freeMemory(); + + List results = curations.stream() .map(curation -> CurationMatchResult.of( curation, curation.getUser(), @@ -66,8 +72,13 @@ public List recommendCurationsWithMatch(Long userId, Conten recommendedKeyword, recommendedStyle )) - .filter(matchResult -> matchResult.getTotalMatchCount() > 0) // 매칭 점수 0점인 큐레이션 제외 - .sorted((a, b) -> Integer.compare(b.getTotalMatchCount(), a.getTotalMatchCount())) // Todo 1. 현재 MatchCount가지고 정렬 -> 취향유사도 해당 로직에서 계산해서 정렬 필요 + .filter(matchResult -> matchResult.getTotalMatchCount() > 0) + .sorted((a, b) -> Integer.compare(b.getTotalMatchCount(), a.getTotalMatchCount())) .collect(Collectors.toList()); + +// long after = runtime.totalMemory() - runtime.freeMemory(); +// System.out.println("사용 메모리: " + (after - before) / 1024 + " KB"); // 100 배치당 1079KB 사용 + + return results; } } \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/security/config/SecurityConfig.java b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java index a79147b..f8bd269 100644 --- a/src/main/java/BookPick/mvp/security/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java @@ -47,7 +47,7 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // 로그인이 필요없는 것들 // 1. auth - .requestMatchers("/api/v1/signup", "/api/v1/login", "/api/v1/logout").permitAll() + .requestMatchers("/api/v1/auth/signup", "/api/v1/auth/login", "/api/v1/auth/logout").permitAll() // 2. author // 3. book // 4. comment From d717554a4908f4ed2eaf006ae9e7fef6b0e5f830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 18 Jan 2026 16:23:27 +0900 Subject: [PATCH 281/291] =?UTF-8?q?feat=20:=20=ED=8A=B9=EC=A0=95=20?= =?UTF-8?q?=ED=81=90=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20dto=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../curation/dto/base/get/list/CurationIdsReq.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationIdsReq.java diff --git a/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationIdsReq.java b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationIdsReq.java new file mode 100644 index 0000000..112fa65 --- /dev/null +++ b/src/main/java/BookPick/mvp/domain/curation/dto/base/get/list/CurationIdsReq.java @@ -0,0 +1,11 @@ +package BookPick.mvp.domain.curation.dto.base.get.list; + +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +public record CurationIdsReq( + @NotEmpty(message = "curationIds는 필수입니다.") + List curationIds +) { +} From 776ce20dc49ae830f8aaf9db26877ce395ae0d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 18 Jan 2026 21:00:41 +0900 Subject: [PATCH 282/291] =?UTF-8?q?feat=20:=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EA=B3=B5=ED=8F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/domain/ReadingPreference/enums/filed/Keyword.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java index ce1fea2..138a5db 100644 --- a/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java +++ b/src/main/java/BookPick/mvp/domain/ReadingPreference/enums/filed/Keyword.java @@ -13,7 +13,8 @@ public enum Keyword implements PreferenceField { FANTASY("판타지"), REALITY("현실"), FUTURE("미래"), - PAST("과거"); + PAST("과거"), + HORROR("공포"); private final String description; From d335e3b747c58d65ffecd5b322041eba6818cbb1 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 23 Jan 2026 09:46:04 +0900 Subject: [PATCH 283/291] =?UTF-8?q?chore:=20test=20server=20cors=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../controller/base/CurationController.java | 30 +++++++++++++++++++ .../mvp/security/config/CorsConfig.java | 1 + 3 files changed, 34 insertions(+) diff --git a/build.gradle b/build.gradle index 4f1c296..d6e926d 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,9 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // CSV + implementation 'com.opencsv:opencsv:5.9' + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 5afab50..2a60cdc 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -14,18 +14,23 @@ import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; import BookPick.mvp.domain.curation.service.base.update.CurationUpdateService; +import BookPick.mvp.domain.curation.service.csv.CurationCsvService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; 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.Operation; +import java.util.List; + @RestController @RequestMapping("/api/v1/curations") @RequiredArgsConstructor @@ -33,6 +38,7 @@ public class CurationController { private final CurationCreateService curationCreateService; private final CurationUpdateService curationUpdateService; + private final CurationCsvService curationCsvService; private final BookSearchService bookSearchService; private final CurrentUserCheck currentUserCheck; @@ -89,6 +95,30 @@ public ResponseEntity> getCurationBookPurchaseLink( )); } + @Operation( + summary = "CSV 파일로 큐레이션 일괄 생성", + description = "CSV 파일을 업로드하여 여러 큐레이션을 한 번에 생성합니다. " + + "CSV 형식: title, bookTitle, bookAuthor, bookIsbn, bookImageUrl, review, moods, genres, keywords, styles, thumbnailUrl, thumbnailColor", + tags = {"Curation"} + ) + @PostMapping(value = "/upload-csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity>> uploadCsv( + @RequestParam("file") MultipartFile file, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + + currentUserCheck.validateLoginUser(currentUser); + + // CSV 파일 처리 및 큐레이션 생성 + List results = curationCsvService.uploadCsvAndCreateCurations(file, currentUser.getId()); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success( + SuccessCode.CURATION_PUBLISH_SUCCESS, + results + )); + } + } diff --git a/src/main/java/BookPick/mvp/security/config/CorsConfig.java b/src/main/java/BookPick/mvp/security/config/CorsConfig.java index 0bc4c45..ccca742 100644 --- a/src/main/java/BookPick/mvp/security/config/CorsConfig.java +++ b/src/main/java/BookPick/mvp/security/config/CorsConfig.java @@ -15,6 +15,7 @@ public WebMvcConfigurer corsConfigurer() { public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("https://bookpick-front.vercel.app") + .allowedOrigins("https://bookpick-front-dev.vercel.app") .allowedMethods("GET","POST","PUT","DELETE","PATCH","OPTIONS") .allowedHeaders("*") .allowCredentials(true); From 66b8d25673d89565c75230e4520f9f7f318154c9 Mon Sep 17 00:00:00 2001 From: halo Date: Fri, 23 Jan 2026 09:56:49 +0900 Subject: [PATCH 284/291] =?UTF-8?q?chore=20:=20csv=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/base/CurationController.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java index 2a60cdc..bd5cd23 100644 --- a/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java +++ b/src/main/java/BookPick/mvp/domain/curation/controller/base/CurationController.java @@ -14,7 +14,6 @@ import BookPick.mvp.domain.curation.repository.CurationRepository; import BookPick.mvp.domain.curation.service.base.create.CurationCreateService; import BookPick.mvp.domain.curation.service.base.update.CurationUpdateService; -import BookPick.mvp.domain.curation.service.csv.CurationCsvService; import BookPick.mvp.domain.user.util.CurrentUserCheck; import BookPick.mvp.global.api.ApiResponse; import BookPick.mvp.global.api.SuccessCode.SuccessCode; @@ -38,7 +37,6 @@ public class CurationController { private final CurationCreateService curationCreateService; private final CurationUpdateService curationUpdateService; - private final CurationCsvService curationCsvService; private final BookSearchService bookSearchService; private final CurrentUserCheck currentUserCheck; @@ -95,29 +93,8 @@ public ResponseEntity> getCurationBookPurchaseLink( )); } - @Operation( - summary = "CSV 파일로 큐레이션 일괄 생성", - description = "CSV 파일을 업로드하여 여러 큐레이션을 한 번에 생성합니다. " + - "CSV 형식: title, bookTitle, bookAuthor, bookIsbn, bookImageUrl, review, moods, genres, keywords, styles, thumbnailUrl, thumbnailColor", - tags = {"Curation"} - ) - @PostMapping(value = "/upload-csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity>> uploadCsv( - @RequestParam("file") MultipartFile file, - @AuthenticationPrincipal CustomUserDetails currentUser - ) { - currentUserCheck.validateLoginUser(currentUser); - // CSV 파일 처리 및 큐레이션 생성 - List results = curationCsvService.uploadCsvAndCreateCurations(file, currentUser.getId()); - - return ResponseEntity.status(HttpStatus.CREATED) - .body(ApiResponse.success( - SuccessCode.CURATION_PUBLISH_SUCCESS, - results - )); - } } From fdcd83aa0b2f6d1b9f518c625f1fb33ecac39678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 25 Jan 2026 11:49:35 +0900 Subject: [PATCH 285/291] =?UTF-8?q?fix:=20CORS=20=EB=8B=A4=EC=A4=91=20orig?= =?UTF-8?q?in=20=EC=84=A4=EC=A0=95=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/BookPick/mvp/domain/curation/entity/Curation.java | 5 +++++ src/main/java/BookPick/mvp/security/config/CorsConfig.java | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java index 766a889..11ca742 100644 --- a/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java +++ b/src/main/java/BookPick/mvp/domain/curation/entity/Curation.java @@ -1,5 +1,6 @@ package BookPick.mvp.domain.curation.entity; +import BookPick.mvp.domain.comment.entity.Comment; import BookPick.mvp.domain.curation.dto.base.CurationReq; import BookPick.mvp.domain.curation.dto.base.update.CurationUpdateReq; import BookPick.mvp.domain.curation.enums.common.State; @@ -12,6 +13,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Builder @@ -81,6 +83,9 @@ public class Curation { @Column(name = "is_draft") private Boolean isDrafted; + @OneToMany(mappedBy = "curation", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + @CreatedDate @Column(updatable = false) diff --git a/src/main/java/BookPick/mvp/security/config/CorsConfig.java b/src/main/java/BookPick/mvp/security/config/CorsConfig.java index ccca742..65b2a78 100644 --- a/src/main/java/BookPick/mvp/security/config/CorsConfig.java +++ b/src/main/java/BookPick/mvp/security/config/CorsConfig.java @@ -14,9 +14,10 @@ public WebMvcConfigurer corsConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("https://bookpick-front.vercel.app") - .allowedOrigins("https://bookpick-front-dev.vercel.app") - .allowedMethods("GET","POST","PUT","DELETE","PATCH","OPTIONS") + .allowedOrigins("https://bookpick-front.vercel.app", + "https://bookpick-front-dev.vercel.app" + ) + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); } From 4a9f54842b5d88884f721cf0e3b38e44a767fef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9B=90=ED=98=84=EC=84=AD?= Date: Sun, 25 Jan 2026 11:50:23 +0900 Subject: [PATCH 286/291] =?UTF-8?q?feat/infra:=20dev=EC=99=80=20prod=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20CI/CD=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 4 +- .github/workflows/prod.yml | 75 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/prod.yml diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index efe543d..59aca7a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -63,11 +63,11 @@ jobs: mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts + ssh-keyscan -H ${{ secrets.SERVER_HOST_DEV }} >> ~/.ssh/known_hosts - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 run: | - ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} 'bash -s' < ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.PROD_SERVER_HOST }} >> ~/.ssh/known_hosts + + - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 + run: | + ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} 'bash -s' < Date: Sun, 25 Jan 2026 12:00:33 +0900 Subject: [PATCH 287/291] =?UTF-8?q?chore:=20dev=20server=20host=20?= =?UTF-8?q?=EA=B9=83=ED=97=88=EB=B8=8C=20=EC=8B=9C=ED=81=AC=EB=A6=BF?= =?UTF-8?q?=ED=82=A4=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 59aca7a..621c596 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -63,11 +63,11 @@ jobs: mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H ${{ secrets.SERVER_HOST_DEV }} >> ~/.ssh/known_hosts + ssh-keyscan -H ${{ secrets.DEV_SERVER_HOST }} >> ~/.ssh/known_hosts - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 run: | - ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST_DEV }} 'bash -s' < Date: Sun, 25 Jan 2026 12:16:18 +0900 Subject: [PATCH 288/291] =?UTF-8?q?chore:=20CorsConfig=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvp/security/config/CorsConfig.java | 29 ------------------- .../mvp/security/config/SecurityConfig.java | 4 ++- 2 files changed, 3 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/BookPick/mvp/security/config/CorsConfig.java diff --git a/src/main/java/BookPick/mvp/security/config/CorsConfig.java b/src/main/java/BookPick/mvp/security/config/CorsConfig.java deleted file mode 100644 index 65b2a78..0000000 --- a/src/main/java/BookPick/mvp/security/config/CorsConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package BookPick.mvp.security.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class CorsConfig { - - @Bean - public WebMvcConfigurer corsConfigurer() { - return new WebMvcConfigurer() { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins("https://bookpick-front.vercel.app", - "https://bookpick-front-dev.vercel.app" - ) - .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } - }; - } -} - -// 오리진 모두 허용 -// evil에서 요 \ No newline at end of file diff --git a/src/main/java/BookPick/mvp/security/config/SecurityConfig.java b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java index f8bd269..35dc6ed 100644 --- a/src/main/java/BookPick/mvp/security/config/SecurityConfig.java +++ b/src/main/java/BookPick/mvp/security/config/SecurityConfig.java @@ -92,7 +92,9 @@ public CorsConfigurationSource corsConfigurationSource() { config.setAllowedOrigins(List.of( "http://localhost:5173", - "https://bookpick-front.vercel.app" + "https://bookpick-front.vercel.app", + "https://bookpick-front-dev.vercel.app" + )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); From 74d9f04477cfeff890c308eb4411d31267111f67 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 26 Jan 2026 21:15:01 +0900 Subject: [PATCH 289/291] =?UTF-8?q?feat:=20AWS=20ECR=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20develop=EC=84=9C=EB=B2=85=20CI?= =?UTF-8?q?/CD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 71 ++++++++++++++------------------------- 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 621c596..a20109e 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,75 +1,54 @@ -name: BookPick BE dev CI/CD (v 1.2) - +name: BookPick BE dev CI/CD (v 1.3) on: -# pull_request: -# branches: -# - develop # develop을 대상으로 하는 PR만 감지 -# types: [ closed ] # PR이 닫힐 때만 실행 - - - # PR Merge 수락 시에도 작동됨 push: branches: - - develop # develop을 대상으로 하는 PR만 감지 - + - develop jobs: - # test: - # runs-on: ubuntu-latest - # steps: - # - # - name : 1. checkout repo - # uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - # - ## - name: 2. run test - ## run: ./gradlew test - # - # - name: 3. set up JDK 21 - # uses: actions/setup-java@v2 - # with: - # java-version: 21 - # distribution: 'temurin' - # - # - name: 4. grant execute permission for gradlew - # run: chmod +x gradlew - - build-and-push: - # needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - + - uses: actions/checkout@v2 - - name: 3. Docker Hub Login - uses: docker/login-action@v3 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 with: - username: ${{secrets.DOCKER_USERNAME}} - password: ${{secrets.DOCKER_TOKEN}} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 - - name: 4. Spring Image Build and Push + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and Push to ECR run: | - docker build --platform linux/amd64 \ - -t ${{ secrets.DOCKER_USERNAME }}/${{secrets.IMAGE}} \ - --push . + docker build --platform linux/amd64 -t 219268921033.dkr.ecr.ap-northeast-2.amazonaws.com/bookpick:1.0.0 . + docker push 219268921033.dkr.ecr.ap-northeast-2.amazonaws.com/bookpick:1.0.0 + deploy: needs: build-and-push - # if: github.event.pull_request.merged == true # merge된 경우에만 실행 runs-on: ubuntu-latest steps: - - name: Set up SSH # 1. SSH 설정 + - name: Set up SSH run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.DEV_SERVER_HOST }} >> ~/.ssh/known_hosts - - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 + - name: Deploy to EC2 run: | ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.DEV_SERVER_HOST }} 'bash -s' < Date: Mon, 26 Jan 2026 21:23:44 +0900 Subject: [PATCH 290/291] =?UTF-8?q?feat:=20aws=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index a20109e..e1ccee2 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -45,8 +45,8 @@ jobs: cd /home/${{ secrets.SERVER_USER }} # ECR 로그인 - aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 219268921033.dkr.ecr.ap-northeast-2.amazonaws.com - + /usr/local/bin/aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 219268921033.dkr.ecr.ap-northeast-2.amazonaws.com + # 새 이미지 풀받고 재시작 docker compose down docker compose pull From 502d968d568350d0eea127c9e423087b6555d060 Mon Sep 17 00:00:00 2001 From: halo Date: Mon, 26 Jan 2026 22:00:32 +0900 Subject: [PATCH 291/291] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EB=8D=95?= =?UTF-8?q?=EC=85=98=20CI/CD=20ECR=EC=97=90=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EB=84=A3=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prod.yml | 77 ++++++++++++++------------------------ 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 3d666e2..6edd887 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -1,75 +1,54 @@ -name: BookPick BE dev CI/CD (v 1.2) - +name: BookPick BE prod CI/CD (v 1.3) on: -# pull_request: -# branches: -# - develop # develop을 대상으로 하는 PR만 감지 -# types: [ closed ] # PR이 닫힐 때만 실행 - - - # PR Merge 수락 시에도 작동됨 push: branches: - - main # develop을 대상으로 하는 PR만 감지 - + - main jobs: - # test: - # runs-on: ubuntu-latest - # steps: - # - # - name : 1. checkout repo - # uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - # - ## - name: 2. run test - ## run: ./gradlew test - # - # - name: 3. set up JDK 21 - # uses: actions/setup-java@v2 - # with: - # java-version: 21 - # distribution: 'temurin' - # - # - name: 4. grant execute permission for gradlew - # run: chmod +x gradlew - - build-and-push: - # needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 # 깃허브 액션이 코드가져옴 - + - uses: actions/checkout@v2 - - name: 3. Docker Hub Login - uses: docker/login-action@v3 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 with: - username: ${{secrets.DOCKER_USERNAME}} - password: ${{secrets.DOCKER_TOKEN}} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 - - name: 4. Spring Image Build and Push + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build and Push to ECR run: | - docker build --platform linux/amd64 \ - -t ${{ secrets.DOCKER_USERNAME }}/${{secrets.IMAGE}} \ - --push . + docker build --platform linux/amd64 -t 219268921033.dkr.ecr.ap-northeast-2.amazonaws.com/bookpick:1.0.0 . + docker push 219268921033.dkr.ecr.ap-northeast-2.amazonaws.com/bookpick:1.0.0 + deploy: needs: build-and-push - # if: github.event.pull_request.merged == true # merge된 경우에만 실행 runs-on: ubuntu-latest steps: - - name: Set up SSH # 1. SSH 설정 + - name: Set up SSH run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H ${{ secrets.PROD_SERVER_HOST }} >> ~/.ssh/known_hosts - - name: Deploy to EC2 # 2. EC2에 접속해서 배포 스크립트 실행 + - name: Deploy to EC2 run: | - ssh -i ~/.ssh/id_rsa ${{ secrets.SERVER_USER }}@${{ secrets.PROD_SERVER_HOST }} 'bash -s' <