Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion deploy/local.init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ VALUES (default, null, null, 'STAFF', '2024-12-11', 2, 2, 1, 1);
INSERT INTO user_club (id, created_date, updated_date, club_role, join_date, match_count, schedule_count, club_id, user_id)
VALUES (default, null, null, 'STAFF', '2024-12-11', 2, 2, 3, 3);
INSERT INTO user_club (id, created_date, updated_date, club_role, join_date, match_count, schedule_count, club_id, user_id)
VALUES (default, null, null, 'STAFF', '2024-12-11', 2, 2, 3, 4);
VALUES (default, null, null, 'STAFF', '2024-12-11', 2, 2, 3, 4);

-- notice
INSERT INTO notice (id, created_date, updated_date, club_id, title, content)
values (default, '2024-12-10', null, 1, 'title', 'content');
INSERT INTO notice (id, created_date, updated_date, club_id, title, content)
values (default, '2024-12-11', null, 1, 'title2', 'content2');
INSERT INTO notice (id, created_date, updated_date, club_id, title, content)
values (default, '2024-12-12', null, 1, 'title3', 'content4');
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
package com.example.moim.club.controller;

import com.example.moim.club.dto.request.NoticeInput;
import com.example.moim.club.dto.request.NoticeOutput;
import com.example.moim.club.dto.response.NoticeOutput;
import com.example.moim.club.service.NoticeCommandService;
import com.example.moim.club.service.NoticeCommandServiceImpl;
import com.example.moim.club.service.NoticeQueryService;
import com.example.moim.global.exception.BaseResponse;
import com.example.moim.global.exception.ResponseCode;
import com.example.moim.user.dto.UserDetailsImpl;
import com.example.moim.user.entity.User;
import com.example.moim.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class NoticeController implements NoticeControllerDocs {
private final NoticeCommandService noticeCommandService;
private final NoticeQueryService noticeQueryService;
private final UserRepository userRepository;

/**
* FIXME: 응답 값이 무조건 있어야 함. user 의 권한 체크는 안해도 되나?
*/
@PostMapping("/notice")
public BaseResponse noticeSave(NoticeInput noticeInput, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) {
noticeCommandService.saveNotice(noticeInput);
return BaseResponse.onSuccess(null, ResponseCode.OK);
@PostMapping("/notice/{clubId}")
public BaseResponse<NoticeOutput> saveNotice(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @ModelAttribute NoticeInput noticeInput, @PathVariable("clubId") Long clubId) {
// public BaseResponse<NoticeOutput> saveNotice(@ModelAttribute NoticeInput noticeInput, @PathVariable("clubId") Long clubId) {
/**
* FIXME: 컨트롤러 테스트용 코드
*/
// User user = userRepository.findById(3L).get();
NoticeOutput noticeOutput = noticeCommandService.saveNotice(userDetailsImpl.getUser(), noticeInput, clubId);
return BaseResponse.onSuccess(noticeOutput, ResponseCode.OK);
}

/**
* FIXME: 공지를 시간 순으로 정렬하지 않아도 되나?
* @return
*/
@GetMapping("/notice/{clubId}")
public BaseResponse<List<NoticeOutput>> noticeSave(@PathVariable Long clubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl) {
return BaseResponse.onSuccess(noticeQueryService.findNotice(clubId), ResponseCode.OK);
public BaseResponse<Slice<NoticeOutput>> findNotices(@PathVariable Long clubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @RequestParam("cursorId") Long cursorId) {
// public BaseResponse<Slice<NoticeOutput>> findNotices(@PathVariable Long clubId, @RequestParam(value = "cursorId", required = false) Long cursorId) {
/**
* FIXME: 컨트롤러 테스트용 코드
*/
// User user = userRepository.findById(1L).get();
return BaseResponse.onSuccess(noticeQueryService.findNotice(userDetailsImpl.getUser(), clubId, cursorId), ResponseCode.OK);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
package com.example.moim.club.controller;

import com.example.moim.club.dto.request.NoticeInput;
import com.example.moim.club.dto.request.NoticeOutput;
import com.example.moim.club.dto.response.NoticeOutput;
import com.example.moim.global.exception.BaseResponse;
import com.example.moim.user.dto.UserDetailsImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Tag(name = "모임 공지 api", description = "모임(club) 공지 api 분리")
public interface NoticeControllerDocs {
@Operation(summary = "공지 생성")
BaseResponse noticeSave(NoticeInput noticeInput, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl);
BaseResponse<NoticeOutput> saveNotice(@AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @ModelAttribute NoticeInput noticeInput, @PathVariable("clubId") Long clubId);
// BaseResponse<NoticeOutput> saveNotice(@ModelAttribute NoticeInput noticeInput, @PathVariable Long clubId);

@Operation(summary = "공지 조회")
BaseResponse<List<NoticeOutput>> noticeSave(@PathVariable Long clubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl);
@Operation(summary = "공지 조회, 최초 요청 시 cursorId null 로 보내기")
BaseResponse<Slice<NoticeOutput>> findNotices(@PathVariable Long clubId, @AuthenticationPrincipal UserDetailsImpl userDetailsImpl, @RequestParam Long cursorId);
// BaseResponse<Slice<NoticeOutput>> findNotices(@PathVariable Long clubId, @RequestParam Long cursorId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
@Data
@NoArgsConstructor
public class NoticeInput {
private Long clubId;
private String title;
private String content;

@Builder
public NoticeInput(Long clubId, String title, String content) {
this.clubId = clubId;
public NoticeInput(String title, String content) {
this.title = title;
this.content = content;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.example.moim.club.dto.request;
package com.example.moim.club.dto.response;

import com.example.moim.club.entity.Notice;
import lombok.Data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import static com.example.moim.club.entity.QClubSearch.clubSearch;
import static org.springframework.util.StringUtils.hasText;

public class ClubRepositoryImpl implements ClubRepositoryCustom{
public class ClubRepositoryImpl implements ClubRepositoryCustom {
private final JPAQueryFactory queryFactory;

public ClubRepositoryImpl(EntityManager em) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import com.example.moim.club.entity.Club;
import com.example.moim.club.entity.Notice;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface NoticeRepository extends JpaRepository<Notice, Long> {
List<Notice> findByClub(Club club);
public interface NoticeRepository extends JpaRepository<Notice, Long>, NoticeRepositoryCustom {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationRepositoryImplNotificationRepository의 구현체로 해석될 위험은 없을까요? 혹시 이름을 NotificationJpaRepository로 변경하는건 불필요한 일일까요?

Copy link
Collaborator Author

@5nam 5nam Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 네이밍에 대한 생각을 못했네요. Jpa로 바꾸는게 좋을 거 같습니다! 반영해서 머지하겠습니다! 좋은 피드백 감사합니다 ㅎㅎ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 오류가 나서 찾아보니까, JPA의 커스텀 리포지토리 규칙이 있다고 합니다! 그래서

XxxRepositoryCustom → 구현체명은 반드시 XxxRepositoryImpl이어야 한다고 합니다!

https://imprint.tistory.com/142

링크 공유 드립니다!

// Page<Notice> findByClub(Club club, Pageable pageable);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.moim.club.repository;

import com.example.moim.club.entity.Club;
import com.example.moim.club.entity.Notice;

import java.util.List;

public interface NoticeRepositoryCustom {
List<Notice> findByCursor(Long cursor, int size, Club club);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example.moim.club.repository;

import com.example.moim.club.entity.Club;
import com.example.moim.club.entity.Notice;
import com.example.moim.club.entity.QNotice;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;

import java.util.List;

public class NoticeRepositoryImpl implements NoticeRepositoryCustom {

private final JPAQueryFactory queryFactory;

public NoticeRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}

@Override
public List<Notice> findByCursor(Long cursor, int size, Club club) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저희 클라이언트가 모바일이기 때문에 스크롤 할 것을 고려하여 커서 방식의 페이지네이션을 선택하신 점 너무 좋아요!

return queryFactory
.selectFrom(QNotice.notice)
.where(
ltCursor(cursor),
clubEq(club)
)
.orderBy(QNotice.notice.id.desc())
.limit(size + 1) // hasNext 판단용으로 1개 더
.fetch();
}

private BooleanExpression ltCursor(Long cursor) {
return cursor != null ? QNotice.notice.id.lt(cursor) : null;
}

private BooleanExpression clubEq(Club club) {
return club != null ? QNotice.notice.club.eq(club) : null;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public UserClubOutput saveClubUser(User user, ClubUserSaveInput clubUserSaveInpu
}
club.plusMemberCount();
UserClub userClub = userClubRepository.save(UserClub.createUserClub(user, club));
/**
* TODO: 알림 보내는 것 새로운 방식에 맞춰서 다시 구현해야 함
*/
eventPublisher.publishEvent(new ClubJoinEvent(user, club));
return new UserClubOutput(userClub);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.example.moim.club.service;

import com.example.moim.club.dto.request.NoticeInput;
import com.example.moim.club.dto.response.NoticeOutput;
import com.example.moim.user.entity.User;

public interface NoticeCommandService {
void saveNotice(NoticeInput noticeInput);
NoticeOutput saveNotice(User user, NoticeInput noticeInput, Long clubId);
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
package com.example.moim.club.service;

import com.example.moim.club.dto.request.NoticeInput;
import com.example.moim.club.dto.request.NoticeOutput;
import com.example.moim.club.dto.response.NoticeOutput;
import com.example.moim.club.entity.Club;
import com.example.moim.club.entity.Notice;
import com.example.moim.club.entity.UserClub;
import com.example.moim.club.exception.advice.ClubControllerAdvice;
import com.example.moim.club.repository.ClubRepository;
import com.example.moim.club.repository.NoticeRepository;
import com.example.moim.club.repository.UserClubRepository;
import com.example.moim.global.enums.ClubRole;
import com.example.moim.global.exception.ResponseCode;
import com.example.moim.user.entity.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class NoticeCommandServiceImpl implements NoticeCommandService {
private final NoticeRepository noticeRepository;
private final ClubRepository clubRepository;
private final UserClubRepository userClubRepository;

public NoticeOutput saveNotice(User user, NoticeInput noticeInput, Long clubId) {
log.debug("saveNotice 진입");
Club club = clubRepository.findById(clubId).orElseThrow(() -> new ClubControllerAdvice(ResponseCode.CLUB_NOT_FOUND));
UserClub userClub = userClubRepository.findByClubAndUser(club, user).orElseThrow(() -> new ClubControllerAdvice(ResponseCode.CLUB_USER_NOT_FOUND));

if (!userClub.getClubRole().equals(ClubRole.STAFF)) { // 공지 등록 권한 확인
log.debug("권한 오류");
throw new ClubControllerAdvice(ResponseCode.CLUB_PERMISSION_DENIED);
}

Notice notice = noticeRepository.save(Notice.createNotice(club, noticeInput.getTitle(), noticeInput.getContent()));

/**
* TODO: 알림 보내는 부분 구현해야함
*/

/**
* transaction 필요하지 않음. save 작업 하나만 이루어지고, 나머지는 다 조회 연산이므로
* save 함수 내부에 이미 transaction 적용되어 있음
* @param noticeInput
*/
public void saveNotice(NoticeInput noticeInput) {
noticeRepository.save(Notice.createNotice(clubRepository.findById(noticeInput.getClubId()).orElseThrow(() -> new ClubControllerAdvice(ResponseCode.CLUB_NOT_FOUND)),
noticeInput.getTitle(), noticeInput.getContent()));
return new NoticeOutput(notice);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.example.moim.club.service;

import com.example.moim.club.dto.request.NoticeOutput;
import com.example.moim.club.dto.response.NoticeOutput;
import com.example.moim.user.entity.User;
import org.springframework.data.domain.Slice;

import java.util.List;

public interface NoticeQueryService {
List<NoticeOutput> findNotice(Long clubId);
Slice<NoticeOutput> findNotice(User user, Long clubId, Long cursorId);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.example.moim.club.service;

import com.example.moim.club.dto.request.NoticeOutput;
import com.example.moim.club.dto.response.NoticeOutput;
import com.example.moim.club.entity.Club;
import com.example.moim.club.entity.Notice;
import com.example.moim.club.entity.UserClub;
import com.example.moim.club.exception.advice.ClubControllerAdvice;
import com.example.moim.club.repository.ClubRepository;
import com.example.moim.club.repository.NoticeRepository;
import com.example.moim.club.repository.UserClubRepository;
import com.example.moim.global.enums.ClubRole;
import com.example.moim.global.exception.ResponseCode;
import com.example.moim.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;

import java.util.List;
Expand All @@ -14,10 +21,29 @@
@RequiredArgsConstructor
public class NoticeQueryServiceImpl implements NoticeQueryService {

private static final int SIZE = 20;

private final NoticeRepository noticeRepository;
private final ClubRepository clubRepository;
private final UserClubRepository userClubRepository;

public Slice<NoticeOutput> findNotice(User user, Long clubId, Long cursorId) {
// 가입된 회원인지 확인, 가입되지 않은 회원이면 공지 열람 권한이 없는 것
Club club = clubRepository.findById(clubId).orElseThrow(() -> new ClubControllerAdvice(ResponseCode.CLUB_NOT_FOUND));
UserClub userClub = userClubRepository.findByClubAndUser(club, user).orElseThrow(() -> new ClubControllerAdvice(ResponseCode.CLUB_USER_NOT_FOUND));

List<Notice> notices = noticeRepository.findByCursor(cursorId, SIZE, club);

boolean hasNext = notices.size() > SIZE;

if (hasNext) {
notices.remove(SIZE);
}

List<NoticeOutput> outputs = notices.stream()
.map(NoticeOutput::new)
.toList();

public List<NoticeOutput> findNotice(Long clubId) {
return noticeRepository.findByClub(clubRepository.findById(clubId).orElseThrow(() -> new ClubControllerAdvice(ResponseCode.CLUB_NOT_FOUND))).stream().map(NoticeOutput::new).toList();
return new SliceImpl<>(outputs, PageRequest.of(0, SIZE), hasNext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
httpSecurity.csrf(csrf -> csrf
.ignoringRequestMatchers("/club/**")
);
httpSecurity.csrf(csrf -> csrf.ignoringRequestMatchers("/notice/**"));

//From 로그인 방식 disable
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
//http basic 인증 방식 disable
Expand Down
Loading