diff --git a/docs/pr/pr-140-refactor--public-access-control.md.md b/docs/pr/pr-140-refactor--public-access-control.md.md new file mode 100644 index 00000000..37bef523 --- /dev/null +++ b/docs/pr/pr-140-refactor--public-access-control.md.md @@ -0,0 +1,108 @@ +# 비회원 사용자 GET 접근 정책 리팩터링 (PR [#140](https://github.com/juulabel/juulabel-back/pull/142)) + +## 개요 + +기존 보안 정책은 인증되지 않은 사용자(비회원)에 대해 사실상 모든 접근을 차단하는 구조였으며, 일부 공개 엔드포인트만 명시적으로 허용하고 있었습니다. 해당 방식은 강력한 보안성을 확보할 수 있는 반면, 신규 유입 사용자나 비회원의 콘텐츠 탐색 경험을 제한하는 문제가 있었습니다. + +이번 리팩터링은 "비회원 사용자도 서비스 콘텐츠를 탐색할 수 있도록 한다"는 목표 아래, 명시된 GET 요청에 한해 접근을 허용하는 방향으로 접근 제어 로직을 개선하였습니다. 단, 상태를 변경하는 요청(POST, PUT, DELETE)은 기존대로 인증을 요구합니다. + +## 변경 전 vs 변경 후 + +| 항목 | 변경 전 | 변경 후 | +| -------------------------- | --------------------------------------- | ------------------------------------------------------------- | +| 콘텐츠 접근성 | ❌ 비회원은 대부분의 콘텐츠에 접근 불가 | ✅ 주요 콘텐츠 열람 가능 (시음노트, 일상생활, 유저 프로필 등) | +| 읽기 작업 처리 | 허용된 소수 엔드포인트만 가능 | 명시된 GET 엔드포인트만 허용, 나머지는 모두 차단 | +| 쓰기 작업 처리 | 인증 필요 (POST, PUT, DELETE 모두 차단) | 동일하게 유지 | + +--- + +## 보안 정책 요약 + +```java +// 완전 공개 엔드 포인트 (우선순위 최상) +private static final String[] PUBLIC_ENDPOINTS = { + "/swagger-ui/**", + "/v3/api-docs/**", + "/error", + "/favicon.ico", + "/", + "/actuator/**", + "/v1/api/auth/login/**", + "/v1/api/auth/sign-up" +}; + +// 관리자 전용 엔트포인트 +private static final String[] ADMIN_ENDPOINTS = { + "/v1/api/admins/permission/test" +}; + +// 인증/인가 필요한 특정 GET 엔드포인트 +private static final String[] PROTECTED_GET_ENDPOINTS = { + "/v1/api/members/my-info", + "/v1/api/members/my-space", + "/v1/api/members/tasting-notes/my", + "/v1/api/members/daily-lives/my", + "/v1/api/members/alcoholic-drinks/my", +}; +``` + +```java +// SecurityConfig.java 내 접근 정책 설정 예시 + + // 1️⃣ 완전 공개 엔드포인트 (우선순위 최상) + .requestMatchers(PUBLIC_ENDPOINTS).permitAll() + + // 2️⃣ CORS preflight 요청 + .requestMatchers(OPTIONS, "**").permitAll() + + // 3️⃣ 관리자 전용 엔드포인트 + .requestMatchers(ADMIN_ENDPOINTS).hasAuthority(MemberRole.ROLE_ADMIN.name()) + + // 4️⃣ 인증이 필요한 특정 GET 엔드포인트 + .requestMatchers(HttpMethod.GET, PROTECTED_GET_ENDPOINTS).authenticated() + + // 5️⃣ 나머지 GET 요청 (비인가 사용자에게 허용) + .requestMatchers(HttpMethod.GET, "**").permitAll() + + // 6️⃣ 나머지 모든 요청 (POST, PUT, DELETE 등 인증 필요) + .anyRequest().authenticated(); +``` + +## 새롭게 허용된 비회원 접근 허용 리소스 목록 + +### 시음노트 API + +| 메서드 | 경로 | 설명 | +| ------ | ------------------------------------------------------- | ------------------------------ | +| GET | `/shared-space/tasting-notes` | 전체 시음노트 목록 조회 | +| GET | `/shared-space/tasting-notes/by-alcoholicDrinks/{id}` | 특정 전통주 관련 시음노트 목록 | +| GET | `/shared-space/tasting-notes/{id}` | 시음노트 상세 정보 | +| GET | `/shared-space/tasting-notes/{id}/comments` | 댓글 목록 | +| GET | `/shared-space/tasting-notes/{id}/comments/{commentId}` | 답글 목록 | + +### 일상생활 API + +| 메서드 | 경로 | 설명 | +| ------ | ---------------------------------------- | ------------------ | +| GET | `/daily-lives` | 일상생활 목록 조회 | +| GET | `/daily-lives/{id}` | 일상생활 상세 정보 | +| GET | `/daily-lives/{id}/comments` | 댓글 목록 | +| GET | `/daily-lives/{id}/comments/{commentId}` | 답글 목록 | + +### 회원 API + +| 메서드 | 경로 | 설명 | +| ------ | ----------------------------- | ------------------------- | +| GET | `/members/{id}/profile` | 특정 유저의 공개 프로필 | +| GET | `/members/{id}/tasting-notes` | 특정 유저의 시음노트 목록 | +| GET | `/members/{id}/daily-lives` | 특정 유저의 일상생활 목록 | +| GET | `/members/{id}/followings` | 팔로잉 목록 | +| GET | `/members/{id}/followers` | 팔로워 목록 | +| GET | `/members/search` | 사용자 검색 | +| GET | `/members/recommendations` | 추천 사용자 조회 | + +## 향후 고려사항 + +- 비회원 트래픽 추이 및 콘텐츠 소비 패턴 모니터링 필요 +- 공개된 GET 리소스에 대해 Abuse 방지를 위한 Rate Limit 적용 검토 +- API 응답 데이터의 개인정보 포함 여부 재점검 및 마스킹 처리 필요 diff --git a/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteCommentQueryRepository.java b/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteCommentQueryRepository.java index e0923423..aee5d104 100644 --- a/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteCommentQueryRepository.java +++ b/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteCommentQueryRepository.java @@ -9,9 +9,9 @@ import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; -import io.jsonwebtoken.lang.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -19,6 +19,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Objects; @Repository @RequiredArgsConstructor @@ -29,45 +30,41 @@ public class TastingNoteCommentQueryRepository { QTastingNoteComment tastingNoteComment = QTastingNoteComment.tastingNoteComment; QTastingNoteCommentLike tastingNoteCommentLike = QTastingNoteCommentLike.tastingNoteCommentLike; - public Slice getAllByTastingNoteId(Member member, Long tastingNoteId, Long lastCommentId, int pageSize) { + public Slice getAllByTastingNoteId(Member member, Long tastingNoteId, Long lastCommentId, + int pageSize) { QTastingNoteComment reply = new QTastingNoteComment("reply"); List commentSummaryList = jpaQueryFactory - .select( - Projections.constructor( - CommentSummary.class, - tastingNoteComment.content, - tastingNoteComment.id, - Projections.constructor( - MemberInfo.class, - tastingNoteComment.member.id, - tastingNoteComment.member.nickname, - tastingNoteComment.member.profileImage - ), - tastingNoteComment.createdAt, - tastingNoteCommentLike.count().as("likeCount"), - JPAExpressions.select(reply.count()) - .from(reply) - .where( - reply.parent.id.eq(tastingNoteComment.id), - isNotDeleted(reply) - ), - isLikedSubQuery(tastingNoteComment, member, tastingNoteCommentLike), - new CaseBuilder() - .when(tastingNoteComment.deletedAt.isNotNull()).then(true) - .otherwise(false) - ) - ) - .from(tastingNoteComment) - .leftJoin(tastingNoteCommentLike).on(tastingNoteCommentLike.tastingNoteComment.eq(tastingNoteComment)) - .where( - tastingNoteComment.tastingNote.id.eq(tastingNoteId), - isNotReply(tastingNoteComment), - noOffsetByCommentId(tastingNoteComment, lastCommentId) - ) - .groupBy(tastingNoteComment.id) - .orderBy(tastingNoteComment.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + CommentSummary.class, + tastingNoteComment.content, + tastingNoteComment.id, + Projections.constructor( + MemberInfo.class, + tastingNoteComment.member.id, + tastingNoteComment.member.nickname, + tastingNoteComment.member.profileImage), + tastingNoteComment.createdAt, + tastingNoteCommentLike.count().as("likeCount"), + JPAExpressions.select(reply.count()) + .from(reply) + .where( + reply.parent.id.eq(tastingNoteComment.id), + isNotDeleted(reply)), + isLikedSubQuery(tastingNoteComment, member, tastingNoteCommentLike), + new CaseBuilder() + .when(tastingNoteComment.deletedAt.isNotNull()).then(true) + .otherwise(false))) + .from(tastingNoteComment) + .leftJoin(tastingNoteCommentLike).on(tastingNoteCommentLike.tastingNoteComment.eq(tastingNoteComment)) + .where( + tastingNoteComment.tastingNote.id.eq(tastingNoteId), + isNotReply(tastingNoteComment), + noOffsetByCommentId(tastingNoteComment, lastCommentId)) + .groupBy(tastingNoteComment.id) + .orderBy(tastingNoteComment.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = commentSummaryList.size() > pageSize; if (hasNext) { @@ -77,38 +74,35 @@ public Slice getAllByTastingNoteId(Member member, Long tastingNo return new SliceImpl<>(commentSummaryList, PageRequest.ofSize(pageSize), hasNext); } - public Slice getAllRepliesByParentId(Member member, Long tastingNoteId, Long tastingNoteCommentId, Long lastReplyId, int pageSize) { + public Slice getAllRepliesByParentId(Member member, Long tastingNoteId, Long tastingNoteCommentId, + Long lastReplyId, int pageSize) { List replySummaryList = jpaQueryFactory - .select( - Projections.constructor( - ReplySummary.class, - tastingNoteComment.content, - tastingNoteComment.id, - Projections.constructor( - MemberInfo.class, - tastingNoteComment.member.id, - tastingNoteComment.member.nickname, - tastingNoteComment.member.profileImage - ), - tastingNoteComment.createdAt, - tastingNoteCommentLike.count().as("likeCount"), - isLikedSubQuery(tastingNoteComment, member, tastingNoteCommentLike), - new CaseBuilder() - .when(tastingNoteComment.deletedAt.isNotNull()).then(true) - .otherwise(false) - ) - ) - .from(tastingNoteComment) - .leftJoin(tastingNoteCommentLike).on(tastingNoteCommentLike.tastingNoteComment.eq(tastingNoteComment)) - .where( - tastingNoteComment.tastingNote.id.eq(tastingNoteId), - tastingNoteComment.parent.id.eq(tastingNoteCommentId), - noOffsetByCommentId(tastingNoteComment, lastReplyId) - ) - .groupBy(tastingNoteComment.id) - .orderBy(tastingNoteComment.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + ReplySummary.class, + tastingNoteComment.content, + tastingNoteComment.id, + Projections.constructor( + MemberInfo.class, + tastingNoteComment.member.id, + tastingNoteComment.member.nickname, + tastingNoteComment.member.profileImage), + tastingNoteComment.createdAt, + tastingNoteCommentLike.count().as("likeCount"), + isLikedSubQuery(tastingNoteComment, member, tastingNoteCommentLike), + new CaseBuilder() + .when(tastingNoteComment.deletedAt.isNotNull()).then(true) + .otherwise(false))) + .from(tastingNoteComment) + .leftJoin(tastingNoteCommentLike).on(tastingNoteCommentLike.tastingNoteComment.eq(tastingNoteComment)) + .where( + tastingNoteComment.tastingNote.id.eq(tastingNoteId), + tastingNoteComment.parent.id.eq(tastingNoteCommentId), + noOffsetByCommentId(tastingNoteComment, lastReplyId)) + .groupBy(tastingNoteComment.id) + .orderBy(tastingNoteComment.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = replySummaryList.size() > pageSize; if (hasNext) { @@ -122,14 +116,19 @@ private BooleanExpression isNotDeleted(QTastingNoteComment tastingNoteComment) { return tastingNoteComment.deletedAt.isNull(); } - private BooleanExpression isLikedSubQuery(QTastingNoteComment tastingNoteComment, Member member, QTastingNoteCommentLike tastingNoteCommentLike) { + private BooleanExpression isLikedSubQuery(QTastingNoteComment tastingNoteComment, Member member, + QTastingNoteCommentLike tastingNoteCommentLike) { + // 비인가 사용자에 대한 좋아요 조회 처리 + if (Objects.isNull(member)) { + return Expressions.FALSE; + } + return jpaQueryFactory - .selectFrom(tastingNoteComment) - .where( - tastingNoteCommentLike.tastingNoteComment.eq(tastingNoteComment), - tastingNoteCommentLike.member.eq(member) - ) - .exists(); + .selectFrom(tastingNoteComment) + .where( + tastingNoteCommentLike.tastingNoteComment.eq(tastingNoteComment), + tastingNoteCommentLike.member.eq(member)) + .exists(); } private BooleanExpression isNotReply(QTastingNoteComment tastingNoteComment) { @@ -137,6 +136,6 @@ private BooleanExpression isNotReply(QTastingNoteComment tastingNoteComment) { } private BooleanExpression noOffsetByCommentId(QTastingNoteComment tastingNoteComment, Long lastCommentId) { - return Objects.isEmpty(lastCommentId) ? null : tastingNoteComment.id.lt(lastCommentId); + return Objects.isNull(lastCommentId) ? null : tastingNoteComment.id.lt(lastCommentId); } } diff --git a/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteQueryRepository.java b/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteQueryRepository.java index a228f007..1d1ad3d6 100644 --- a/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteQueryRepository.java +++ b/src/main/java/com/juu/juulabel/alcohol/repository/query/TastingNoteQueryRepository.java @@ -16,6 +16,7 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; @@ -47,7 +48,10 @@ public class TastingNoteQueryRepository { QTastingNoteLike tastingNoteLike = QTastingNoteLike.tastingNoteLike; QTastingNoteComment tastingNoteComment = QTastingNoteComment.tastingNoteComment; - public Slice findAllAlcoholicDrinks(String search, String lastAlcoholicDrinksName, int pageSize) { + private static final String THUMBNAIL_PATH = "thumbnailPath"; + + public Slice findAllAlcoholicDrinks(String search, String lastAlcoholicDrinksName, + int pageSize) { List alcoholicDrinksList = jpaQueryFactory .select( Projections.constructor( @@ -59,24 +63,19 @@ public Slice findAllAlcoholicDrinks(String search, Strin Projections.constructor( AlcoholTypeSummary.class, alcoholType.id, - alcoholType.name - ), + alcoholType.name), Projections.constructor( BrewerySummary.class, brewery.id, brewery.name, brewery.region, - brewery.message - ) - ) - ) + brewery.message))) .from(alcoholicDrinks) .innerJoin(alcoholType).on(alcoholicDrinks.alcoholType.eq(alcoholType)) .innerJoin(brewery).on(alcoholicDrinks.brewery.eq(brewery)) .where( containSearch(alcoholicDrinks, search), - noOffsetAlcoholicDrinksName(alcoholicDrinks, lastAlcoholicDrinksName) - ) + noOffsetAlcoholicDrinksName(alcoholicDrinks, lastAlcoholicDrinksName)) .orderBy(alcoholicDrinksNameAsc(alcoholicDrinks)) .limit(pageSize + 1L) .fetch(); @@ -91,36 +90,32 @@ public Slice findAllAlcoholicDrinks(String search, Strin public Slice getAllTastingNotes(Member member, Long lastTastingNoteId, int pageSize) { List tastingNoteSummaryList = jpaQueryFactory - .select( - Projections.constructor( - TastingNoteSummary.class, - tastingNote.id, - tastingNote.alcoholDrinksInfo.alcoholicDrinksName, - Projections.constructor( - MemberInfo.class, - tastingNote.member.id, - tastingNote.member.nickname, - tastingNote.member.profileImage - ), - tastingNoteImage.imagePath.as("thumbnailPath"), - tastingNote.alcoholDrinksInfo.alcoholTypeName, - tastingNote.createdAt, - hasMultipleImagesSubQuery(tastingNote, tastingNoteImage) - ) - ) - .from(tastingNote) - .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) - .and(tastingNoteImage.seq.eq(1)) - .and(isNotDeleted(tastingNoteImage))) - .where( - isNotPrivateOrAuthor(tastingNote, member), - isNotDeleted(tastingNote), - noOffsetByTastingNoteId(tastingNote, lastTastingNoteId) - ) - .groupBy(tastingNote.id) - .orderBy(tastingNote.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + TastingNoteSummary.class, + tastingNote.id, + tastingNote.alcoholDrinksInfo.alcoholicDrinksName, + Projections.constructor( + MemberInfo.class, + tastingNote.member.id, + tastingNote.member.nickname, + tastingNote.member.profileImage), + tastingNoteImage.imagePath.as(THUMBNAIL_PATH), + tastingNote.alcoholDrinksInfo.alcoholTypeName, + tastingNote.createdAt, + hasMultipleImagesSubQuery(tastingNote, tastingNoteImage))) + .from(tastingNote) + .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) + .and(tastingNoteImage.seq.eq(1)) + .and(isNotDeleted(tastingNoteImage))) + .where( + getPrivacyCondition(tastingNote, member), + isNotDeleted(tastingNote), + noOffsetByTastingNoteId(tastingNote, lastTastingNoteId)) + .groupBy(tastingNote.id) + .orderBy(tastingNote.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = tastingNoteSummaryList.size() > pageSize; if (hasNext) { @@ -130,40 +125,37 @@ public Slice getAllTastingNotes(Member member, Long lastTast return new SliceImpl<>(tastingNoteSummaryList, PageRequest.ofSize(pageSize), hasNext); } - public Slice getAllTastingNotesByAlcoholicDrinksId(Member member, Long lastTastingNoteId, int pageSize, Long alcoholicDrinksId) { + public Slice getAllTastingNotesByAlcoholicDrinksId(Member member, + Long lastTastingNoteId, int pageSize, Long alcoholicDrinksId) { List tastingNoteSummaryList = jpaQueryFactory - .select( - Projections.constructor( - AlcoholicDrinksTastingNoteSummary.class, - tastingNote.id, - tastingNote.alcoholDrinksInfo.alcoholicDrinksName, - Projections.constructor( - MemberInfo.class, - tastingNote.member.id, - tastingNote.member.nickname, - tastingNote.member.profileImage - ), - tastingNoteImage.imagePath.as("thumbnailPath"), - tastingNote.alcoholDrinksInfo.alcoholTypeName, - tastingNote.createdAt, - hasMultipleImagesSubQuery(tastingNote, tastingNoteImage), - alcoholicDrinks.tastingNoteCount - ) - ) - .from(tastingNote) - .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) - .and(tastingNoteImage.seq.eq(1)) - .and(isNotDeleted(tastingNoteImage))) - .where( - isNotPrivateOrAuthor(tastingNote, member), - isNotDeleted(tastingNote), - noOffsetByTastingNoteId(tastingNote, lastTastingNoteId), - tastingNote.alcoholicDrinks.id.eq(alcoholicDrinksId) - ) - .groupBy(tastingNote.id) - .orderBy(tastingNote.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + AlcoholicDrinksTastingNoteSummary.class, + tastingNote.id, + tastingNote.alcoholDrinksInfo.alcoholicDrinksName, + Projections.constructor( + MemberInfo.class, + tastingNote.member.id, + tastingNote.member.nickname, + tastingNote.member.profileImage), + tastingNoteImage.imagePath.as(THUMBNAIL_PATH), + tastingNote.alcoholDrinksInfo.alcoholTypeName, + tastingNote.createdAt, + hasMultipleImagesSubQuery(tastingNote, tastingNoteImage), + alcoholicDrinks.tastingNoteCount)) + .from(tastingNote) + .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) + .and(tastingNoteImage.seq.eq(1)) + .and(isNotDeleted(tastingNoteImage))) + .where( + getPrivacyCondition(tastingNote, member), + isNotDeleted(tastingNote), + noOffsetByTastingNoteId(tastingNote, lastTastingNoteId), + tastingNote.alcoholicDrinks.id.eq(alcoholicDrinksId)) + .groupBy(tastingNote.id) + .orderBy(tastingNote.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = tastingNoteSummaryList.size() > pageSize; if (hasNext) { @@ -175,37 +167,33 @@ public Slice getAllTastingNotesByAlcoholicDri public Slice getAllMyTastingNotes(Member member, Long lastTastingNoteId, int pageSize) { List myTastingNoteSummaryList = jpaQueryFactory - .select( - Projections.constructor( - MyTastingNoteSummary.class, - tastingNote.id, - tastingNote.alcoholDrinksInfo.alcoholicDrinksName, - Projections.constructor( - MemberInfo.class, - tastingNote.member.id, - tastingNote.member.nickname, - tastingNote.member.profileImage - ), - tastingNoteImage.imagePath.as("thumbnailPath"), - tastingNote.alcoholDrinksInfo.alcoholTypeName, - tastingNote.createdAt, - hasMultipleImagesSubQuery(tastingNote, tastingNoteImage), - tastingNote.isPrivate - ) - ) - .from(tastingNote) - .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) - .and(tastingNoteImage.seq.eq(1)) - .and(isNotDeleted(tastingNoteImage))) - .where( - tastingNote.member.eq(member), - isNotDeleted(tastingNote), - noOffsetByTastingNoteId(tastingNote, lastTastingNoteId) - ) - .groupBy(tastingNote.id) - .orderBy(tastingNote.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + MyTastingNoteSummary.class, + tastingNote.id, + tastingNote.alcoholDrinksInfo.alcoholicDrinksName, + Projections.constructor( + MemberInfo.class, + tastingNote.member.id, + tastingNote.member.nickname, + tastingNote.member.profileImage), + tastingNoteImage.imagePath.as(THUMBNAIL_PATH), + tastingNote.alcoholDrinksInfo.alcoholTypeName, + tastingNote.createdAt, + hasMultipleImagesSubQuery(tastingNote, tastingNoteImage), + tastingNote.isPrivate)) + .from(tastingNote) + .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) + .and(tastingNoteImage.seq.eq(1)) + .and(isNotDeleted(tastingNoteImage))) + .where( + tastingNote.member.eq(member), + isNotDeleted(tastingNote), + noOffsetByTastingNoteId(tastingNote, lastTastingNoteId)) + .groupBy(tastingNote.id) + .orderBy(tastingNote.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = myTastingNoteSummaryList.size() > pageSize; if (hasNext) { @@ -215,39 +203,36 @@ public Slice getAllMyTastingNotes(Member member, Long last return new SliceImpl<>(myTastingNoteSummaryList, PageRequest.ofSize(pageSize), hasNext); } - public Slice getAllTastingNotesByMember(Member loginMember, Long memberId, Long lastTastingNoteId, int pageSize) { + public Slice getAllTastingNotesByMember(Member loginMember, Long memberId, + Long lastTastingNoteId, int pageSize) { List tastingNoteSummaryList = jpaQueryFactory - .select( - Projections.constructor( - TastingNoteSummary.class, - tastingNote.id, - tastingNote.alcoholDrinksInfo.alcoholicDrinksName, - Projections.constructor( - MemberInfo.class, - tastingNote.member.id, - tastingNote.member.nickname, - tastingNote.member.profileImage - ), - tastingNoteImage.imagePath.as("thumbnailPath"), - tastingNote.alcoholDrinksInfo.alcoholTypeName, - tastingNote.createdAt, - hasMultipleImagesSubQuery(tastingNote, tastingNoteImage) - ) - ) - .from(tastingNote) - .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) - .and(tastingNoteImage.seq.eq(1)) - .and(isNotDeleted(tastingNoteImage))) - .where( - tastingNote.member.id.eq(memberId), - isNotPrivateOrAuthor(tastingNote, loginMember), - isNotDeleted(tastingNote), - noOffsetByTastingNoteId(tastingNote, lastTastingNoteId) - ) - .groupBy(tastingNote.id) - .orderBy(tastingNote.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + TastingNoteSummary.class, + tastingNote.id, + tastingNote.alcoholDrinksInfo.alcoholicDrinksName, + Projections.constructor( + MemberInfo.class, + tastingNote.member.id, + tastingNote.member.nickname, + tastingNote.member.profileImage), + tastingNoteImage.imagePath.as(THUMBNAIL_PATH), + tastingNote.alcoholDrinksInfo.alcoholTypeName, + tastingNote.createdAt, + hasMultipleImagesSubQuery(tastingNote, tastingNoteImage))) + .from(tastingNote) + .leftJoin(tastingNoteImage).on(tastingNoteImage.tastingNote.eq(tastingNote) + .and(tastingNoteImage.seq.eq(1)) + .and(isNotDeleted(tastingNoteImage))) + .where( + tastingNote.member.id.eq(memberId), + getPrivacyCondition(tastingNote, loginMember), + isNotDeleted(tastingNote), + noOffsetByTastingNoteId(tastingNote, lastTastingNoteId)) + .groupBy(tastingNote.id) + .orderBy(tastingNote.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = tastingNoteSummaryList.size() > pageSize; if (hasNext) { @@ -257,45 +242,41 @@ public Slice getAllTastingNotesByMember(Member loginMember, return new SliceImpl<>(tastingNoteSummaryList, PageRequest.ofSize(pageSize), hasNext); } - public long countBySearch (String search){ + public long countBySearch(String search) { return jpaQueryFactory .select(alcoholicDrinks.count()) .from(alcoholicDrinks) .where( - alcoholicDrinks.name.contains(search). - or(brewery.name.contains(search)), - isNotDeleted(alcoholicDrinks) - ) + alcoholicDrinks.name.contains(search).or(brewery.name.contains(search)), + isNotDeleted(alcoholicDrinks)) .fetchOne(); } public long getMyTastingNoteCount(Member member) { Long tastingNoteCount = jpaQueryFactory - .select(tastingNote.count()) - .from(tastingNote) - .where( - tastingNote.member.eq(member), - isNotDeleted(tastingNote) - ) - .fetchOne(); + .select(tastingNote.count()) + .from(tastingNote) + .where( + tastingNote.member.eq(member), + isNotDeleted(tastingNote)) + .fetchOne(); return Optional.ofNullable(tastingNoteCount) - .orElseThrow(() -> new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_FOUND)); + .orElseThrow(() -> new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_FOUND)); } public long getTastingNoteCountByMemberId(Long memberId, Member loginMember) { Long tastingNoteCount = jpaQueryFactory - .select(tastingNote.count()) - .from(tastingNote) - .where( - tastingNote.member.id.eq(memberId), - isNotPrivateOrAuthor(tastingNote, loginMember), - isNotDeleted(tastingNote) - ) - .fetchOne(); + .select(tastingNote.count()) + .from(tastingNote) + .where( + tastingNote.member.id.eq(memberId), + getPrivacyCondition(tastingNote, loginMember), + isNotDeleted(tastingNote)) + .fetchOne(); return Optional.ofNullable(tastingNoteCount) - .orElseThrow(() -> new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_FOUND)); + .orElseThrow(() -> new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_FOUND)); } private OrderSpecifier alcoholicDrinksNameAsc(QAlcoholicDrinks alcoholicDrinks) { @@ -311,7 +292,8 @@ private BooleanExpression containSearch(QAlcoholicDrinks alcoholicDrinks, String .or(brewery.name.containsIgnoreCase(search)); } - private BooleanExpression noOffsetAlcoholicDrinksName(QAlcoholicDrinks alcoholicDrinks, String lastAlcoholicDrinksName) { + private BooleanExpression noOffsetAlcoholicDrinksName(QAlcoholicDrinks alcoholicDrinks, + String lastAlcoholicDrinksName) { if (Objects.isNull(lastAlcoholicDrinksName)) { return null; } @@ -323,6 +305,10 @@ private BooleanExpression noOffsetByTastingNoteId(QTastingNote tastingNote, Long return io.jsonwebtoken.lang.Objects.isEmpty(lastTastingNoteId) ? null : tastingNote.id.lt(lastTastingNoteId); } + private BooleanExpression getPrivacyCondition(QTastingNote tastingNote, Member member) { + return Objects.isNull(member) ? isNotPrivate(tastingNote) : isNotPrivateOrAuthor(tastingNote, member); + } + private BooleanExpression isNotPrivate(QTastingNote tastingNote) { return tastingNote.isPrivate.isFalse(); } @@ -339,115 +325,111 @@ private BooleanExpression isNotDeleted(QTastingNoteImage tastingNoteImage) { return tastingNoteImage.deletedAt.isNull(); } - private BooleanExpression isNotDeleted (QAlcoholicDrinks alcoholicDrinks){ + private BooleanExpression isNotDeleted(QAlcoholicDrinks alcoholicDrinks) { return alcoholicDrinks.deletedAt.isNull(); } private BooleanExpression hasMultipleImagesSubQuery(QTastingNote tastingNote, QTastingNoteImage tastingNoteImage) { return jpaQueryFactory - .selectFrom(tastingNoteImage) - .where( - tastingNoteImage.tastingNote.eq(tastingNote), - isNotDeleted(tastingNoteImage) - ) - .groupBy(tastingNoteImage.tastingNote) - .having(tastingNoteImage.count().goe(2)) - .exists(); + .selectFrom(tastingNoteImage) + .where( + tastingNoteImage.tastingNote.eq(tastingNote), + isNotDeleted(tastingNoteImage)) + .groupBy(tastingNoteImage.tastingNote) + .having(tastingNoteImage.count().goe(2)) + .exists(); } public TastingNoteDetailInfo getTastingNoteDetailById(Long tastingNoteId, Member member) { TastingNoteDetailInfo tastingNoteDetailInfo = jpaQueryFactory - .select( - Projections.constructor( - TastingNoteDetailInfo.class, - tastingNote.id, - Projections.constructor( - MemberInfo.class, - tastingNote.member.id, - tastingNote.member.nickname, - tastingNote.member.profileImage - ), - tastingNote.createdAt, - tastingNote.alcoholDrinksInfo.alcoholicDrinksName, - tastingNote.alcoholDrinksInfo.alcoholTypeName, - tastingNote.alcoholDrinksInfo.alcoholContent, - tastingNote.alcoholDrinksInfo.breweryName, - tastingNote.color.rgb, -// getSensoryLevelIds(tastingNote, tastingNoteSensoryLevel, sensoryLevel), -// getScentIds(tastingNote, tastingNoteScent, scent), -// getFlavorLevelIds(tastingNote, tastingNoteFlavorLevel, flavorLevel), - tastingNote.content, - tastingNote.rating, - tastingNoteLike.countDistinct().as("likeCount"), - tastingNoteComment.countDistinct().as("commentCount"), - isLikedSubQuery(tastingNote, member) - ) - ) - .from(tastingNote) - .leftJoin(tastingNoteLike).on(tastingNoteLike.tastingNote.eq(tastingNote)) - .leftJoin(tastingNoteComment).on(tastingNoteComment.tastingNote.eq(tastingNote)) - .where( - eqId(tastingNote, tastingNoteId), - isNotPrivateOrAuthor(tastingNote, member), - isNotDeleted(tastingNote) - ) - .groupBy(tastingNote.id) - .fetchOne(); + .select( + Projections.constructor( + TastingNoteDetailInfo.class, + tastingNote.id, + Projections.constructor( + MemberInfo.class, + tastingNote.member.id, + tastingNote.member.nickname, + tastingNote.member.profileImage), + tastingNote.createdAt, + tastingNote.alcoholDrinksInfo.alcoholicDrinksName, + tastingNote.alcoholDrinksInfo.alcoholTypeName, + tastingNote.alcoholDrinksInfo.alcoholContent, + tastingNote.alcoholDrinksInfo.breweryName, + tastingNote.color.rgb, + // getSensoryLevelIds(tastingNote, tastingNoteSensoryLevel, sensoryLevel), + // getScentIds(tastingNote, tastingNoteScent, scent), + // getFlavorLevelIds(tastingNote, tastingNoteFlavorLevel, flavorLevel), + tastingNote.content, + tastingNote.rating, + tastingNoteLike.countDistinct().as("likeCount"), + tastingNoteComment.countDistinct().as("commentCount"), + isLikedSubQuery(tastingNote, member))) + .from(tastingNote) + .leftJoin(tastingNoteLike).on(tastingNoteLike.tastingNote.eq(tastingNote)) + .leftJoin(tastingNoteComment).on(tastingNoteComment.tastingNote.eq(tastingNote)) + .where( + eqId(tastingNote, tastingNoteId), + getPrivacyCondition(tastingNote, member), + isNotDeleted(tastingNote)) + .groupBy(tastingNote.id) + .fetchOne(); return Optional.ofNullable(tastingNoteDetailInfo) - .orElseThrow(() -> new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_FOUND)); + .orElseThrow(() -> new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_FOUND)); } public List getSensoryLevelIds(Long tastingNoteId, Member member) { return jpaQueryFactory - .select(sensoryLevel.id) - .from(tastingNoteSensoryLevel) - .join(tastingNoteSensoryLevel.sensoryLevel, sensoryLevel) - .where( - tastingNoteSensoryLevel.tastingNote.eq(tastingNote), - eqId(tastingNote, tastingNoteId), - isNotPrivateOrAuthor(tastingNote, member), - isNotDeleted(tastingNote) - ) - .fetch(); + .select(sensoryLevel.id) + .from(tastingNoteSensoryLevel) + .join(tastingNoteSensoryLevel.sensoryLevel, sensoryLevel) + .where( + tastingNoteSensoryLevel.tastingNote.eq(tastingNote), + eqId(tastingNote, tastingNoteId), + getPrivacyCondition(tastingNote, member), + isNotDeleted(tastingNote)) + .fetch(); } public List getScentIds(Long tastingNoteId, Member member) { return jpaQueryFactory - .select(scent.id) - .from(tastingNoteScent) - .join(tastingNoteScent.scent, scent) - .where( - tastingNoteScent.tastingNote.eq(tastingNote), - eqId(tastingNote, tastingNoteId), - isNotPrivateOrAuthor(tastingNote, member), - isNotDeleted(tastingNote) - ) - .fetch(); + .select(scent.id) + .from(tastingNoteScent) + .join(tastingNoteScent.scent, scent) + .where( + tastingNoteScent.tastingNote.eq(tastingNote), + eqId(tastingNote, tastingNoteId), + getPrivacyCondition(tastingNote, member), + isNotDeleted(tastingNote)) + .fetch(); } public List getFlavorLevelIds(Long tastingNoteId, Member member) { return jpaQueryFactory - .select(flavorLevel.id) - .from(tastingNoteFlavorLevel) - .join(tastingNoteFlavorLevel.flavorLevel, flavorLevel) - .where( - tastingNoteFlavorLevel.tastingNote.eq(tastingNote), - eqId(tastingNote, tastingNoteId), - isNotPrivateOrAuthor(tastingNote, member), - isNotDeleted(tastingNote) - ) - .fetch(); + .select(flavorLevel.id) + .from(tastingNoteFlavorLevel) + .join(tastingNoteFlavorLevel.flavorLevel, flavorLevel) + .where( + tastingNoteFlavorLevel.tastingNote.eq(tastingNote), + eqId(tastingNote, tastingNoteId), + getPrivacyCondition(tastingNote, member), + isNotDeleted(tastingNote)) + .fetch(); } private BooleanExpression isLikedSubQuery(QTastingNote tastingNote, Member member) { + // 로그인 안한 경우 좋아요 여부 체크 안함 + if (Objects.isNull(member)) { + return Expressions.FALSE; + } + return jpaQueryFactory - .selectFrom(tastingNoteLike) - .where( - tastingNoteLike.tastingNote.eq(tastingNote), - tastingNoteLike.member.eq(member) - ) - .exists(); + .selectFrom(tastingNoteLike) + .where( + tastingNoteLike.tastingNote.eq(tastingNote), + tastingNoteLike.member.eq(member)) + .exists(); } private BooleanExpression eqId(QTastingNote tastingNote, Long tastingNoteId) { @@ -456,11 +438,10 @@ private BooleanExpression eqId(QTastingNote tastingNote, Long tastingNoteId) { public Long getAlcoholicDrinksByTastingNoteId(Long tastingNoteId) { return jpaQueryFactory - .select(alcoholicDrinks.id) - .from(tastingNote) - .where( - eqId(tastingNote, tastingNoteId) - ) - .fetchOne(); + .select(alcoholicDrinks.id) + .from(tastingNote) + .where( + eqId(tastingNote, tastingNoteId)) + .fetchOne(); } } diff --git a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java index 389e8691..c51fd8b0 100644 --- a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java +++ b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java @@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; @@ -16,7 +17,6 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; import java.util.List; import static org.springframework.http.HttpMethod.OPTIONS; @@ -28,21 +28,27 @@ public class SecurityConfig { private final JwtAuthorizationFilter jwtAuthenticationFilter; private final JwtExceptionFilter jwtExceptionFilter; - // Public endpoints that don't require authentication + // 완전 공개 엔드 포인트 (우선순위 최상) private static final String[] PUBLIC_ENDPOINTS = { "/swagger-ui/**", "/v3/api-docs/**", "/error", "/favicon.ico", "/", "/actuator/**", - "/v1/api/alcohols/**", "/v1/api/terms/**", "/v1/api/images", - "/v1/api/auth/**", "/v1/api/shared-space/tasting-notes/**", "/v1/api/notifications/**", - "/v1/api/daily-lives/**", "/v1/api/alcoholicDrinks/**", "v1/api/follow", "/**", - "/v1/api/reports" + "/v1/api/auth/login/**", "/v1/api/auth/sign-up" }; - // Admin-only endpoints + // 관리자 전용 엔드포인트 private static final String[] ADMIN_ENDPOINTS = { "/v1/api/admins/permission/test" }; - // Allowed origins for CORS + // 인증/인가 필요한 특정 GET 엔드포인트 + private static final String[] PROTECTED_GET_ENDPOINTS = { + "/v1/api/members/my-info", + "/v1/api/members/my-space", + "/v1/api/members/tasting-notes/my", + "/v1/api/members/daily-lives/my", + "/v1/api/members/alcoholic-drinks/my" + }; + + // CORS 허용 원본 private static final String[] ALLOWED_ORIGINS = { "http://localhost:8084", "http://localhost:8080", @@ -52,9 +58,6 @@ public class SecurityConfig { "https://dev.juulabel.com", "https://qa.juulabel.com", "https://juulabel.com", - "https://juulabel.shop", - "https://juulabel-front.vercel.app/", - "https://juulabel-front-seven.vercel.app/", "https://d3jwyw9rpnxu8p.cloudfront.net" }; @@ -85,23 +88,28 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .build(); } + // Spring Security processes authorization rules in order, and the first match + // wins private void configureAuthorization( org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry authorize) { authorize - // Allow OPTIONS requests for CORS preflight - .requestMatchers(OPTIONS, "**").permitAll() - - // Public endpoints + // 1️⃣ 완전 공개 엔드포인트 (우선순위 최상) .requestMatchers(PUBLIC_ENDPOINTS).permitAll() - // Admin endpoints + // 2️⃣ CORS preflight 요청 + .requestMatchers(OPTIONS, "**").permitAll() + + // 3️⃣ 관리자 전용 엔드포인트 .requestMatchers(ADMIN_ENDPOINTS).hasAuthority(MemberRole.ROLE_ADMIN.name()) - // Specific authenticated endpoints - .requestMatchers("/v1/api/members/logout").authenticated() + // 4️⃣ 인증이 필요한 특정 GET 엔드포인트 + .requestMatchers(HttpMethod.GET, PROTECTED_GET_ENDPOINTS).authenticated(); + + // 5️⃣ 나머지 GET 요청 (비인가 사용자에게 허용) + authorize.requestMatchers(HttpMethod.GET, "**").permitAll(); - // All other requests require authentication - .anyRequest().authenticated(); + // 6️⃣ 나머지 POST, PUT, DELETE 요청 (기본적으로 인증 필요) + authorize.anyRequest().authenticated(); } @Bean @@ -111,7 +119,6 @@ public UrlBasedCorsConfigurationSource corsConfigurationSource() { // Configure CORS settings config.addAllowedHeader("*"); config.addAllowedMethod("*"); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedOrigins(List.of(ALLOWED_ORIGINS)); config.addExposedHeader(HttpHeaders.AUTHORIZATION); config.setAllowCredentials(true); diff --git a/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeCommentQueryRepository.java b/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeCommentQueryRepository.java index 71f2b14e..57964cb7 100644 --- a/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeCommentQueryRepository.java +++ b/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeCommentQueryRepository.java @@ -9,9 +9,9 @@ import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; -import io.jsonwebtoken.lang.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -19,6 +19,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Objects; @Repository @RequiredArgsConstructor @@ -29,45 +30,41 @@ public class DailyLifeCommentQueryRepository { QDailyLifeComment dailyLifeComment = QDailyLifeComment.dailyLifeComment; QDailyLifeCommentLike dailyLifeCommentLike = QDailyLifeCommentLike.dailyLifeCommentLike; - public Slice getAllByDailyLifeId(Member member, Long dailyLifeId, Long lastCommentId, int pageSize) { + public Slice getAllByDailyLifeId(Member member, Long dailyLifeId, Long lastCommentId, + int pageSize) { QDailyLifeComment reply = new QDailyLifeComment("reply"); List commentSummaryList = jpaQueryFactory - .select( - Projections.constructor( - CommentSummary.class, - dailyLifeComment.content, - dailyLifeComment.id, - Projections.constructor( - MemberInfo.class, - dailyLifeComment.member.id, - dailyLifeComment.member.nickname, - dailyLifeComment.member.profileImage - ), - dailyLifeComment.createdAt, - dailyLifeCommentLike.count().as("likeCount"), - JPAExpressions.select(reply.count()) - .from(reply) - .where( - reply.parent.id.eq(dailyLifeComment.id), - isNotDeleted(reply) - ), - isLikedSubQuery(dailyLifeComment, member, dailyLifeCommentLike), - new CaseBuilder() - .when(dailyLifeComment.deletedAt.isNotNull()).then(true) - .otherwise(false) - ) - ) - .from(dailyLifeComment) - .leftJoin(dailyLifeCommentLike).on(dailyLifeCommentLike.dailyLifeComment.eq(dailyLifeComment)) - .where( - dailyLifeComment.dailyLife.id.eq(dailyLifeId), - isNotReply(dailyLifeComment), - noOffsetByCommentId(dailyLifeComment, lastCommentId) - ) - .groupBy(dailyLifeComment.id) - .orderBy(dailyLifeComment.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + CommentSummary.class, + dailyLifeComment.content, + dailyLifeComment.id, + Projections.constructor( + MemberInfo.class, + dailyLifeComment.member.id, + dailyLifeComment.member.nickname, + dailyLifeComment.member.profileImage), + dailyLifeComment.createdAt, + dailyLifeCommentLike.count().as("likeCount"), + JPAExpressions.select(reply.count()) + .from(reply) + .where( + reply.parent.id.eq(dailyLifeComment.id), + isNotDeleted(reply)), + isLikedSubQuery(dailyLifeComment, member, dailyLifeCommentLike), + new CaseBuilder() + .when(dailyLifeComment.deletedAt.isNotNull()).then(true) + .otherwise(false))) + .from(dailyLifeComment) + .leftJoin(dailyLifeCommentLike).on(dailyLifeCommentLike.dailyLifeComment.eq(dailyLifeComment)) + .where( + dailyLifeComment.dailyLife.id.eq(dailyLifeId), + isNotReply(dailyLifeComment), + noOffsetByCommentId(dailyLifeComment, lastCommentId)) + .groupBy(dailyLifeComment.id) + .orderBy(dailyLifeComment.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = commentSummaryList.size() > pageSize; if (hasNext) { @@ -77,38 +74,35 @@ public Slice getAllByDailyLifeId(Member member, Long dailyLifeId return new SliceImpl<>(commentSummaryList, PageRequest.ofSize(pageSize), hasNext); } - public Slice getAllRepliesByParentId(Member member, Long dailyLifeId, Long dailyLifeCommentId, Long lastReplyId, int pageSize) { + public Slice getAllRepliesByParentId(Member member, Long dailyLifeId, Long dailyLifeCommentId, + Long lastReplyId, int pageSize) { List replySummaryList = jpaQueryFactory - .select( - Projections.constructor( - ReplySummary.class, - dailyLifeComment.content, - dailyLifeComment.id, - Projections.constructor( - MemberInfo.class, - dailyLifeComment.member.id, - dailyLifeComment.member.nickname, - dailyLifeComment.member.profileImage - ), - dailyLifeComment.createdAt, - dailyLifeCommentLike.count().as("likeCount"), - isLikedSubQuery(dailyLifeComment, member, dailyLifeCommentLike), - new CaseBuilder() - .when(dailyLifeComment.deletedAt.isNotNull()).then(true) - .otherwise(false) - ) - ) - .from(dailyLifeComment) - .leftJoin(dailyLifeCommentLike).on(dailyLifeCommentLike.dailyLifeComment.eq(dailyLifeComment)) - .where( - dailyLifeComment.dailyLife.id.eq(dailyLifeId), - dailyLifeComment.parent.id.eq(dailyLifeCommentId), - noOffsetByCommentId(dailyLifeComment, lastReplyId) - ) - .groupBy(dailyLifeComment.id) - .orderBy(dailyLifeComment.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + ReplySummary.class, + dailyLifeComment.content, + dailyLifeComment.id, + Projections.constructor( + MemberInfo.class, + dailyLifeComment.member.id, + dailyLifeComment.member.nickname, + dailyLifeComment.member.profileImage), + dailyLifeComment.createdAt, + dailyLifeCommentLike.count().as("likeCount"), + isLikedSubQuery(dailyLifeComment, member, dailyLifeCommentLike), + new CaseBuilder() + .when(dailyLifeComment.deletedAt.isNotNull()).then(true) + .otherwise(false))) + .from(dailyLifeComment) + .leftJoin(dailyLifeCommentLike).on(dailyLifeCommentLike.dailyLifeComment.eq(dailyLifeComment)) + .where( + dailyLifeComment.dailyLife.id.eq(dailyLifeId), + dailyLifeComment.parent.id.eq(dailyLifeCommentId), + noOffsetByCommentId(dailyLifeComment, lastReplyId)) + .groupBy(dailyLifeComment.id) + .orderBy(dailyLifeComment.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = replySummaryList.size() > pageSize; if (hasNext) { @@ -118,18 +112,23 @@ public Slice getAllRepliesByParentId(Member member, Long dailyLife return new SliceImpl<>(replySummaryList, PageRequest.ofSize(pageSize), hasNext); } - private BooleanExpression isLikedSubQuery(QDailyLifeComment dailyLifeComment, Member member, QDailyLifeCommentLike dailyLifeCommentLike) { + private BooleanExpression isLikedSubQuery(QDailyLifeComment dailyLifeComment, Member member, + QDailyLifeCommentLike dailyLifeCommentLike) { + // 로그인 안한 경우 좋아요 여부 체크 안함 + if (Objects.isNull(member)) { + return Expressions.FALSE; + } + return jpaQueryFactory - .selectFrom(dailyLifeComment) - .where( - dailyLifeCommentLike.dailyLifeComment.eq(dailyLifeComment), - dailyLifeCommentLike.member.eq(member) - ) - .exists(); + .selectFrom(dailyLifeComment) + .where( + dailyLifeCommentLike.dailyLifeComment.eq(dailyLifeComment), + dailyLifeCommentLike.member.eq(member)) + .exists(); } private BooleanExpression noOffsetByCommentId(QDailyLifeComment dailyLifeComment, Long lastCommentId) { - return Objects.isEmpty(lastCommentId) ? null : dailyLifeComment.id.lt(lastCommentId); + return Objects.isNull(lastCommentId) ? null : dailyLifeComment.id.lt(lastCommentId); } private BooleanExpression isNotReply(QDailyLifeComment dailyLifeComment) { diff --git a/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeQueryRepository.java b/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeQueryRepository.java index b10fe0b7..5171f96a 100644 --- a/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeQueryRepository.java +++ b/src/main/java/com/juu/juulabel/dailylife/repository/query/DailyLifeQueryRepository.java @@ -1,6 +1,5 @@ package com.juu.juulabel.dailylife.repository.query; - import com.juu.juulabel.common.exception.InvalidParamException; import com.juu.juulabel.common.exception.code.ErrorCode; import com.juu.juulabel.dailylife.domain.QDailyLife; @@ -14,10 +13,10 @@ import com.juu.juulabel.member.request.MemberInfo; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.JPQLQuery; import com.querydsl.jpa.impl.JPAQueryFactory; -import io.jsonwebtoken.lang.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -25,6 +24,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Objects; import java.util.Optional; @Repository @@ -40,76 +40,70 @@ public class DailyLifeQueryRepository { public DailyLifeDetailInfo getDailyLifeDetailById(Long dailyLifeId, Member member) { DailyLifeDetailInfo dailyLifeDetailInfo = jpaQueryFactory - .select( - Projections.constructor( - DailyLifeDetailInfo.class, - dailyLife.title, - dailyLife.content, - dailyLife.id, - Projections.constructor( - MemberInfo.class, - dailyLife.member.id, - dailyLife.member.nickname, - dailyLife.member.profileImage - ), - dailyLife.createdAt, - dailyLifeLike.countDistinct().as("likeCount"), - dailyLifeComment.countDistinct().as("commentCount"), - isLikedSubQuery(dailyLife, member) - ) - ) - .from(dailyLife) - .leftJoin(dailyLifeComment).on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) - .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) - .where( - eqId(dailyLife, dailyLifeId), - isNotPrivateOrAuthor(dailyLife, member), - isNotDeleted(dailyLife) - ) - .groupBy(dailyLife.id) - .fetchOne(); + .select( + Projections.constructor( + DailyLifeDetailInfo.class, + dailyLife.title, + dailyLife.content, + dailyLife.id, + Projections.constructor( + MemberInfo.class, + dailyLife.member.id, + dailyLife.member.nickname, + dailyLife.member.profileImage), + dailyLife.createdAt, + dailyLifeLike.countDistinct().as("likeCount"), + dailyLifeComment.countDistinct().as("commentCount"), + isLikedSubQuery(dailyLife, member))) + .from(dailyLife) + .leftJoin(dailyLifeComment) + .on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) + .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) + .where( + eqId(dailyLife, dailyLifeId), + getPrivacyCondition(dailyLife, member), + isNotDeleted(dailyLife)) + .groupBy(dailyLife.id) + .fetchOne(); return Optional.ofNullable(dailyLifeDetailInfo) - .orElseThrow(() -> new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_FOUND)); + .orElseThrow(() -> new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_FOUND)); } public Slice getAllDailyLife(Member member, Long lastDailyLifeId, int pageSize) { List dailyLifeSummaryList = jpaQueryFactory - .select( - Projections.constructor( - DailyLifeSummary.class, - dailyLife.title, - dailyLife.content, - dailyLife.id, - Projections.constructor( - MemberInfo.class, - dailyLife.member.id, - dailyLife.member.nickname, - dailyLife.member.profileImage - ), - dailyLifeImage.imagePath.as("thumbnailPath"), - getImageCountSubQuery(dailyLife), - dailyLife.createdAt, - dailyLifeLike.countDistinct().as("likeCount"), - dailyLifeComment.countDistinct().as("commentCount"), - isLikedSubQuery(dailyLife, member) - ) - ) - .from(dailyLife) - .leftJoin(dailyLifeComment).on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) - .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) - .leftJoin(dailyLifeImage).on(dailyLifeImage.dailyLife.eq(dailyLife) - .and(dailyLifeImage.seq.eq(1)) - .and(isNotDeleted(dailyLifeImage))) - .where( - isNotPrivateOrAuthor(dailyLife, member), - isNotDeleted(dailyLife), - noOffsetByDailyLifeId(dailyLife, lastDailyLifeId) - ) - .groupBy(dailyLife.id) - .orderBy(dailyLife.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + DailyLifeSummary.class, + dailyLife.title, + dailyLife.content, + dailyLife.id, + Projections.constructor( + MemberInfo.class, + dailyLife.member.id, + dailyLife.member.nickname, + dailyLife.member.profileImage), + dailyLifeImage.imagePath.as("thumbnailPath"), + getImageCountSubQuery(dailyLife), + dailyLife.createdAt, + dailyLifeLike.countDistinct().as("likeCount"), + dailyLifeComment.countDistinct().as("commentCount"), + isLikedSubQuery(dailyLife, member))) + .from(dailyLife) + .leftJoin(dailyLifeComment) + .on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) + .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) + .leftJoin(dailyLifeImage).on(dailyLifeImage.dailyLife.eq(dailyLife) + .and(dailyLifeImage.seq.eq(1)) + .and(isNotDeleted(dailyLifeImage))) + .where( + getPrivacyCondition(dailyLife, member), + isNotDeleted(dailyLife), + noOffsetByDailyLifeId(dailyLife, lastDailyLifeId)) + .groupBy(dailyLife.id) + .orderBy(dailyLife.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = dailyLifeSummaryList.size() > pageSize; if (hasNext) { @@ -121,42 +115,39 @@ public Slice getAllDailyLife(Member member, Long lastDailyLife public Slice getAllMyDailyLives(Member member, Long lastDailyLifeId, int pageSize) { List myDailyLifeSummaryList = jpaQueryFactory - .select( - Projections.constructor( - MyDailyLifeSummary.class, - dailyLife.title, - dailyLife.content, - dailyLife.id, - Projections.constructor( - MemberInfo.class, - dailyLife.member.id, - dailyLife.member.nickname, - dailyLife.member.profileImage - ), - dailyLifeImage.imagePath.as("thumbnailPath"), - getImageCountSubQuery(dailyLife), - dailyLife.createdAt, - dailyLifeLike.countDistinct().as("likeCount"), - dailyLifeComment.countDistinct().as("commentCount"), - dailyLife.isPrivate, - isLikedSubQuery(dailyLife, member) - ) - ) - .from(dailyLife) - .leftJoin(dailyLifeComment).on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) - .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) - .leftJoin(dailyLifeImage).on(dailyLifeImage.dailyLife.eq(dailyLife) - .and(dailyLifeImage.seq.eq(1)) - .and(isNotDeleted(dailyLifeImage))) - .where( - dailyLife.member.eq(member), - isNotDeleted(dailyLife), - noOffsetByDailyLifeId(dailyLife, lastDailyLifeId) - ) - .groupBy(dailyLife.id) - .orderBy(dailyLife.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + MyDailyLifeSummary.class, + dailyLife.title, + dailyLife.content, + dailyLife.id, + Projections.constructor( + MemberInfo.class, + dailyLife.member.id, + dailyLife.member.nickname, + dailyLife.member.profileImage), + dailyLifeImage.imagePath.as("thumbnailPath"), + getImageCountSubQuery(dailyLife), + dailyLife.createdAt, + dailyLifeLike.countDistinct().as("likeCount"), + dailyLifeComment.countDistinct().as("commentCount"), + dailyLife.isPrivate, + isLikedSubQuery(dailyLife, member))) + .from(dailyLife) + .leftJoin(dailyLifeComment) + .on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) + .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) + .leftJoin(dailyLifeImage).on(dailyLifeImage.dailyLife.eq(dailyLife) + .and(dailyLifeImage.seq.eq(1)) + .and(isNotDeleted(dailyLifeImage))) + .where( + dailyLife.member.eq(member), + isNotDeleted(dailyLife), + noOffsetByDailyLifeId(dailyLife, lastDailyLifeId)) + .groupBy(dailyLife.id) + .orderBy(dailyLife.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = myDailyLifeSummaryList.size() > pageSize; if (hasNext) { @@ -166,44 +157,42 @@ public Slice getAllMyDailyLives(Member member, Long lastDail return new SliceImpl<>(myDailyLifeSummaryList, PageRequest.ofSize(pageSize), hasNext); } - public Slice getAllDailyLivesByMember(Member loginMember, Long memberId, Long lastDailyLifeId, int pageSize) { + public Slice getAllDailyLivesByMember(Member loginMember, Long memberId, Long lastDailyLifeId, + int pageSize) { List dailyLifeSummaryList = jpaQueryFactory - .select( - Projections.constructor( - DailyLifeSummary.class, - dailyLife.title, - dailyLife.content, - dailyLife.id, - Projections.constructor( - MemberInfo.class, - dailyLife.member.id, - dailyLife.member.nickname, - dailyLife.member.profileImage - ), - dailyLifeImage.imagePath.as("thumbnailPath"), - getImageCountSubQuery(dailyLife), - dailyLife.createdAt, - dailyLifeLike.countDistinct().as("likeCount"), - dailyLifeComment.countDistinct().as("commentCount"), - isLikedSubQuery(dailyLife, loginMember) - ) - ) - .from(dailyLife) - .leftJoin(dailyLifeComment).on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) - .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) - .leftJoin(dailyLifeImage).on(dailyLifeImage.dailyLife.eq(dailyLife) - .and(dailyLifeImage.seq.eq(1)) - .and(isNotDeleted(dailyLifeImage))) - .where( - dailyLife.member.id.eq(memberId), - isNotPrivateOrAuthor(dailyLife, loginMember), - isNotDeleted(dailyLife), - noOffsetByDailyLifeId(dailyLife, lastDailyLifeId) - ) - .groupBy(dailyLife.id) - .orderBy(dailyLife.id.desc()) - .limit(pageSize + 1L) - .fetch(); + .select( + Projections.constructor( + DailyLifeSummary.class, + dailyLife.title, + dailyLife.content, + dailyLife.id, + Projections.constructor( + MemberInfo.class, + dailyLife.member.id, + dailyLife.member.nickname, + dailyLife.member.profileImage), + dailyLifeImage.imagePath.as("thumbnailPath"), + getImageCountSubQuery(dailyLife), + dailyLife.createdAt, + dailyLifeLike.countDistinct().as("likeCount"), + dailyLifeComment.countDistinct().as("commentCount"), + isLikedSubQuery(dailyLife, loginMember))) + .from(dailyLife) + .leftJoin(dailyLifeComment) + .on(dailyLifeComment.dailyLife.eq(dailyLife).and(isNotDeleted(dailyLifeComment))) + .leftJoin(dailyLifeLike).on(dailyLifeLike.dailyLife.eq(dailyLife)) + .leftJoin(dailyLifeImage).on(dailyLifeImage.dailyLife.eq(dailyLife) + .and(dailyLifeImage.seq.eq(1)) + .and(isNotDeleted(dailyLifeImage))) + .where( + dailyLife.member.id.eq(memberId), + getPrivacyCondition(dailyLife, loginMember), + isNotDeleted(dailyLife), + noOffsetByDailyLifeId(dailyLife, lastDailyLifeId)) + .groupBy(dailyLife.id) + .orderBy(dailyLife.id.desc()) + .limit(pageSize + 1L) + .fetch(); boolean hasNext = dailyLifeSummaryList.size() > pageSize; if (hasNext) { @@ -215,35 +204,37 @@ public Slice getAllDailyLivesByMember(Member loginMember, Long public long getMyDailyLifeCount(Member member) { Long dailyLifeCount = jpaQueryFactory - .select(dailyLife.count()) - .from(dailyLife) - .where( - dailyLife.member.eq(member), - isNotDeleted(dailyLife) - ) - .fetchOne(); + .select(dailyLife.count()) + .from(dailyLife) + .where( + dailyLife.member.eq(member), + isNotDeleted(dailyLife)) + .fetchOne(); return Optional.ofNullable(dailyLifeCount) - .orElseThrow(() -> new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_FOUND)); + .orElseThrow(() -> new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_FOUND)); } public long getDailyLifeCountByMemberId(Long memberId, Member loginMember) { Long dailyLifeCount = jpaQueryFactory - .select(dailyLife.count()) - .from(dailyLife) - .where( - dailyLife.member.id.eq(memberId), - isNotDeleted(dailyLife), - isNotPrivateOrAuthor(dailyLife, loginMember) - ) - .fetchOne(); + .select(dailyLife.count()) + .from(dailyLife) + .where( + dailyLife.member.id.eq(memberId), + isNotDeleted(dailyLife), + getPrivacyCondition(dailyLife, loginMember)) + .fetchOne(); return Optional.ofNullable(dailyLifeCount) - .orElseThrow(() -> new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_FOUND)); + .orElseThrow(() -> new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_FOUND)); } private BooleanExpression noOffsetByDailyLifeId(QDailyLife dailyLife, Long lastDailyLifeId) { - return Objects.isEmpty(lastDailyLifeId) ? null : dailyLife.id.lt(lastDailyLifeId); + return Objects.isNull(lastDailyLifeId) ? null : dailyLife.id.lt(lastDailyLifeId); + } + + private BooleanExpression getPrivacyCondition(QDailyLife dailyLife, Member member) { + return Objects.isNull(member) ? isNotPrivate(dailyLife) : isNotPrivateOrAuthor(dailyLife, member); } private BooleanExpression isNotDeleted(QDailyLife dailyLife) { @@ -254,6 +245,10 @@ private BooleanExpression isNotDeleted(QDailyLifeImage dailyLifeImage) { return dailyLifeImage.deletedAt.isNull(); } + private BooleanExpression isNotPrivate(QDailyLife dailyLife) { + return dailyLife.isPrivate.isFalse(); + } + private BooleanExpression isNotPrivateOrAuthor(QDailyLife dailyLife, Member member) { return dailyLife.isPrivate.isFalse().or(dailyLife.member.eq(member)); } @@ -267,22 +262,25 @@ private BooleanExpression eqId(QDailyLife dailyLife, Long dailyLifeId) { } private BooleanExpression isLikedSubQuery(QDailyLife dailyLife, Member member) { + // 로그인 안한 경우 좋아요 여부 체크 안함 + if (Objects.isNull(member)) { + return Expressions.FALSE; + } + return jpaQueryFactory - .selectFrom(dailyLifeLike) - .where( - dailyLifeLike.dailyLife.eq(dailyLife), - dailyLifeLike.member.eq(member) - ) - .exists(); + .selectFrom(dailyLifeLike) + .where( + dailyLifeLike.dailyLife.eq(dailyLife), + dailyLifeLike.member.eq(member)) + .exists(); } private JPQLQuery getImageCountSubQuery(QDailyLife dailyLife) { return JPAExpressions.select(dailyLifeImage.count()) - .from(dailyLifeImage) - .where( - dailyLifeImage.dailyLife.eq(dailyLife), - isNotDeleted(dailyLifeImage) - ); + .from(dailyLifeImage) + .where( + dailyLifeImage.dailyLife.eq(dailyLife), + isNotDeleted(dailyLifeImage)); } } diff --git a/src/main/java/com/juu/juulabel/follow/repository/FollowReader.java b/src/main/java/com/juu/juulabel/follow/repository/FollowReader.java index 4d009868..0388b966 100644 --- a/src/main/java/com/juu/juulabel/follow/repository/FollowReader.java +++ b/src/main/java/com/juu/juulabel/follow/repository/FollowReader.java @@ -8,6 +8,9 @@ import com.juu.juulabel.follow.repository.jpa.FollowJpaRepository; import com.juu.juulabel.follow.repository.query.FollowQueryRepository; import lombok.RequiredArgsConstructor; + +import java.util.Objects; + import org.springframework.data.domain.Slice; @Reader @@ -23,46 +26,49 @@ public Follow findOrNullByFollowerAndFollowee(final Member follower, final Membe } public Slice findAllFollowing(final Member loginMember, - final Member member, - final Long lastFollowId, - final int pageSize) { + final Member member, + final Long lastFollowId, + final int pageSize) { return followQueryRepository.findAllFollowing(loginMember, member, lastFollowId, pageSize); } public Slice findAllFollower(final Member loginMember, - final Member member, - final Long lastFollowId, - final int pageSize) { + final Member member, + final Long lastFollowId, + final int pageSize) { return followQueryRepository.findAllFollower(loginMember, member, lastFollowId, pageSize); } public Slice getSearchFollowUser(final Member loginMember, - final Long lastFollowId, - final int pageSize, - final String username) { - return followQueryRepository.getSearchUserList(loginMember,lastFollowId, pageSize, username); + final Long lastFollowId, + final int pageSize, + final String username) { + return followQueryRepository.getSearchUserList(loginMember, lastFollowId, pageSize, username); } public RecommendListResponse getRecommendUserList(final Member loginMember, - final Long badgeLastUserId, - final Long tastingLastUserId, - final int pageSize) { + final Long badgeLastUserId, + final Long tastingLastUserId, + final int pageSize) { - Slice badgeRecommendUsers = followQueryRepository.findBadgeRecommendUserList(loginMember, badgeLastUserId, pageSize); - Slice tastingRecommendUsers = followQueryRepository.findTastingRecommendUserList(loginMember, tastingLastUserId, pageSize); + Slice badgeRecommendUsers = followQueryRepository.findBadgeRecommendUserList(loginMember, + badgeLastUserId, pageSize); + Slice tastingRecommendUsers = Objects.isNull(tastingLastUserId) + ? followQueryRepository.findRandomRecommendUserList(pageSize) + : followQueryRepository.findTastingRecommendUserList(loginMember, tastingLastUserId, pageSize); return new RecommendListResponse(badgeRecommendUsers, tastingRecommendUsers); } - public long countFollowing(final Member member){ + public long countFollowing(final Member member) { return followQueryRepository.countFollowing(member); } - public long countFollower(final Member member){ + public long countFollower(final Member member) { return followQueryRepository.countFollower(member); } - public boolean isFollowing(final Member loginMember , final Member member){ - return followQueryRepository.isFollowing(loginMember,member); + public boolean isFollowing(final Member loginMember, final Member member) { + return followQueryRepository.isFollowing(loginMember, member); } } diff --git a/src/main/java/com/juu/juulabel/follow/repository/query/FollowQueryRepository.java b/src/main/java/com/juu/juulabel/follow/repository/query/FollowQueryRepository.java index 7c79ceb9..1bd6b4b6 100644 --- a/src/main/java/com/juu/juulabel/follow/repository/query/FollowQueryRepository.java +++ b/src/main/java/com/juu/juulabel/follow/repository/query/FollowQueryRepository.java @@ -14,7 +14,6 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQueryFactory; -import io.jsonwebtoken.lang.Objects; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; @@ -23,6 +22,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -30,297 +30,295 @@ @RequiredArgsConstructor public class FollowQueryRepository { - private final JPAQueryFactory jpaQueryFactory; - - QFollow follow = QFollow.follow; - QMember follower = new QMember("follower"); - QMember followee = new QMember("followee"); - QMember members = new QMember("member"); - QMemberAlcoholType memberAlcoholType = QMemberAlcoholType.memberAlcoholType; - QAlcoholType alcoholType = QAlcoholType.alcoholType; - QTastingNote tastingNote = QTastingNote.tastingNote; - QDailyLife dailyLife = QDailyLife.dailyLife; - - public Slice findAllFollowing(Member loginMember, Member member, Long lastFollowId, int pageSize) { - List followingList = jpaQueryFactory - .select( - Projections.constructor( - FollowUser.class, - followee.id, - followee.nickname, - followee.profileImage, - followee.hasBadge, - jpaQueryFactory - .selectFrom(follow) - .where( - follow.follower.eq(loginMember), - follow.followee.eq(followee) - ) - .exists() - )) - .from(follow) - .innerJoin(follow.follower, follower) - .innerJoin(follow.followee, followee) - .where( - follower.eq(member), - noOffsetByFollowId(follow, lastFollowId) - ) - .orderBy(followIdDesc(follow)) - .limit(pageSize + 1L) - .fetch(); - - boolean hasNext = followingList.size() > pageSize; - if (hasNext) { - followingList.remove(pageSize); + private final JPAQueryFactory jpaQueryFactory; + + QFollow follow = QFollow.follow; + QMember follower = new QMember("follower"); + QMember followee = new QMember("followee"); + QMember members = new QMember("member"); + QMemberAlcoholType memberAlcoholType = QMemberAlcoholType.memberAlcoholType; + QAlcoholType alcoholType = QAlcoholType.alcoholType; + QTastingNote tastingNote = QTastingNote.tastingNote; + QDailyLife dailyLife = QDailyLife.dailyLife; + + public Slice findAllFollowing(Member loginMember, Member member, Long lastFollowId, int pageSize) { + List followingList = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + followee.id, + followee.nickname, + followee.profileImage, + followee.hasBadge, + isFollowingSubQuery(followee, loginMember) + + )) + .from(follow) + .innerJoin(follow.follower, follower) + .innerJoin(follow.followee, followee) + .where( + follower.eq(member), + noOffsetByFollowId(follow, lastFollowId)) + .orderBy(followIdDesc(follow)) + .limit(pageSize + 1L) + .fetch(); + + boolean hasNext = followingList.size() > pageSize; + if (hasNext) { + followingList.remove(pageSize); + } + + return new SliceImpl<>(followingList, PageRequest.ofSize(pageSize), hasNext); } - return new SliceImpl<>(followingList, PageRequest.ofSize(pageSize), hasNext); - } - - public Slice findAllFollower(Member loginMember, Member member, Long lastFollowId, int pageSize) { - List followerList = jpaQueryFactory - .select( - Projections.constructor( - FollowUser.class, - follower.id, - follower.nickname, - follower.profileImage, - follower.hasBadge, - jpaQueryFactory - .selectFrom(follow) - .where( - follow.follower.eq(loginMember), - follow.followee.eq(follower) - ) - .exists() - )) - .from(follow) - .innerJoin(follow.follower, follower) - .innerJoin(follow.followee, followee) - .where( - followee.eq(member), - noOffsetByFollowId(follow, lastFollowId) - ) - .orderBy(followIdDesc(follow)) - .limit(pageSize + 1L) - .fetch(); - - boolean hasNext = followerList.size() > pageSize; - if (hasNext) { - followerList.remove(pageSize); + public Slice findAllFollower(Member loginMember, Member member, Long lastFollowId, int pageSize) { + List followerList = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + follower.id, + follower.nickname, + follower.profileImage, + follower.hasBadge, + isFollowingSubQuery(follower, loginMember))) + .from(follow) + .innerJoin(follow.follower, follower) + .innerJoin(follow.followee, followee) + .where( + followee.eq(member), + noOffsetByFollowId(follow, lastFollowId)) + .orderBy(followIdDesc(follow)) + .limit(pageSize + 1L) + .fetch(); + + boolean hasNext = followerList.size() > pageSize; + if (hasNext) { + followerList.remove(pageSize); + } + + return new SliceImpl<>(followerList, PageRequest.ofSize(pageSize), hasNext); } - return new SliceImpl<>(followerList, PageRequest.ofSize(pageSize), hasNext); - } - - public long countFollowing(final Member member){ - return jpaQueryFactory - .select(follow.count()) - .from(follow) - .where(follow.follower.eq(member)) - .fetchOne(); - } - - public long countFollower(final Member member){ - return jpaQueryFactory - .select(follow.count()) - .from(follow) - .where(follow.followee.eq(member)) - .fetchOne(); - } - - public Slice getSearchUserList(Member loginMember, Long lastFollowId, int pageSize, String username){ - List searchUserList = jpaQueryFactory - .select( - Projections.constructor( - FollowUser.class, - members.id, - members.nickname, - members.profileImage, - members.hasBadge, - jpaQueryFactory - .selectFrom(follow) - .where( - follow.follower.eq(loginMember), - follow.followee.eq(members) - ) - .exists() - )) - .from(members) - .where( - members.nickname.contains(username) - ) - .limit(pageSize + 1L) - .fetch(); - - boolean hasNext = searchUserList.size() > pageSize; - if (hasNext) { - searchUserList.remove(pageSize); + public long countFollowing(final Member member) { + return jpaQueryFactory + .select(follow.count()) + .from(follow) + .where(follow.follower.eq(member)) + .fetchOne(); } - return new SliceImpl<>(searchUserList, PageRequest.ofSize(pageSize), hasNext); - } - - public boolean isFollowing(final Member loginMember, final Member member){ - - Long result = jpaQueryFactory - .select(follow.count()) - .from(follow) - .where( - follow.follower.eq(loginMember), - follow.followee.eq(member) - ) - .fetchOne(); - - return result != null && result > 0; - } - - public Slice findBadgeRecommendUserList(final Member loginMember, Long badgeLastUserId, int pageSize){ - List BadgeRecommendUserList = jpaQueryFactory - .select( - Projections.constructor( - FollowUser.class, - members.id, - members.nickname, - members.profileImage, - members.hasBadge, - jpaQueryFactory - .selectFrom(follow) - .where( - follow.follower.eq(loginMember), - follow.followee.eq(members) - ) - .exists() - ) - ) - .from(members) - .where( - members.hasBadge.isTrue(), - noOffsetByFollowId(members, badgeLastUserId) - ) - .limit(pageSize + 1L) - .fetch(); - - boolean hasNext = BadgeRecommendUserList.size() > pageSize; - if (hasNext) { - BadgeRecommendUserList.remove(pageSize); + + public long countFollower(final Member member) { + return jpaQueryFactory + .select(follow.count()) + .from(follow) + .where(follow.followee.eq(member)) + .fetchOne(); } - return new SliceImpl<>(BadgeRecommendUserList, PageRequest.ofSize(pageSize), hasNext); - } - - public Slice findTastingRecommendUserList(final Member loginMember, Long tastingLastUserId, int pageSize){ - - List preferredAlcoholTypes = jpaQueryFactory - .select(alcoholType) - .from(memberAlcoholType) - .join(memberAlcoholType.alcoholType, alcoholType) - .where(memberAlcoholType.member.eq(loginMember)) - .fetch(); - - System.out.println("preferredAlcoholTypes = " + preferredAlcoholTypes.stream() - .map(AlcoholType::getId) - .toList()); - - - List TastingRecommendUserList1 = jpaQueryFactory - .select( - Projections.constructor( - FollowUser.class, - members.id, - members.nickname, - members.profileImage, - members.hasBadge, - jpaQueryFactory - .selectFrom(follow) - .where( - follow.follower.eq(loginMember), - follow.followee.eq(members) - ) - .exists() - ) - ) - .from(members) - .join(memberAlcoholType).on(members.id.eq(memberAlcoholType.member.id)) - .join(memberAlcoholType.alcoholType, alcoholType) - .leftJoin(tastingNote).on(tastingNote.member.eq(members)) - .leftJoin(dailyLife).on(dailyLife.member.eq(members)) - .where( - memberAlcoholType.alcoholType.in(preferredAlcoholTypes), - members.ne(loginMember), - noOffsetByFollowId(members, tastingLastUserId) - ) - .groupBy(members.id) - .orderBy( - tastingNote.id.count().add(dailyLife.id.count()).desc(), - Expressions.numberTemplate(Double.class, "function('rand')").asc() - ) - .limit(3) - .fetch(); - - List TastingRecommendUserList2 = jpaQueryFactory - .select( - Projections.constructor( - FollowUser.class, - members.id, - members.nickname, - members.profileImage, - members.hasBadge, - jpaQueryFactory - .selectFrom(follow) - .where( + public Slice getSearchUserList(Member loginMember, Long lastFollowId, int pageSize, + String username) { + List searchUserList = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + members.id, + members.nickname, + members.profileImage, + members.hasBadge, + isFollowingSubQuery(members, loginMember))) + .from(members) + .where( + members.nickname.contains(username)) + .limit(pageSize + 1L) + .fetch(); + + boolean hasNext = searchUserList.size() > pageSize; + if (hasNext) { + searchUserList.remove(pageSize); + } + return new SliceImpl<>(searchUserList, PageRequest.ofSize(pageSize), hasNext); + } + + private BooleanExpression isFollowingSubQuery(QMember member, Member loginMember) { + // 로그인 안한 경우 팔로우 여부 체크 안함 + if (Objects.isNull(loginMember)) { + return Expressions.FALSE; + } + + return jpaQueryFactory + .selectFrom(follow) + .where(follow.follower.eq(loginMember), follow.followee.eq(member)) + .exists(); + } + + public boolean isFollowing(final Member loginMember, final Member member) { + if (Objects.isNull(loginMember)) { + return false; + } + + Long result = jpaQueryFactory + .select(follow.count()) + .from(follow) + .where( follow.follower.eq(loginMember), - follow.followee.eq(members) - ) - .exists() - ) - ) - .from(members) - .join(memberAlcoholType).on(members.id.eq(memberAlcoholType.member.id)) - .join(memberAlcoholType.alcoholType, alcoholType) - .where( - memberAlcoholType.alcoholType.in(preferredAlcoholTypes), - members.ne(loginMember) - ) - .groupBy(members.id) - .orderBy( - Expressions.numberTemplate(Double.class, "function('rand')").asc() - ) - .limit(pageSize - 3) - .fetch(); - - Set existingUserIds = TastingRecommendUserList1.stream() - .map(FollowUser::id) - .collect(Collectors.toSet()); - - List recommendUserList = new ArrayList<>(TastingRecommendUserList1); - for(FollowUser user : TastingRecommendUserList2) { - if (!existingUserIds.contains(user.id())){ - recommendUserList.add(user); - } - if (recommendUserList.size()>=pageSize){ - break; - } + follow.followee.eq(member)) + .fetchOne(); + + return result != null && result > 0; } - boolean hasNext = recommendUserList.size() > pageSize; - if (hasNext) { - recommendUserList.remove(pageSize); + public Slice findBadgeRecommendUserList(final Member loginMember, Long badgeLastUserId, + int pageSize) { + List BadgeRecommendUserList = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + members.id, + members.nickname, + members.profileImage, + members.hasBadge, + isFollowingSubQuery(members, loginMember))) + .from(members) + .where( + members.hasBadge.isTrue(), + noOffsetByFollowId(members, badgeLastUserId)) + .limit(pageSize + 1L) + .fetch(); + + boolean hasNext = BadgeRecommendUserList.size() > pageSize; + if (hasNext) { + BadgeRecommendUserList.remove(pageSize); + } + + return new SliceImpl<>(BadgeRecommendUserList, PageRequest.ofSize(pageSize), hasNext); } - return new SliceImpl<>(recommendUserList, PageRequest.ofSize(pageSize), hasNext); - } + public Slice findTastingRecommendUserList(final Member loginMember, Long tastingLastUserId, + int pageSize) { + + List preferredAlcoholTypes = jpaQueryFactory + .select(alcoholType) + .from(memberAlcoholType) + .join(memberAlcoholType.alcoholType, alcoholType) + .where(memberAlcoholType.member.eq(loginMember)) + .fetch(); + + List TastingRecommendUserList1 = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + members.id, + members.nickname, + members.profileImage, + members.hasBadge, + jpaQueryFactory + .selectFrom(follow) + .where( + follow.follower.eq( + loginMember), + follow.followee.eq( + members)) + .exists())) + .from(members) + .join(memberAlcoholType).on(members.id.eq(memberAlcoholType.member.id)) + .join(memberAlcoholType.alcoholType, alcoholType) + .leftJoin(tastingNote).on(tastingNote.member.eq(members)) + .leftJoin(dailyLife).on(dailyLife.member.eq(members)) + .where( + memberAlcoholType.alcoholType.in(preferredAlcoholTypes), + members.ne(loginMember), + noOffsetByFollowId(members, tastingLastUserId)) + .groupBy(members.id) + .orderBy( + tastingNote.id.count().add(dailyLife.id.count()).desc(), + Expressions.numberTemplate(Double.class, "function('rand')").asc()) + .limit(3) + .fetch(); + + List TastingRecommendUserList2 = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + members.id, + members.nickname, + members.profileImage, + members.hasBadge, + jpaQueryFactory + .selectFrom(follow) + .where( + follow.follower.eq( + loginMember), + follow.followee.eq( + members)) + .exists())) + .from(members) + .join(memberAlcoholType).on(members.id.eq(memberAlcoholType.member.id)) + .join(memberAlcoholType.alcoholType, alcoholType) + .where( + memberAlcoholType.alcoholType.in(preferredAlcoholTypes), + members.ne(loginMember)) + .groupBy(members.id) + .orderBy( + Expressions.numberTemplate(Double.class, "function('rand')").asc()) + .limit(pageSize - 3) + .fetch(); + + Set existingUserIds = TastingRecommendUserList1.stream() + .map(FollowUser::id) + .collect(Collectors.toSet()); + + List recommendUserList = new ArrayList<>(TastingRecommendUserList1); + for (FollowUser user : TastingRecommendUserList2) { + if (!existingUserIds.contains(user.id())) { + recommendUserList.add(user); + } + if (recommendUserList.size() >= pageSize) { + break; + } + } + + boolean hasNext = recommendUserList.size() > pageSize; + if (hasNext) { + recommendUserList.remove(pageSize); + } + + return new SliceImpl<>(recommendUserList, PageRequest.ofSize(pageSize), hasNext); + } + public Slice findRandomRecommendUserList( + int pageSize) { + List recommendUserList = jpaQueryFactory + .select( + Projections.constructor( + FollowUser.class, + members.id, + members.nickname, + members.profileImage, + members.hasBadge, + isFollowingSubQuery(members, null))) + .from(members) + .orderBy( + Expressions.numberTemplate(Double.class, "function('rand')").asc()) + .limit(pageSize + 1L) + .fetch(); + + boolean hasNext = recommendUserList.size() > pageSize; + if (hasNext) { + recommendUserList.remove(pageSize); + } + + return new SliceImpl<>(recommendUserList, PageRequest.ofSize(pageSize), hasNext); + } - private OrderSpecifier followIdDesc(QFollow follow) { - return follow.id.desc(); - } + private OrderSpecifier followIdDesc(QFollow follow) { + return follow.id.desc(); + } - private BooleanExpression noOffsetByFollowId(QFollow follow, Long lastFollowId) { - return Objects.isEmpty(lastFollowId) ? null : follow.id.lt(lastFollowId); - } + private BooleanExpression noOffsetByFollowId(QFollow follow, Long lastFollowId) { + return Objects.isNull(lastFollowId) ? null : follow.id.lt(lastFollowId); + } - private BooleanExpression noOffsetByFollowId(QMember members, Long lastFollowId) { - return Objects.isEmpty(lastFollowId) ? null : members.id.lt(lastFollowId); - } + private BooleanExpression noOffsetByFollowId(QMember members, Long lastFollowId) { + return Objects.isNull(lastFollowId) ? null : members.id.lt(lastFollowId); + } } -