Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a10858e
[EA3-122] refactor: 스터디 목록 조회 기능 querydsl 사용
pia01190 Jul 14, 2025
c0ccc2c
[EA3-122] feature: 스터디 헤더 목록 조회 및 WebExceptionAdvice 수정
pia01190 Jul 15, 2025
ba77666
[EA3-122] feature: 스터디 대표 이미지 조회 기능
pia01190 Jul 15, 2025
016934c
[EA3-122] feature: 스터디장 스터디 정보 조회 기능
pia01190 Jul 15, 2025
01e0de5
Merge branch 'develop' into feature/EA3-122-study-crud
pia01190 Jul 15, 2025
cd6e4f4
[EA3-122] refactor: studyQueryRepository static import 추가
pia01190 Jul 15, 2025
42d6cd7
Merge branch 'develop' into feature/EA3-122-study-crud
pia01190 Jul 15, 2025
2295702
[EA3-136] fix:entity 변경 및 PR 템플릿 에러 수정
hyeunS-P Jul 15, 2025
110d6c1
[EA3-136] feature:pr 수정 중에 이미 존재하는 링크 삭제하도록 변경
hyeunS-P Jul 16, 2025
459ab32
feature:PR 템플릿 조회 시점에 활성화된 링크만 조회
hyeunS-P Jul 16, 2025
7adfb0d
[EA3-97] refactor: StudyQueryRepository 쿼리 수정
pia01190 Jul 16, 2025
0b77d7b
[EA3-97] refactor: request 생성자 추가
pia01190 Jul 16, 2025
db4d1e0
[EA3-97] refactor: 에러 처리를 위한 메서드 순서 변경
pia01190 Jul 16, 2025
3d25aa1
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-fin…
pia01190 Jul 16, 2025
9d885f7
[EA3-132] feature: 리뷰 기능 예외 추가 및 수정
Tokwasp Jul 16, 2025
8c694cf
Merge pull request #87 from prgrms-web-devcourse-final-project/featur…
Tokwasp Jul 16, 2025
e90a8f4
Merge pull request #107 from prgrms-web-devcourse-final-project/featu…
hyeunS-P Jul 16, 2025
6571d4f
Merge pull request #105 from prgrms-web-devcourse-final-project/fix/E…
dbrkdgus00 Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package grep.neogul_coder.domain.prtemplate.controller.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
public class LinkUpdateRequest {

private String prUrl;
@Schema(description = "링크 이름", example = "인스타그램")
private String urlName;

@Schema(description = "링크 URL", example = "https://instagram.com/example")
private String prUrl;

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class PrUpdateRequest {
private String location;

@Size(max = 2, message = "링크는 최대 2개 까지 가능합니다.")
@Schema(description = "PR 링크 목록", example = "[\"https://instagram.com/example\"]")
@Schema(description = "PR 링크 목록", example = "[{\"urlName\": \"인스타그램\", \"prUrl\": \"https://instagram.com/example\"}]")
private List<LinkUpdateRequest> prUrls;

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
@Repository
public interface LinkRepository extends JpaRepository<Link, Long> {
Link findByPrId(Long prId);

List<Link> findAllByPrIdAndActivatedTrue(Long prId);
List<Link> findAllByPrId(Long prId);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package grep.neogul_coder.domain.prtemplate.service;

import grep.neogul_coder.domain.prtemplate.controller.dto.request.LinkUpdateRequest;
import grep.neogul_coder.domain.prtemplate.controller.dto.request.PrUpdateRequest;
import grep.neogul_coder.domain.prtemplate.entity.Link;
import grep.neogul_coder.domain.prtemplate.entity.PrTemplate;
import grep.neogul_coder.domain.prtemplate.exception.code.PrTemplateErrorCode;
import grep.neogul_coder.domain.prtemplate.repository.LinkRepository;
import grep.neogul_coder.domain.prtemplate.repository.PrTemplateRepository;
import grep.neogul_coder.global.exception.business.NotFoundException;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.Size;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
Expand All @@ -28,13 +24,19 @@ public void deleteByPrId(Long prId) {
}

public void update(Long prId, List<LinkUpdateRequest> prUrls) {
linkRepository.findByPrId(prId);
List<Link> existingLinks = linkRepository.findAllByPrId(prId);
for (Link link : existingLinks) {
link.delete();
}
linkRepository.saveAll(existingLinks);

for (LinkUpdateRequest request : prUrls) {
Link link = Link.builder()
.prId(prId)
.prUrl(request.getPrUrl())
.build();
.prId(prId)
.urlName(request.getUrlName())
.prUrl(request.getPrUrl())
.activated(true)
.build();

linkRepository.save(link);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public PrPageResponse toResponse(Long userId) {
User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException(
UserErrorCode.USER_NOT_FOUND, "회원이 존재하지 않습니다."));
PrTemplate prTemplate = prTemplateRepository.findByUserId(userId);
List<Link> links = linkRepository.findAllByPrId(prTemplate.getId());
List<Link> links = linkRepository.findAllByPrIdAndActivatedTrue(prTemplate.getId());
List<ReviewEntity> reviews = reviewRepository.findAllByTargetUserId(userId);

List<PrPageResponse.UserProfileDto> userProfiles = List.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
@Getter
public enum ReviewErrorCode implements ErrorCode {
NOT_SINGLE_REVIEW_TYPE(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.name(), "단일 리뷰 타입이 아닙니다. ( ex) GOOD, BAD 혼합 )"),
STUDY_MEMBER_EMPTY(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.name(), "스터디 맴버들을 조회 하였으나 스터디 맴버가 없습니다");
ALREADY_REVIEW_WRITE_USER(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.name(), "이미 리뷰를 작성한 회원 입니다"),

STUDY_MEMBER_EMPTY(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.name(), "스터디 맴버들을 조회 하였으나 스터디 맴버가 없습니다"),
STUDY_NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.name(), "스터디를 찾지 못했습니다.");

private static final String BASIC_ERROR_NAME = "REVIEW";
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class ReviewSaveRequest {
@Schema(example = "너무 친절 하세요!", description = "주관 리뷰")
private String content;

private ReviewSaveRequest() {}

@Builder
private ReviewSaveRequest(long studyId, long targetUserId, ReviewType reviewType,
List<String> reviewTag, String content) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,14 @@ public Page<ReviewEntity> findContentsPagingBy(Pageable pageable, long userId) {

return new PageImpl<>(reviews, pageable, (total != null) ? total : 0L);
}

public ReviewEntity findBy(long studyId, long targetUserId, long writeUserId) {
return queryFactory.selectFrom(reviewEntity)
.where(
reviewEntity.studyId.eq(studyId),
reviewEntity.targetUserId.eq(targetUserId),
reviewEntity.writeUserId.eq(writeUserId)
)
.fetchOne();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package grep.neogul_coder.domain.review.repository;

import grep.neogul_coder.domain.review.Review;
import grep.neogul_coder.domain.review.entity.ReviewEntity;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ReviewRepository extends JpaRepository<ReviewEntity, Long> {
List<ReviewEntity> findByTargetUserId(long userId);

List<ReviewEntity> findAllByTargetUserId(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import grep.neogul_coder.domain.study.Study;
import grep.neogul_coder.domain.study.StudyMember;
import grep.neogul_coder.domain.study.repository.StudyMemberRepository;
import grep.neogul_coder.domain.study.repository.StudyRepository;
import grep.neogul_coder.domain.users.entity.User;
import grep.neogul_coder.domain.users.repository.UserRepository;
import grep.neogul_coder.global.exception.business.BusinessException;
import grep.neogul_coder.global.exception.business.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
Expand All @@ -29,14 +31,15 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import static grep.neogul_coder.domain.review.ReviewErrorCode.STUDY_MEMBER_EMPTY;
import static grep.neogul_coder.domain.review.ReviewErrorCode.*;

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class ReviewService {

private final UserRepository userRepository;
private final StudyRepository studyRepository;
private final StudyMemberRepository studyMemberRepository;

private final ReviewTagFinder reviewTagFinder;
Expand All @@ -46,21 +49,26 @@ public class ReviewService {
private final ReviewTagRepository reviewTagRepository;

public ReviewTargetUsersInfo getReviewTargetUsersInfo(long studyId, String myNickname) {
List<StudyMember> studyMembers = studyMemberRepository.findByStudyIdFetchStudy(studyId);
List<User> targetUsers = findReviewTargetUsers(studyMembers, myNickname);
Study study = extractStudyFrom(studyMembers);
Study study = findValidStudy(studyId);
List<StudyMember> studyMembers = findValidStudyMember(studyId);

List<User> targetUsers = findReviewTargetUsers(studyMembers, myNickname);
return ReviewTargetUsersInfo.of(study, targetUsers);
}

@Transactional
public void save(ReviewSaveServiceRequest request, long userId) {
public void save(ReviewSaveServiceRequest request, long writeUserId) {
if(isAlreadyWrittenReviewBy(request.getStudyId(), request.getTargetUserId(), writeUserId)){
throw new BusinessException(ALREADY_REVIEW_WRITE_USER, ALREADY_REVIEW_WRITE_USER.getMessage());
}

Study study = findValidStudy(request.getStudyId());
ReviewTags reviewTags = ReviewTags.from(convertStringToReviewTag(request.getReviewTag()));
ReviewType reviewType = reviewTags.ensureSingleReviewType();

Review review = request.toReview(reviewTags.getReviewTags(), reviewType, userId);
Review review = request.toReview(reviewTags.getReviewTags(), reviewType, writeUserId);
List<ReviewTagEntity> reviewTagEntities = mapToReviewTagEntities(reviewTags);
reviewRepository.save(ReviewEntity.from(review, reviewTagEntities, request.getStudyId()));
reviewRepository.save(ReviewEntity.from(review, reviewTagEntities, study.getId()));
}

public MyReviewTagsInfo getMyReviewTags(long userId) {
Expand All @@ -81,6 +89,20 @@ public ReviewContentsPagingInfo getMyReviewContents(Pageable pageable, long user
return ReviewContentsPagingInfo.of(reviewPages, userIdMap);
}

private Study findValidStudy(long studyId){
return studyRepository.findById(studyId)
.orElseThrow(() -> new NotFoundException(STUDY_NOT_FOUND, STUDY_NOT_FOUND.getMessage()));
}

private List<StudyMember> findValidStudyMember(long studyId) {
List<StudyMember> studyMembers = studyMemberRepository.findByStudyIdFetchStudy(studyId);

if (studyMembers.isEmpty()) {
throw new NotFoundException(STUDY_MEMBER_EMPTY, STUDY_MEMBER_EMPTY.getMessage());
}
return studyMembers;
}

private List<User> findReviewTargetUsers(List<StudyMember> studyMembers, String myNickname) {
List<Long> userIds = studyMembers.stream()
.map(StudyMember::getUserId)
Expand All @@ -92,11 +114,8 @@ private List<User> findReviewTargetUsers(List<StudyMember> studyMembers, String
.toList();
}

private Study extractStudyFrom(List<StudyMember> studyMembers) {
if (studyMembers.isEmpty()) {
throw new NotFoundException(STUDY_MEMBER_EMPTY, STUDY_MEMBER_EMPTY.getMessage());
}
return studyMembers.getFirst().getStudy();
private boolean isAlreadyWrittenReviewBy(long studyId, long targetUserId, long writeUserId) {
return reviewQueryRepository.findBy(studyId, targetUserId, writeUserId) != null;
}

private List<ReviewTag> convertStringToReviewTag(List<String> reviewTags) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public ApiResponse<List<StudyItemResponse>> getStudies(@AuthenticationPrincipal

@GetMapping("/{studyId}/header")
public ApiResponse<StudyHeaderResponse> getStudyHeader(@PathVariable("studyId") Long studyId) {
return ApiResponse.success(new StudyHeaderResponse());
return ApiResponse.success(studyService.getStudyHeader(studyId));
}

@GetMapping("/{studyId}")
Expand All @@ -38,13 +38,16 @@ public ApiResponse<StudyResponse> getStudy(@PathVariable("studyId") Long studyId
}

@GetMapping("/me/images")
public ApiResponse<List<StudyImageResponse>> getStudyImages() {
return ApiResponse.success(List.of(new StudyImageResponse()));
public ApiResponse<List<StudyImageResponse>> getStudyImages(@AuthenticationPrincipal Principal userDetails) {
Long userId = userDetails.getUserId();
return ApiResponse.success(studyService.getStudyImages(userId));
}

@GetMapping("/{studyId}/info")
public ApiResponse<StudyInfoResponse> getStudyInfo(@PathVariable("studyId") Long studyId) {
return ApiResponse.success(new StudyInfoResponse());
public ApiResponse<StudyInfoResponse> getStudyInfo(@PathVariable("studyId") Long studyId,
@AuthenticationPrincipal Principal userDetails) {
Long userId = userDetails.getUserId();
return ApiResponse.success(studyService.getStudyInfo(studyId, userId));
}

@GetMapping("/{studyId}/me")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ public interface StudySpecification {
ApiResponse<StudyResponse> getStudy(Long studyId);

@Operation(summary = "스터디 대표 이미지 조회", description = "참여중인 스터디의 대표 이미지 목록을 조회합니다.")
ApiResponse<List<StudyImageResponse>> getStudyImages();
ApiResponse<List<StudyImageResponse>> getStudyImages(Principal userDetails);

@Operation(summary = "스터디 정보 조회", description = "스터디장이 스터디 정보를 조회합니다.")
ApiResponse<StudyInfoResponse> getStudyInfo(Long studyId);
ApiResponse<StudyInfoResponse> getStudyInfo(Long studyId, Principal userDetails);

@Operation(summary = "스터디 내 정보 조회", description = "스터디의 내 정보를 조회합니다.")
ApiResponse<StudyMyInfoResponse> getStudyMyInfo(Long studyId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class StudyCreateRequest {
@Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg")
private String imageUrl;

private StudyCreateRequest() {}

@Builder
private StudyCreateRequest(String name, Category category, StudyType studyType, String location,
LocalDate startDate, LocalDate endDate, String introduction, String imageUrl) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package grep.neogul_coder.domain.study.controller.dto.response;

import grep.neogul_coder.domain.study.Study;
import grep.neogul_coder.domain.study.enums.StudyType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
Expand All @@ -21,4 +23,24 @@ public class StudyHeaderResponse {

@Schema(description = "지역", example = "서울")
private String location;

@Builder
private StudyHeaderResponse(String name, String introduction, String imageUrl,
StudyType studyType, String location) {
this.name = name;
this.introduction = introduction;
this.imageUrl = imageUrl;
this.studyType = studyType;
this.location = location;
}

public static StudyHeaderResponse from(Study study) {
return StudyHeaderResponse.builder()
.name(study.getName())
.introduction(study.getIntroduction())
.imageUrl(study.getImageUrl())
.studyType(study.getStudyType())
.location(study.getLocation())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package grep.neogul_coder.domain.study.controller.dto.response;

import grep.neogul_coder.domain.study.Study;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Getter
Expand All @@ -11,4 +13,17 @@ public class StudyImageResponse {

@Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg")
private String imageUrl;

@Builder
private StudyImageResponse(Long studyId, String imageUrl) {
this.studyId = studyId;
this.imageUrl = imageUrl;
}

public static StudyImageResponse from(Study study) {
return StudyImageResponse.builder()
.studyId(study.getId())
.imageUrl(study.getImageUrl())
.build();
}
}
Loading