diff --git a/.gitignore b/.gitignore index 2ca2d0c..8c5e601 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,8 @@ out/ ### VS Code ### .vscode/ -### Environment variables ### +### Environment variables ###::wq + .env *.env +.env diff --git a/build.gradle b/build.gradle index de3f44d..2d4aef1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,13 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.4.5' + id 'org.springframework.boot' version '3.2.5'//스웨거로 인해 버전 낮춤 id 'io.spring.dependency-management' version '1.1.7' } +ext { + queryDslVersion = "5.0.0" +} + group = 'com.example' version = '0.0.1-SNAPSHOT' @@ -13,6 +17,10 @@ java { } } +ext { + queryDslVersion = "5.0.0" +} + configurations { compileOnly { extendsFrom annotationProcessor @@ -21,29 +29,69 @@ configurations { repositories { mavenCentral() + + // springdoc 2.5.0은 여기서 제공됨 + maven { url 'https://repo.spring.io/release' } } dependencies { + // Spring Boot implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' - runtimeOnly('io.jsonwebtoken:jjwt-jackson:0.11.5') + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // DB runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + + // AWS + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.538' + + // QueryDSL + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta" + annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // Swagger (springdoc) + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' + + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.538' -} tasks.named('test') { useJUnitPlatform() } +// WAR 파일 안 만들게 설정 jar { enabled = false -} +} + +// QueryDSL 설정 +def querydslDir = "src/main/generated" + +sourceSets { + main { + java { + srcDirs += querydslDir + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir) +} + + diff --git a/src/main/generated/com/example/FixLog/domain/bookmark/QBookmark.java b/src/main/generated/com/example/FixLog/domain/bookmark/QBookmark.java new file mode 100644 index 0000000..8a75601 --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/bookmark/QBookmark.java @@ -0,0 +1,56 @@ +package com.example.FixLog.domain.bookmark; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QBookmark is a Querydsl query type for Bookmark + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBookmark extends EntityPathBase { + + private static final long serialVersionUID = -975112590L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QBookmark bookmark = new QBookmark("bookmark"); + + public final NumberPath bookmarkId = createNumber("bookmarkId", Long.class); + + public final QBookmarkFolder folderId; + + public final BooleanPath isMarked = createBoolean("isMarked"); + + public final com.example.FixLog.domain.post.QPost postId; + + public QBookmark(String variable) { + this(Bookmark.class, forVariable(variable), INITS); + } + + public QBookmark(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QBookmark(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QBookmark(PathMetadata metadata, PathInits inits) { + this(Bookmark.class, metadata, inits); + } + + public QBookmark(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.folderId = inits.isInitialized("folderId") ? new QBookmarkFolder(forProperty("folderId"), inits.get("folderId")) : null; + this.postId = inits.isInitialized("postId") ? new com.example.FixLog.domain.post.QPost(forProperty("postId"), inits.get("postId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/bookmark/QBookmarkFolder.java b/src/main/generated/com/example/FixLog/domain/bookmark/QBookmarkFolder.java new file mode 100644 index 0000000..9ae87de --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/bookmark/QBookmarkFolder.java @@ -0,0 +1,55 @@ +package com.example.FixLog.domain.bookmark; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QBookmarkFolder is a Querydsl query type for BookmarkFolder + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QBookmarkFolder extends EntityPathBase { + + private static final long serialVersionUID = 655942016L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QBookmarkFolder bookmarkFolder = new QBookmarkFolder("bookmarkFolder"); + + public final ListPath bookmarks = this.createList("bookmarks", Bookmark.class, QBookmark.class, PathInits.DIRECT2); + + public final NumberPath folderId = createNumber("folderId", Long.class); + + public final StringPath folderName = createString("folderName"); + + public final com.example.FixLog.domain.member.QMember userId; + + public QBookmarkFolder(String variable) { + this(BookmarkFolder.class, forVariable(variable), INITS); + } + + public QBookmarkFolder(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QBookmarkFolder(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QBookmarkFolder(PathMetadata metadata, PathInits inits) { + this(BookmarkFolder.class, metadata, inits); + } + + public QBookmarkFolder(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.userId = inits.isInitialized("userId") ? new com.example.FixLog.domain.member.QMember(forProperty("userId"), inits.get("userId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/follow/QFollow.java b/src/main/generated/com/example/FixLog/domain/follow/QFollow.java new file mode 100644 index 0000000..4186783 --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/follow/QFollow.java @@ -0,0 +1,54 @@ +package com.example.FixLog.domain.follow; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QFollow is a Querydsl query type for Follow + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QFollow extends EntityPathBase { + + private static final long serialVersionUID = 1281338898L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QFollow follow = new QFollow("follow"); + + public final com.example.FixLog.domain.member.QMember followerId; + + public final NumberPath followId = createNumber("followId", Long.class); + + public final com.example.FixLog.domain.member.QMember followingId; + + public QFollow(String variable) { + this(Follow.class, forVariable(variable), INITS); + } + + public QFollow(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QFollow(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QFollow(PathMetadata metadata, PathInits inits) { + this(Follow.class, metadata, inits); + } + + public QFollow(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.followerId = inits.isInitialized("followerId") ? new com.example.FixLog.domain.member.QMember(forProperty("followerId"), inits.get("followerId")) : null; + this.followingId = inits.isInitialized("followingId") ? new com.example.FixLog.domain.member.QMember(forProperty("followingId"), inits.get("followingId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/fork/QFork.java b/src/main/generated/com/example/FixLog/domain/fork/QFork.java new file mode 100644 index 0000000..4631aad --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/fork/QFork.java @@ -0,0 +1,57 @@ +package com.example.FixLog.domain.fork; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QFork is a Querydsl query type for Fork + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QFork extends EntityPathBase { + + private static final long serialVersionUID = 1327242738L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QFork fork = new QFork("fork"); + + public final com.example.FixLog.domain.post.QPost forkedPostId; + + public final NumberPath forkId = createNumber("forkId", Long.class); + + public final com.example.FixLog.domain.post.QPost originalPostId; + + public final com.example.FixLog.domain.member.QMember userId; + + public QFork(String variable) { + this(Fork.class, forVariable(variable), INITS); + } + + public QFork(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QFork(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QFork(PathMetadata metadata, PathInits inits) { + this(Fork.class, metadata, inits); + } + + public QFork(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.forkedPostId = inits.isInitialized("forkedPostId") ? new com.example.FixLog.domain.post.QPost(forProperty("forkedPostId"), inits.get("forkedPostId")) : null; + this.originalPostId = inits.isInitialized("originalPostId") ? new com.example.FixLog.domain.post.QPost(forProperty("originalPostId"), inits.get("originalPostId")) : null; + this.userId = inits.isInitialized("userId") ? new com.example.FixLog.domain.member.QMember(forProperty("userId"), inits.get("userId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/like/QPostLike.java b/src/main/generated/com/example/FixLog/domain/like/QPostLike.java new file mode 100644 index 0000000..5bb85a6 --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/like/QPostLike.java @@ -0,0 +1,56 @@ +package com.example.FixLog.domain.like; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QPostLike is a Querydsl query type for PostLike + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QPostLike extends EntityPathBase { + + private static final long serialVersionUID = -1255529902L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QPostLike postLike = new QPostLike("postLike"); + + public final BooleanPath isLiked = createBoolean("isLiked"); + + public final NumberPath likeId = createNumber("likeId", Long.class); + + public final com.example.FixLog.domain.post.QPost postId; + + public final com.example.FixLog.domain.member.QMember userId; + + public QPostLike(String variable) { + this(PostLike.class, forVariable(variable), INITS); + } + + public QPostLike(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QPostLike(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QPostLike(PathMetadata metadata, PathInits inits) { + this(PostLike.class, metadata, inits); + } + + public QPostLike(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.postId = inits.isInitialized("postId") ? new com.example.FixLog.domain.post.QPost(forProperty("postId"), inits.get("postId")) : null; + this.userId = inits.isInitialized("userId") ? new com.example.FixLog.domain.member.QMember(forProperty("userId"), inits.get("userId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/member/QMember.java b/src/main/generated/com/example/FixLog/domain/member/QMember.java new file mode 100644 index 0000000..b466382 --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/member/QMember.java @@ -0,0 +1,71 @@ +package com.example.FixLog.domain.member; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QMember is a Querydsl query type for Member + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QMember extends EntityPathBase { + + private static final long serialVersionUID = -933378574L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QMember member = new QMember("member1"); + + public final StringPath bio = createString("bio"); + + public final com.example.FixLog.domain.bookmark.QBookmarkFolder bookmarkFolder; + + public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); + + public final StringPath email = createString("email"); + + public final BooleanPath isDeleted = createBoolean("isDeleted"); + + public final StringPath nickname = createString("nickname"); + + public final StringPath password = createString("password"); + + public final ListPath posts = this.createList("posts", com.example.FixLog.domain.post.Post.class, com.example.FixLog.domain.post.QPost.class, PathInits.DIRECT2); + + public final StringPath profileImageUrl = createString("profileImageUrl"); + + public final EnumPath socialType = createEnum("socialType", SocialType.class); + + public final DateTimePath updatedAt = createDateTime("updatedAt", java.time.LocalDateTime.class); + + public final NumberPath userId = createNumber("userId", Long.class); + + public QMember(String variable) { + this(Member.class, forVariable(variable), INITS); + } + + public QMember(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QMember(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QMember(PathMetadata metadata, PathInits inits) { + this(Member.class, metadata, inits); + } + + public QMember(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.bookmarkFolder = inits.isInitialized("bookmarkFolder") ? new com.example.FixLog.domain.bookmark.QBookmarkFolder(forProperty("bookmarkFolder"), inits.get("bookmarkFolder")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/post/QPost.java b/src/main/generated/com/example/FixLog/domain/post/QPost.java new file mode 100644 index 0000000..76d7522 --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/post/QPost.java @@ -0,0 +1,81 @@ +package com.example.FixLog.domain.post; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QPost is a Querydsl query type for Post + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QPost extends EntityPathBase { + + private static final long serialVersionUID = 1578031282L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QPost post = new QPost("post"); + + public final ListPath bookmarks = this.createList("bookmarks", com.example.FixLog.domain.bookmark.Bookmark.class, com.example.FixLog.domain.bookmark.QBookmark.class, PathInits.DIRECT2); + + public final StringPath causeAnalysis = createString("causeAnalysis"); + + public final StringPath coverImage = createString("coverImage"); + + public final DateTimePath createdAt = createDateTime("createdAt", java.time.LocalDateTime.class); + + public final DateTimePath editedAt = createDateTime("editedAt", java.time.LocalDateTime.class); + + public final StringPath environment = createString("environment"); + + public final StringPath errorMessage = createString("errorMessage"); + + public final StringPath extraContent = createString("extraContent"); + + public final NumberPath postId = createNumber("postId", Long.class); + + public final ListPath postLikes = this.createList("postLikes", com.example.FixLog.domain.like.PostLike.class, com.example.FixLog.domain.like.QPostLike.class, PathInits.DIRECT2); + + public final ListPath postTags = this.createList("postTags", PostTag.class, QPostTag.class, PathInits.DIRECT2); + + public final StringPath postTitle = createString("postTitle"); + + public final StringPath problem = createString("problem"); + + public final StringPath referenceLink = createString("referenceLink"); + + public final StringPath reproduceCode = createString("reproduceCode"); + + public final StringPath solutionCode = createString("solutionCode"); + + public final com.example.FixLog.domain.member.QMember userId; + + public QPost(String variable) { + this(Post.class, forVariable(variable), INITS); + } + + public QPost(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QPost(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QPost(PathMetadata metadata, PathInits inits) { + this(Post.class, metadata, inits); + } + + public QPost(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.userId = inits.isInitialized("userId") ? new com.example.FixLog.domain.member.QMember(forProperty("userId"), inits.get("userId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/post/QPostTag.java b/src/main/generated/com/example/FixLog/domain/post/QPostTag.java new file mode 100644 index 0000000..d3844cd --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/post/QPostTag.java @@ -0,0 +1,54 @@ +package com.example.FixLog.domain.post; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.PathInits; + + +/** + * QPostTag is a Querydsl query type for PostTag + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QPostTag extends EntityPathBase { + + private static final long serialVersionUID = -1582016120L; + + private static final PathInits INITS = PathInits.DIRECT2; + + public static final QPostTag postTag = new QPostTag("postTag"); + + public final QPost postId; + + public final NumberPath postTagId = createNumber("postTagId", Long.class); + + public final com.example.FixLog.domain.tag.QTag tagId; + + public QPostTag(String variable) { + this(PostTag.class, forVariable(variable), INITS); + } + + public QPostTag(Path path) { + this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS)); + } + + public QPostTag(PathMetadata metadata) { + this(metadata, PathInits.getFor(metadata, INITS)); + } + + public QPostTag(PathMetadata metadata, PathInits inits) { + this(PostTag.class, metadata, inits); + } + + public QPostTag(Class type, PathMetadata metadata, PathInits inits) { + super(type, metadata, inits); + this.postId = inits.isInitialized("postId") ? new QPost(forProperty("postId"), inits.get("postId")) : null; + this.tagId = inits.isInitialized("tagId") ? new com.example.FixLog.domain.tag.QTag(forProperty("tagId")) : null; + } + +} + diff --git a/src/main/generated/com/example/FixLog/domain/tag/QTag.java b/src/main/generated/com/example/FixLog/domain/tag/QTag.java new file mode 100644 index 0000000..04398e7 --- /dev/null +++ b/src/main/generated/com/example/FixLog/domain/tag/QTag.java @@ -0,0 +1,43 @@ +package com.example.FixLog.domain.tag; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QTag is a Querydsl query type for Tag + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QTag extends EntityPathBase { + + private static final long serialVersionUID = -1474567286L; + + public static final QTag tag = new QTag("tag"); + + public final EnumPath tagCategory = createEnum("tagCategory", TagCategory.class); + + public final NumberPath tagId = createNumber("tagId", Long.class); + + public final StringPath tagInfo = createString("tagInfo"); + + public final StringPath tagName = createString("tagName"); + + public QTag(String variable) { + super(Tag.class, forVariable(variable)); + } + + public QTag(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QTag(PathMetadata metadata) { + super(Tag.class, metadata); + } + +} + diff --git a/src/main/generated/com/example/FixLog/tset/QTestMember.java b/src/main/generated/com/example/FixLog/tset/QTestMember.java new file mode 100644 index 0000000..daad525 --- /dev/null +++ b/src/main/generated/com/example/FixLog/tset/QTestMember.java @@ -0,0 +1,41 @@ +package com.example.FixLog.tset; + +import static com.querydsl.core.types.PathMetadataFactory.*; + +import com.querydsl.core.types.dsl.*; + +import com.querydsl.core.types.PathMetadata; +import javax.annotation.processing.Generated; +import com.querydsl.core.types.Path; + + +/** + * QTestMember is a Querydsl query type for TestMember + */ +@Generated("com.querydsl.codegen.DefaultEntitySerializer") +public class QTestMember extends EntityPathBase { + + private static final long serialVersionUID = -1256703330L; + + public static final QTestMember testMember = new QTestMember("testMember"); + + public final NumberPath id = createNumber("id", Long.class); + + public final StringPath name = createString("name"); + + public final StringPath password = createString("password"); + + public QTestMember(String variable) { + super(TestMember.class, forVariable(variable)); + } + + public QTestMember(Path path) { + super(path.getType(), path.getMetadata()); + } + + public QTestMember(PathMetadata metadata) { + super(TestMember.class, metadata); + } + +} + diff --git a/src/main/java/com/example/FixLog/config/QuerydslConfig.java b/src/main/java/com/example/FixLog/config/QuerydslConfig.java new file mode 100644 index 0000000..c69a710 --- /dev/null +++ b/src/main/java/com/example/FixLog/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package com.example.FixLog.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/config/SecurityConfig.java b/src/main/java/com/example/FixLog/config/SecurityConfig.java index d620cac..ede3958 100644 --- a/src/main/java/com/example/FixLog/config/SecurityConfig.java +++ b/src/main/java/com/example/FixLog/config/SecurityConfig.java @@ -2,7 +2,6 @@ import com.example.FixLog.repository.MemberRepository; import com.example.FixLog.util.JwtUtil; -import com.example.FixLog.config.JwtAuthenticationFilter; import jakarta.servlet.Filter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -33,18 +32,20 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers(HttpMethod.GET, "/", "/main", "/main/**").permitAll() .requestMatchers(HttpMethod.GET, "/auth/**").permitAll() .requestMatchers(HttpMethod.POST, "/auth/**").permitAll() - .requestMatchers(HttpMethod.POST, "/members/signup").permitAll() .requestMatchers(HttpMethod.GET, "/members/check-email").permitAll() .requestMatchers(HttpMethod.GET, "/members/check-nickname").permitAll() - .requestMatchers(HttpMethod.GET, "/search/**").permitAll() + .requestMatchers(HttpMethod.GET, "/", "/main", "/main/**").permitAll() .requestMatchers(HttpMethod.GET, "/posts/**").permitAll() // h2-console (로컬 테스트용) .requestMatchers(HttpMethod.GET, "/h2-console/**").permitAll() // 배포 확인용 임시 허용 .requestMatchers(HttpMethod.GET, "/test", "/test/**").permitAll() + // Swagger 허용 + .requestMatchers(HttpMethod.GET,"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers(HttpMethod.POST,"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + .requestMatchers(HttpMethod.PATCH,"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() // 그 외 모든 요청은 인증 필요 - .requestMatchers(HttpMethod.GET, "/test", "/test/**").permitAll() // 테스트용 허용 .anyRequest().authenticated() ) @@ -56,7 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public Filter jwtAuthenticationFilter() { - return new JwtAuthenticationFilter(jwtUtil, memberRepository); + return new com.example.FixLog.config.JwtAuthenticationFilter(jwtUtil, memberRepository); } @Bean diff --git a/src/main/java/com/example/FixLog/config/SwaggerConfig.java b/src/main/java/com/example/FixLog/config/SwaggerConfig.java new file mode 100644 index 0000000..4b224a1 --- /dev/null +++ b/src/main/java/com/example/FixLog/config/SwaggerConfig.java @@ -0,0 +1,32 @@ +package com.example.FixLog.config; + +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.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + final String securitySchemeName = "bearerAuth"; + + return new OpenAPI() + .info(new Info() + .title("FixLog API Docs") + .description("FixLog 백엔드 API 명세서") + .version("v1.0")) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) // 전체 보안 요구사항 + .components(new Components().addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + )); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/controller/FollowController.java b/src/main/java/com/example/FixLog/controller/FollowController.java index 7f211e1..ba7ade2 100644 --- a/src/main/java/com/example/FixLog/controller/FollowController.java +++ b/src/main/java/com/example/FixLog/controller/FollowController.java @@ -7,6 +7,7 @@ import com.example.FixLog.dto.follow.response.FollowerListResponseDto; import com.example.FixLog.dto.follow.response.FollowingListResponseDto; import com.example.FixLog.service.FollowService; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -22,17 +23,19 @@ public class FollowController { private final FollowService followService; // 팔로우하기 + @SecurityRequirement(name = "bearerAuth") @PostMapping public ResponseEntity> follow( @RequestBody FollowRequestDto followRequestDto, @AuthenticationPrincipal UserDetails userDetails // jwt 구현 전까지 임시 사용 -> 이후 AuthenticationPrincipal 사용 예정 - ){ + ){ String requesterEmail = userDetails.getUsername(); FollowResponseDto result = followService.follow(requesterEmail, followRequestDto.getTargetMemberId()); return ResponseEntity.ok(Response.success("팔로우 완료", result)); } // 언팔로우하기 + @SecurityRequirement(name = "bearerAuth") @PostMapping("/unfollow") public ResponseEntity> unfollow( @RequestBody UnfollowRequestDto requestDto, @@ -44,6 +47,7 @@ public ResponseEntity> unfollow( } // 나를 팔로우하는 목록 조회 + @SecurityRequirement(name = "bearerAuth") @GetMapping("/followers") public ResponseEntity>> getMyFollowers( @AuthenticationPrincipal UserDetails userDetails @@ -54,6 +58,7 @@ public ResponseEntity>> getMyFollowers( } // 내가 팔로우하는 목록 조회 + @SecurityRequirement(name = "bearerAuth") @GetMapping("/followings") public ResponseEntity>> getMyFollowings( @AuthenticationPrincipal UserDetails userDetails diff --git a/src/main/java/com/example/FixLog/controller/MemberController.java b/src/main/java/com/example/FixLog/controller/MemberController.java index 1e7c9f3..88bd6a2 100644 --- a/src/main/java/com/example/FixLog/controller/MemberController.java +++ b/src/main/java/com/example/FixLog/controller/MemberController.java @@ -12,6 +12,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; @RestController @RequestMapping("/members") @@ -40,8 +41,10 @@ public ResponseEntity> checkNickname(@Reques return ResponseEntity.ok(Response.success(msg, new DuplicateCheckResponseDto(exists))); } + @SecurityRequirement(name = "bearerAuth") @GetMapping("/me") public ResponseEntity> getMyInfo(@AuthenticationPrincipal Member member) { + System.out.println("회원 정보 조회 요청: " + member.getEmail()); //추후에 log.info()로 바꿀 예정 MemberInfoResponseDto responseDto = new MemberInfoResponseDto( member.getEmail(), member.getNickname(), @@ -49,6 +52,7 @@ public ResponseEntity> getMyInfo(@Authentication member.getBio(), member.getSocialType() ); + System.out.println("회원 정보 조회 완료: " + member.getEmail()); return ResponseEntity.ok(Response.success("회원 정보 조회 성공", responseDto)); } @@ -62,6 +66,7 @@ public ResponseEntity> getProfilePreview() { return ResponseEntity.ok(Response.success("닉네임&프로필사진 조회 성공", dto)); } + @SecurityRequirement(name = "bearerAuth") @DeleteMapping("/me") public ResponseEntity> withdraw( @AuthenticationPrincipal Member member, diff --git a/src/main/java/com/example/FixLog/controller/MypageMemberController.java b/src/main/java/com/example/FixLog/controller/MypageMemberController.java index 630cea4..05d115e 100644 --- a/src/main/java/com/example/FixLog/controller/MypageMemberController.java +++ b/src/main/java/com/example/FixLog/controller/MypageMemberController.java @@ -10,6 +10,7 @@ import com.example.FixLog.service.S3Service; import com.example.FixLog.service.MemberService; import com.example.FixLog.domain.member.Member; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import jakarta.validation.Valid; @@ -26,6 +27,7 @@ public class MypageMemberController { private final MemberService memberService; private final S3Service s3Service; + @SecurityRequirement(name = "bearerAuth") @PatchMapping("/members/nickname") public ResponseEntity> editNickname( @RequestBody @Valid EditNicknameRequestDto requestDto @@ -35,6 +37,7 @@ public ResponseEntity> editNickname( return ResponseEntity.ok(Response.success("닉네임 수정 성공", "SUCCESS")); } + @SecurityRequirement(name = "bearerAuth") @PatchMapping("/members/password") public ResponseEntity> editPassword( @RequestBody @Valid EditPasswordRequestDto requestDto @@ -44,6 +47,7 @@ public ResponseEntity> editPassword( return ResponseEntity.ok(Response.success("비밀번호 변경 성공", "SUCCESS")); } + @SecurityRequirement(name = "bearerAuth") @GetMapping("/members/profile-image/presign") public ResponseEntity> presignProfileImage( @AuthenticationPrincipal Member member, @@ -59,6 +63,7 @@ public ResponseEntity> presignProfileImage( return ResponseEntity.ok(Response.success("Presigned URL 발급 성공", dto)); } + @SecurityRequirement(name = "bearerAuth") @PatchMapping("/members/profile-image") public ResponseEntity> updateProfileImageUrl( @AuthenticationPrincipal Member member, @@ -72,6 +77,7 @@ public ResponseEntity> updateProfileImageUrl( return ResponseEntity.ok(Response.success("프로필 이미지 저장 성공", "SUCCESS")); } + @SecurityRequirement(name = "bearerAuth") @PatchMapping("/members/bio") public ResponseEntity> editBio( @RequestBody @Valid EditBioRequestDto requestDto diff --git a/src/main/java/com/example/FixLog/controller/MypagePostController.java b/src/main/java/com/example/FixLog/controller/MypagePostController.java index d6d3d05..e1da547 100644 --- a/src/main/java/com/example/FixLog/controller/MypagePostController.java +++ b/src/main/java/com/example/FixLog/controller/MypagePostController.java @@ -33,6 +33,7 @@ public ResponseEntity>> getMyPos return ResponseEntity.ok(Response.success("내가 작성한 글 보기 성공", data)); } +<<<<<<< HEAD // 내가 좋아요한 글 @GetMapping("/likes") public ResponseEntity>> getLikedPosts( @@ -45,6 +46,20 @@ public ResponseEntity>> getLiked PageResponseDto result = mypagePostService.getLikedPosts(email, page, sort, size); return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result)); } +======= + // 내가 좋아요한 글 + @GetMapping("/likes") + public ResponseEntity>> getLikedPosts( + @AuthenticationPrincipal UserDetails userDetails, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "4") int size, + @RequestParam(defaultValue = "0") int sort) { + + String email = userDetails.getUsername(); + PageResponseDto result = mypagePostService.getLikedPosts(email, page, sort, size); + return ResponseEntity.ok(Response.success("내가 좋아요한 글 보기 성공", result)); + } +>>>>>>> origin/develop } diff --git a/src/main/java/com/example/FixLog/controller/SearchController.java b/src/main/java/com/example/FixLog/controller/SearchController.java new file mode 100644 index 0000000..e74145d --- /dev/null +++ b/src/main/java/com/example/FixLog/controller/SearchController.java @@ -0,0 +1,39 @@ +package com.example.FixLog.controller; + +import com.example.FixLog.dto.PageResponseDto; +import com.example.FixLog.dto.Response; +import com.example.FixLog.dto.search.SearchPostDto; +import com.example.FixLog.service.SearchService; +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.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/main") +public class SearchController { + + private final SearchService searchService; + + @GetMapping("/search") + public Response> search( + @RequestParam(required = false) String keyword, + @RequestParam(required = false) List tags, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + Page result = searchService.searchPosts(keyword, tags, pageable); + + PageResponseDto responseDto = PageResponseDto.from(result, dto -> dto); + return Response.success("검색 성공", responseDto); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/dto/search/SearchPostDto.java b/src/main/java/com/example/FixLog/dto/search/SearchPostDto.java new file mode 100644 index 0000000..571359e --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/search/SearchPostDto.java @@ -0,0 +1,21 @@ +package com.example.FixLog.dto.search; + +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +@Builder +public class SearchPostDto { + private Long postId; + private String title; + private String content; + private String writerNickname; + private String writerProfileImage; + private List tags; // 예: [“spring-boot”, “jwt”, “java”] + private LocalDateTime createdAt; + private int likeCount; + private int bookmarkCount; +} diff --git a/src/main/java/com/example/FixLog/dto/search/SearchRequestDto.java b/src/main/java/com/example/FixLog/dto/search/SearchRequestDto.java new file mode 100644 index 0000000..55a1d60 --- /dev/null +++ b/src/main/java/com/example/FixLog/dto/search/SearchRequestDto.java @@ -0,0 +1,23 @@ +package com.example.FixLog.dto.search; + +import lombok.Getter; + +import java.util.List; + +/** + * 게시글 검색 요청 DTO + * - keyword와 태그 카테고리 정보를 담아야 됨 + */ +@Getter +public class SearchRequestDto { + + private String keyword; // 텍스트 검색어 + + private String bigCategory; // 택1 (ex: 백엔드) + private String majorCategory; // 택1 (ex: Spring) + private String middleCategory; // 택1 (ex: JPA) + private List minorCategories; // 복수 선택 가능 (ex: JWT, CI/CD 등) + + private Integer page; // 1부터 시작 + private String sort; // 정렬 기준 (ex: recent, popular) +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/mock/PostMockDataInitializer.java b/src/main/java/com/example/FixLog/mock/PostMockDataInitializer.java new file mode 100644 index 0000000..d361cc9 --- /dev/null +++ b/src/main/java/com/example/FixLog/mock/PostMockDataInitializer.java @@ -0,0 +1,100 @@ +package com.example.FixLog.mock; + +import com.example.FixLog.domain.bookmark.Bookmark; +import com.example.FixLog.domain.like.PostLike; +import com.example.FixLog.domain.member.Member; +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.domain.post.PostTag; +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.repository.MemberRepository; +import com.example.FixLog.repository.bookmark.BookmarkFolderRepository; +import com.example.FixLog.repository.bookmark.BookmarkRepository; +import com.example.FixLog.repository.like.PostLikeRepository; +import com.example.FixLog.repository.post.PostRepository; +import com.example.FixLog.repository.post.PostTagRepository; +import com.example.FixLog.repository.tag.TagRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + + +@Component +@Order(6) +@RequiredArgsConstructor +public class PostMockDataInitializer implements CommandLineRunner { + + private final MemberRepository memberRepository; + private final PostRepository postRepository; + private final TagRepository tagRepository; + private final PostTagRepository postTagRepository; + private final PostLikeRepository postLikeRepository; + private final BookmarkRepository bookmarkRepository; + private final BookmarkFolderRepository bookmarkFolderRepository; + + @Override + @Transactional + public void run(String... args) { + if (postRepository.count() > 8) return; // 기존 1개 외 추가 + + Optional memberOpt = memberRepository.findByEmail("test2@example.com"); + if (memberOpt.isEmpty()) { + System.out.println("test2@example.com 회원 없음"); + return; + } + + Member member = memberOpt.get(); + Map tagMap = tagRepository.findAll().stream() + .collect(Collectors.toMap(Tag::getTagName, tag -> tag)); + + List config = List.of( + new String[]{"백엔드", "스프링부트", "자바", "NullPointerException", "500 Internal Server Error"}, + new String[]{"프론트엔드", "리액트", "자바스크립트", "Cannot read property of undefined", "상태(state) 업데이트 누락"}, + new String[]{"머신러닝", "케라스", "파이썬", "OutOfMemoryError", "HTTP 에러"}, + new String[]{"백엔드", "노드", "JSON", "CORS 정책 오류", "404 Not Found"}, + new String[]{"프론트엔드", "넥스트", "CSS", "스타일 깨짐", "렌더링 무한 루프"}, + new String[]{"머신러닝", "사이킷런", "R", "ClassNotFoundException", "Permission Error"} + ); + + for (int i = 0; i < config.size(); i++) { + String[] tags = config.get(i); + Post post = Post.builder() + .userId(member) + .postTitle("테스트 업그레이드 " + (i + 2)) + .coverImage("https://cdn.example.com/images/test" + (i + 2) + ".jpg") + .problem("이 게시물은 문제 설명이 200자를 넘도록 작성되었습니다. 문제 발생 상황, 재현 과정, 로그, 화면 캡처 등 다양한 정보가 포함될 수 있습니다. 이 텍스트는 말줄임표가 잘 붙는지 확인하기 위한 용도로 작성되었으며, 검색 결과에서는 200자까지만 보여야 합니다. 이후 내용은 생략될 수 있습니다. 추가 텍스트를 더 붙입니다. 더 붙입니다. 더 붙입니다.") + .errorMessage("이건 에러다 keyword 포함") + .environment("환경 정보") + .reproduceCode("재현 코드") + .solutionCode("해결 코드") + .causeAnalysis("원인 분석") + .referenceLink("https://example.com") + .extraContent("추가 설명") + .createdAt(LocalDateTime.now()) + .editedAt(LocalDateTime.now()) + .build(); + postRepository.save(post); + + List postTags = Arrays.stream(tags) + .map(tagMap::get) + .filter(Objects::nonNull) + .map(tag -> new PostTag(post, tag)) + .toList(); + postTagRepository.saveAll(postTags); + + postLikeRepository.save(new PostLike(member, post)); + + bookmarkFolderRepository.findFirstByUserId(member).ifPresent(folder -> { + Bookmark bookmark = new Bookmark(folder, post); + bookmarkRepository.save(bookmark); + }); + } + + System.out.println("test2@example.com 사용자의 게시물 6개 + 태그/좋아요/북마크 생성 완료"); + } +} diff --git a/src/main/java/com/example/FixLog/mock/TagMockDataInitializer.java b/src/main/java/com/example/FixLog/mock/TagMockDataInitializer.java new file mode 100644 index 0000000..51d3ee9 --- /dev/null +++ b/src/main/java/com/example/FixLog/mock/TagMockDataInitializer.java @@ -0,0 +1,88 @@ +package com.example.FixLog.mock; + +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.repository.tag.TagRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.example.FixLog.domain.tag.TagCategory.*; + +@Component +@RequiredArgsConstructor +@Order(4) +public class TagMockDataInitializer implements CommandLineRunner { + + private final TagRepository tagRepository; + + @Override + public void run(String... args) { + // 현재 저장된 태그 이름들 (중복 방지용) + Set existingTagNames = tagRepository.findAll() + .stream() + .map(Tag::getTagName) + .collect(Collectors.toSet()); + + List tagsToInsert = new ArrayList<>(); + + // BIG_CATEGORY + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(BIG_CATEGORY, "백엔드", "backend")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(BIG_CATEGORY, "머신러닝", "machine learning")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(BIG_CATEGORY, "프론트엔드", "frontend")); + + // MAJOR_CATEGORY + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "장고", "django")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "스프링부트", "spring-boot")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "넥스트", "next.js")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "케라스", "keras")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "파이토치", "pytorch")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "사이킷런", "scikit-learn")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "노드", "node.js")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "리액트", "react")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MAJOR_CATEGORY, "리액트 네이티브", "react-native")); + + // MIDDLE_CATEGORY + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "CSS", "css")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "자바스크립트", "javascript")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "R", "r")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "JSON", "json")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "자바", "java")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "Haskell", "haskell")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "파이썬", "python")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MIDDLE_CATEGORY, "C", "c")); + + // MINOR_CATEGORY + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "NullPointerException", "null-pointer-exception")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "500 Internal Server Error", "500-error")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "CORS 정책 오류", "cors-error")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "Database connection timeout", "db-timeout")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "ClassNotFoundException", "class-not-found")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "Cannot read property of undefined", "undefined-property")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "상태(state) 업데이트 누락", "state-missing")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "HTTP 에러", "http-error")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "렌더링 무한 루프", "render-loop")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "스타일 깨짐", "style-break")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "404 Not Found", "404-error")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "Permission Error", "permission-error")); + addIfNotExist(tagsToInsert, existingTagNames, Tag.of(MINOR_CATEGORY, "OutOfMemoryError", "out-of-memory")); + + if (!tagsToInsert.isEmpty()) { + tagRepository.saveAll(tagsToInsert); + System.out.println("중복 없는 목업 태그 " + tagsToInsert.size() + "개 삽입 완료"); + } else { + System.out.println("삽입할 신규 태그가 없습니다."); + } + } + + private void addIfNotExist(List list, Set existingNames, Tag tag) { + if (!existingNames.contains(tag.getTagName())) { + list.add(tag); + } + } +} diff --git a/src/main/java/com/example/FixLog/repository/post/PostRepository.java b/src/main/java/com/example/FixLog/repository/post/PostRepository.java index e0dd7bc..f818a33 100644 --- a/src/main/java/com/example/FixLog/repository/post/PostRepository.java +++ b/src/main/java/com/example/FixLog/repository/post/PostRepository.java @@ -9,7 +9,7 @@ import java.util.List; -public interface PostRepository extends JpaRepository { +public interface PostRepository extends JpaRepository, PostRepositoryCustom{ List findTop12ByOrderByCreatedAtDesc(); List findTop12ByOrderByPostLikesDesc(); Page findAllByOrderByCreatedAtDesc(Pageable pageable); diff --git a/src/main/java/com/example/FixLog/repository/post/PostRepositoryCustom.java b/src/main/java/com/example/FixLog/repository/post/PostRepositoryCustom.java new file mode 100644 index 0000000..6c820ae --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/post/PostRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.example.FixLog.repository.post; + +import com.example.FixLog.dto.search.SearchPostDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface PostRepositoryCustom { + Page searchByKeywordAndTags(String keyword, List tags, Pageable pageable); +} diff --git a/src/main/java/com/example/FixLog/repository/post/PostRepositoryImpl.java b/src/main/java/com/example/FixLog/repository/post/PostRepositoryImpl.java new file mode 100644 index 0000000..e8aedad --- /dev/null +++ b/src/main/java/com/example/FixLog/repository/post/PostRepositoryImpl.java @@ -0,0 +1,87 @@ +package com.example.FixLog.repository.post; + +import com.example.FixLog.domain.post.Post; +import com.example.FixLog.dto.search.SearchPostDto; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.example.FixLog.domain.post.QPost.post; +import static com.example.FixLog.domain.post.QPostTag.postTag; +import static com.example.FixLog.domain.tag.QTag.tag; + +@RequiredArgsConstructor +public class PostRepositoryImpl implements PostRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Page searchByKeywordAndTags(String keyword, List tags, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + + // 1. 키워드 조건 + //containsIgnoreCase 쓰려면 post에서 String problem, String errorMessage의 @Lob 어노테이션을 없애야 함 + // + keyword 검색 시 @Lob 필드(CLOB)에는 lower() 사용 시 에러 발생 가능 + // 따라서 단순 like 연산만 사용하여 처리함 + if (keyword != null && !keyword.isBlank()) { + builder.and(post.problem.like("%" + keyword + "%") + .or(post.errorMessage.like("%" + keyword + "%")) + .or(post.postTitle.like("%" + keyword + "%"))); + } + + // 2. 태그 조건 (AND 조건) + if (tags != null && !tags.isEmpty()) { + NumberExpression count = postTag.tagId.tagId.count(); + + JPQLQuery subQuery = queryFactory + .select(postTag.postId.postId) + .from(postTag) + .where(postTag.tagId.tagName.in(tags)) + .groupBy(postTag.postId.postId) + .having(count.eq((long) tags.size())); + + builder.and(post.postId.in(subQuery)); + } + + // 3. 본문 조회 + List result = queryFactory + .selectFrom(post) + .leftJoin(post.postTags, postTag).fetchJoin() + .leftJoin(postTag.tagId, tag).fetchJoin() + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + // 4. DTO 매핑 + List content = result.stream().map(p -> SearchPostDto.builder() + .postId(p.getPostId()) + .title(p.getPostTitle()) + .content(p.getProblem().length() > 200 ? p.getProblem().substring(0, 200) + "…" : p.getProblem()) + .writerNickname(p.getUserId().getNickname()) + .writerProfileImage(p.getUserId().getProfileImageUrl()) + .tags(p.getPostTags().stream().map(pt -> pt.getTagId().getTagName()).toList()) + .createdAt(p.getCreatedAt()) + .likeCount(p.getPostLikes().size()) + .bookmarkCount(p.getBookmarks().size()) + .build() + ).collect(Collectors.toList()); + + // 5. 페이징 처리 + return PageableExecutionUtils.getPage(content, pageable, + () -> queryFactory + .select(post.postId) + .from(post) + .where(builder) + .fetch() + .size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/service/AuthService.java b/src/main/java/com/example/FixLog/service/AuthService.java index 3a941e8..31cc96e 100644 --- a/src/main/java/com/example/FixLog/service/AuthService.java +++ b/src/main/java/com/example/FixLog/service/AuthService.java @@ -20,6 +20,9 @@ public class AuthService { private final JwtUtil jwtUtil; public LoginResponseDto login(LoginRequestDto requestDto) { + + System.out.println("로그인 요청: " + requestDto.getEmail()); + Member member = memberRepository.findByEmail(requestDto.getEmail()) .orElseThrow(() -> new CustomException(ErrorCode.USER_NICKNAME_NOT_FOUND)); @@ -29,6 +32,8 @@ public LoginResponseDto login(LoginRequestDto requestDto) { String token = jwtUtil.createToken(member.getUserId(), member.getEmail()); + System.out.println("로그인 성공: " + member.getEmail() + " (nickname: " + member.getNickname() + ")"); + // 응답에서 null-safe하게 기본 이미지 처리 포함 return LoginResponseDto.from(member, token); } diff --git a/src/main/java/com/example/FixLog/service/MemberService.java b/src/main/java/com/example/FixLog/service/MemberService.java index 760be59..9028c89 100644 --- a/src/main/java/com/example/FixLog/service/MemberService.java +++ b/src/main/java/com/example/FixLog/service/MemberService.java @@ -35,6 +35,8 @@ public class MemberService { */ @Transactional public void signup(SignupRequestDto request) { + System.out.println("회원가입 요청 - 이메일: " + request.getEmail()); + System.out.println("회원가입 요청 - 닉네임: " + request.getNickname()); validateEmail(request.getEmail()); validateNickname(request.getNickname()); @@ -47,6 +49,8 @@ public void signup(SignupRequestDto request) { memberRepository.save(member); bookmarkFolderRepository.save(new BookmarkFolder(member)); + + System.out.println("회원가입 완료: " + request.getEmail()); } /** @@ -66,9 +70,11 @@ public Member getCurrentMemberInfo() { */ @Transactional public void editNickname(Member member, String newNickname) { + System.out.println("닉네임 수정 요청: " + member.getNickname() + " → " + newNickname); validateNickname(newNickname); member.updateNickname(newNickname); memberRepository.save(member); + System.out.println("닉네임 수정 완료: " + member.getNickname()); } /** @@ -76,9 +82,11 @@ public void editNickname(Member member, String newNickname) { */ @Transactional public void editPassword(Member member, EditPasswordRequestDto dto) { + System.out.println("비밀번호 수정 요청: 이메일 - " + member.getEmail()); validatePasswordChange(dto.getCurrentPassword(), dto.getNewPassword(), member.getPassword()); member.updatePassword(passwordEncoder.encode(dto.getNewPassword())); memberRepository.save(member); + System.out.println("비밀번호 수정 완료"); } /** @@ -86,19 +94,24 @@ public void editPassword(Member member, EditPasswordRequestDto dto) { */ @Transactional public void editProfileImage(Member member, String newProfileImageUrl) { + System.out.println("프로필 이미지 수정 요청: " + member.getEmail()); String finalImage = Optional.ofNullable(newProfileImageUrl).orElse(DefaultImage.PROFILE); member.updateProfileImage(finalImage); memberRepository.save(member); + System.out.println("프로필 이미지 수정 완료"); } + /** * 소개글 수정 */ @Transactional public void editBio(Member member, String newBio) { + System.out.println("소개글 수정 요청: " + member.getEmail()); String finalBio = Optional.ofNullable(newBio).orElse(DefaultText.BIO); member.updateBio(finalBio); memberRepository.save(member); + System.out.println("소개글 수정 완료"); } /** @@ -149,12 +162,14 @@ public LoginResponseDto getLoginResponse(Member member, String accessToken) { // ========================== 검증 메서드 ========================== public void validateEmail(String email) { + System.out.println("이메일 중복 검사 진행 중: " + email); if (isEmailDuplicated(email)) { throw new CustomException(ErrorCode.EMAIL_DUPLICATED); } } public void validateNickname(String nickname) { + System.out.println(" 닉네임 중복 검사 진행 중: " + nickname); if (isNicknameDuplicated(nickname)) { throw new CustomException(ErrorCode.NICKNAME_DUPLICATED); } diff --git a/src/main/java/com/example/FixLog/service/SearchService.java b/src/main/java/com/example/FixLog/service/SearchService.java new file mode 100644 index 0000000..3ab5572 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/SearchService.java @@ -0,0 +1,10 @@ +package com.example.FixLog.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import java.util.List; +import com.example.FixLog.dto.search.SearchPostDto; + +public interface SearchService { + Page searchPosts(String keyword, List tags, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/example/FixLog/service/impl/SearchServiceImpl.java b/src/main/java/com/example/FixLog/service/impl/SearchServiceImpl.java new file mode 100644 index 0000000..ab171f8 --- /dev/null +++ b/src/main/java/com/example/FixLog/service/impl/SearchServiceImpl.java @@ -0,0 +1,38 @@ +package com.example.FixLog.service.impl; + +import com.example.FixLog.domain.tag.Tag; +import com.example.FixLog.dto.search.SearchPostDto; +import com.example.FixLog.repository.post.PostRepository; +import com.example.FixLog.repository.tag.TagRepository; +import com.example.FixLog.service.SearchService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class SearchServiceImpl implements SearchService { + + private final PostRepository postRepository; + private final TagRepository tagRepository; + + @Override + public Page searchPosts(String keyword, List tags, Pageable pageable) { + System.out.println("[검색 요청] 키워드: " + keyword + ", 태그: " + tags + ", 페이지 번호: " + pageable.getPageNumber()); + Page result; + + if (tags == null || tags.isEmpty()) { + result = postRepository.searchByKeywordAndTags(keyword, null, pageable); + } else { + result = postRepository.searchByKeywordAndTags(keyword, tags, pageable); + } + + // 검색 시 태그는 카테고리별로 1개씩만 전달되므로, 우선순위 계산 없이 그대로 전달 + System.out.println("[검색 결과] 총 개수: " + result.getTotalElements()); + return result; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f8da5a6..dae0b10 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,43 +1,69 @@ -# Correct property name spring.application.name=FixLog -## DB setting -#spring.h2.console.enabled=true -#spring.h2.console.path=/h2-console -# -## DataBase Info -#spring.datasource.url=jdbc:h2:tcp://localhost/~/fixlog -#spring.datasource.driver-class-name=org.h2.Driver -#spring.datasource.username=sa -#spring.datasource.password= -# -#spring.jpa.show-sql=true -#spring.jpa.hibernate.ddl-auto=update -#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect -##### dev ##### + +##### [DEV] ##### server.port=8083 +# DB + +server.port=8083 + +# DB (MySQL) spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +<<<<<<< HEAD spring.datasource.url=${MYSQL_URL} spring.datasource.username=${MYSQL_USERNAME} spring.datasource.password=${MYSQL_PASSWORD} +======= +spring.datasource.url=jdbc:mysql://fixlog-db.c7cau8y2srl7.ap-northeast-2.rds.amazonaws.com:3306/fixlog?serverTimezone=Asia/Seoul +spring.datasource.username=admin +spring.datasource.password=${MYSQL_PASSWORD} + +# JPA +>>>>>>> origin/develop spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.jpa.properties.hibernate.format_sql=true -# AWS S3 configuration +# AWS S3 cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID} cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY} cloud.aws.region.static=${AWS_REGION} cloud.aws.s3.bucket=${AWS_S3_BUCKET} +# JWT +jwt.secret=${JWT_KEY} + +# ?? ?? +logging.level.org.springframework.security=DEBUG -spring.jwt.secret=${JWT_KEY} -# 로그 레벨 설정 -logging.level.root=INFO -logging.level.com.example.FixLog=DEBUG -logging.file.name=logs/app.log + + +##### [PROD] ##### + +# server.port=8083 +# +# spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +# spring.datasource.url=${MYSQL_URL} +# spring.datasource.username=${MYSQL_USERNAME} +# spring.datasource.password=${MYSQL_PASSWORD} +# +# spring.jpa.hibernate.ddl-auto=update +# spring.jpa.show-sql=true +# spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect +# spring.jpa.properties.hibernate.format_sql=true +# +# cloud.aws.credentials.access-key=${AWS_ACCESS_KEY_ID} +# cloud.aws.credentials.secret-key=${AWS_SECRET_ACCESS_KEY} +# cloud.aws.region.static=${AWS_REGION} +# cloud.aws.s3.bucket=${AWS_S3_BUCKET} +# +# jwt.secret=${JWT_KEY} +# +# logging.level.root=INFO +# logging.level.com.example.FixLog=DEBUG +# logging.file.name=logs/app.log