Skip to content

Commit e27e1a1

Browse files
안훈기안훈기
authored andcommitted
Merge branch 'develop' into feat/club-api
2 parents 13c04a1 + 30e1665 commit e27e1a1

48 files changed

Lines changed: 1394 additions & 234 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

label.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[
2+
{
3+
"name": "🐞 bug",
4+
"color": "d73a4a",
5+
"description": "버그 리포트"
6+
},
7+
{
8+
"name": "✨ feature",
9+
"color": "0e8a16",
10+
"description": "새로운 기능 요청"
11+
},
12+
{
13+
"name": "♻️ refactor",
14+
"color": "6f42c1",
15+
"description": "리팩토링 작업"
16+
},
17+
{
18+
"name": "🧹 chore",
19+
"color": "1d76db",
20+
"description": "빌드 설정 등 잡일"
21+
},
22+
{
23+
"name": "🚧 in progress",
24+
"color": "fbca04",
25+
"description": "작업 진행 중"
26+
},
27+
{
28+
"name": "👀 review needed",
29+
"color": "ffab00",
30+
"description": "리뷰 요청 필요"
31+
},
32+
{
33+
"name": "🔴 priority: high",
34+
"color": "e11d21",
35+
"description": "높은 우선순위"
36+
},
37+
{
38+
"name": "🟠 priority: medium",
39+
"color": "f9d0c4",
40+
"description": "중간 우선순위"
41+
},
42+
{
43+
"name": "🟢 priority: low",
44+
"color": "c2e0c6",
45+
"description": "낮은 우선순위"
46+
}
47+
]

src/main/java/com/be/sportizebe/domain/chat/controller/NoteController.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import com.be.sportizebe.domain.post.entity.Post;
77
import com.be.sportizebe.domain.post.exception.PostErrorCode;
88
import com.be.sportizebe.domain.post.repository.PostRepository;
9-
import com.be.sportizebe.domain.user.entity.User;
109
import com.be.sportizebe.global.exception.CustomException;
1110
import com.be.sportizebe.global.response.BaseResponse;
11+
import com.be.sportizebe.global.cache.dto.UserAuthInfo;
1212
import io.swagger.v3.oas.annotations.Operation;
1313
import io.swagger.v3.oas.annotations.Parameter;
1414
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -33,12 +33,12 @@ public class NoteController {
3333
@Operation(summary = "1대1 채팅방 생성", description = "게시글 작성자와 1대1 쪽지 채팅방을 생성합니다. 채팅방 이름은 게시글 제목으로 설정됩니다. 이미 채팅방이 존재하면 기존 채팅방을 반환합니다.")
3434
public ResponseEntity<BaseResponse<ChatRoomResponse>> createChatRoom(
3535
@Parameter(description = "게시글 ID") @PathVariable Long postId,
36-
@AuthenticationPrincipal User user) {
36+
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
3737

3838
Post post = postRepository.findById(postId)
3939
.orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND));
4040

41-
ChatRoom room = chatRoomService.createNote(post, user);
41+
ChatRoom room = chatRoomService.createNote(post, userAuthInfo.getId());
4242

4343
return ResponseEntity.status(HttpStatus.CREATED)
4444
.body(BaseResponse.success("채팅방 생성 성공", ChatRoomResponse.from(room)));
@@ -47,9 +47,9 @@ public ResponseEntity<BaseResponse<ChatRoomResponse>> createChatRoom(
4747
@GetMapping("/rooms")
4848
@Operation(summary = "내 쪽지 채팅방 목록 조회", description = "현재 사용자가 참여한 모든 쪽지 채팅방 목록을 조회합니다.")
4949
public ResponseEntity<BaseResponse<List<ChatRoomResponse>>> getMyChatRooms(
50-
@AuthenticationPrincipal User user) {
50+
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
5151

52-
List<ChatRoomResponse> rooms = chatRoomService.findMyNoteRooms(user).stream()
52+
List<ChatRoomResponse> rooms = chatRoomService.findMyNoteRooms(userAuthInfo.getId()).stream()
5353
.map(ChatRoomResponse::from)
5454
.toList();
5555

src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.be.sportizebe.domain.chat.entity.ChatRoom;
44
import com.be.sportizebe.domain.post.entity.Post;
5-
import com.be.sportizebe.domain.user.entity.User;
65
import org.springframework.data.jpa.repository.JpaRepository;
76

87
import java.util.List;
@@ -11,8 +10,8 @@
1110
public interface ChatRoomRepository extends JpaRepository<ChatRoom, Long> {
1211

1312
// 특정 게시글에 대해 두 사용자 간의 1대1 채팅방이 이미 존재하는지 확인
14-
Optional<ChatRoom> findByPostAndGuestUser(Post post, User guestUser);
13+
Optional<ChatRoom> findByPostAndGuestUserId(Post post, Long guestUserId);
1514

1615
// 사용자가 참여한 1대1 채팅방 목록 조회 (게시글 작성자 또는 채팅 요청자로 참여)
17-
List<ChatRoom> findByPost_UserOrGuestUser(User postUser, User guestUser);
16+
List<ChatRoom> findByPost_UserIdOrGuestUserId(Long postUserId, Long guestUserId);
1817
}

src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.be.sportizebe.domain.club.entity.Club;
77
import com.be.sportizebe.domain.post.entity.Post;
88
import com.be.sportizebe.domain.user.entity.User;
9+
import com.be.sportizebe.domain.user.exception.UserErrorCode;
10+
import com.be.sportizebe.domain.user.repository.UserRepository;
911
import com.be.sportizebe.global.exception.CustomException;
1012
import lombok.RequiredArgsConstructor;
1113
import org.springframework.stereotype.Service;
@@ -18,6 +20,7 @@
1820
@Transactional(readOnly = true)
1921
public class ChatRoomService {
2022
private final ChatRoomRepository chatRoomRepository;
23+
private final UserRepository userRepository;
2124

2225
@Transactional
2326
public ChatRoom createGroup(Club club) {
@@ -29,17 +32,19 @@ public ChatRoom createGroup(Club club) {
2932
}
3033

3134
@Transactional
32-
public ChatRoom createNote(Post post, User guestUser) {
35+
public ChatRoom createNote(Post post, Long guestUserId) {
3336
User hostUser = post.getUser(); // 게시글 등록자
3437

3538
// 자기 자신에게 채팅 불가
36-
if (hostUser.getId() == guestUser.getId()) {
39+
if (hostUser.getId() == guestUserId) {
3740
throw new CustomException(ChatErrorCode.SELF_CHAT_NOT_ALLOWED);
3841
}
3942

4043
// 이미 존재하는 채팅방이 있으면 반환
41-
return chatRoomRepository.findByPostAndGuestUser(post, guestUser)
44+
return chatRoomRepository.findByPostAndGuestUserId(post, guestUserId)
4245
.orElseGet(() -> {
46+
User guestUser = userRepository.findById(guestUserId)
47+
.orElseThrow(() -> new CustomException(UserErrorCode.USER_NOT_FOUND));
4348
ChatRoom room = ChatRoom.builder()
4449
.chatRoomType(ChatRoom.ChatRoomType.NOTE)
4550
.post(post)
@@ -50,8 +55,8 @@ public ChatRoom createNote(Post post, User guestUser) {
5055
}
5156

5257
// 사용자가 참여한 1대1 채팅방 목록 조회
53-
public List<ChatRoom> findMyNoteRooms(User user) {
54-
return chatRoomRepository.findByPost_UserOrGuestUser(user, user);
58+
public List<ChatRoom> findMyNoteRooms(Long userId) {
59+
return chatRoomRepository.findByPost_UserIdOrGuestUserId(userId, userId);
5560
}
5661

5762
public List<ChatRoom> findAll() {

src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java

Lines changed: 30 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import com.be.sportizebe.domain.club.dto.response.ClubResponse;
88
import com.be.sportizebe.domain.club.dto.response.ClubScrollResponse;
99
import com.be.sportizebe.domain.club.service.ClubServiceImpl;
10-
import com.be.sportizebe.domain.user.entity.User;
10+
import com.be.sportizebe.global.cache.dto.UserAuthInfo;
1111
import com.be.sportizebe.global.response.BaseResponse;
1212
import io.swagger.v3.oas.annotations.Operation;
1313
import io.swagger.v3.oas.annotations.Parameter;
@@ -24,70 +24,39 @@
2424
@RestController
2525
@RequiredArgsConstructor
2626
@RequestMapping("/api/clubs")
27-
@Tag(name = "club", description = "동호회 관련 API")
27+
@Tag(name = "club", description = "동호회 관리 관련 API")
2828
public class ClubController {
2929

3030
private final ClubServiceImpl clubService;
3131

32-
@PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
33-
@Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다. (이미지 첨부 가능)")
34-
public ResponseEntity<BaseResponse<ClubResponse>> createClub(
35-
@RequestPart("request") @Valid ClubCreateRequest request,
36-
@RequestPart(value = "image", required = false) MultipartFile image,
37-
@AuthenticationPrincipal User user) {
38-
ClubResponse response = clubService.createClub(request, image, user);
39-
return ResponseEntity.status(HttpStatus.CREATED)
40-
.body(BaseResponse.success("동호회 생성 성공", response));
41-
}
42-
@GetMapping("/me")
43-
@Operation(summary = "내가 가입한 동호회 조회(무한스크롤)", description = "로그인한 사용자가 가입한 동호회를 커서 기반 무한스크롤로 조회합니다.")
44-
public ResponseEntity<BaseResponse<ClubScrollResponse>> getMyClubs(
45-
@Parameter(description = "커서(마지막 clubId). 첫 조회는 null") @RequestParam(required = false) Long cursor,
46-
@Parameter(description = "조회 개수", example = "10") @RequestParam(defaultValue = "10") int size,
47-
@AuthenticationPrincipal User user
48-
) {
49-
ClubScrollResponse response = clubService.getMyClubsByScroll(cursor, size, user);
50-
return ResponseEntity.ok(BaseResponse.success("내 동호회 조회 성공", response));
51-
}
52-
@GetMapping
53-
@Operation(summary = "모든 동호회 조회 (무한스크롤)",
54-
description = "커서 기반 무한스크롤 방식으로 동호회 목록을 조회합니다.")
55-
public ResponseEntity<BaseResponse<ClubScrollResponse>> getClubs(
56-
@RequestParam(required = false) Long cursor,
57-
@RequestParam(defaultValue = "20") int size
58-
) {
59-
ClubScrollResponse response = clubService.getClubsByScroll(cursor, size);
60-
return ResponseEntity.ok(
61-
BaseResponse.success("동호회 목록 조회 성공", response)
62-
);
63-
}
32+
@PostMapping(value = "", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
33+
@Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다. (이미지 첨부 가능)")
34+
public ResponseEntity<BaseResponse<ClubResponse>> createClub(
35+
@RequestPart("request") @Valid ClubCreateRequest request,
36+
@RequestPart(value = "image", required = false) MultipartFile image,
37+
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
38+
ClubResponse response = clubService.createClub(request, image, userAuthInfo.getId());
39+
return ResponseEntity.status(HttpStatus.CREATED)
40+
.body(BaseResponse.success("동호회 생성 성공", response));
41+
}
6442

65-
@GetMapping("/{clubId}")
66-
@Operation(summary = "동호회 상세 조회", description = "동호회 단건 상세 정보를 조회합니다.")
67-
public ResponseEntity<BaseResponse<ClubDetailResponse>> getClub(
68-
@Parameter(description = "동호회 ID") @PathVariable Long clubId
69-
) {
70-
ClubDetailResponse response = clubService.getClub(clubId);
71-
return ResponseEntity.ok(BaseResponse.success("동호회 상세 조회 성공", response));
72-
}
43+
@PutMapping("/{clubId}")
44+
@Operation(summary = "동호회 수정", description = "동호회 정보를 수정합니다. 동호회장만 수정할 수 있습니다.")
45+
public ResponseEntity<BaseResponse<ClubResponse>> updateClub(
46+
@PathVariable Long clubId,
47+
@RequestBody @Valid ClubUpdateRequest request,
48+
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
49+
ClubResponse response = clubService.updateClub(clubId, request, userAuthInfo.getId());
50+
return ResponseEntity.ok(BaseResponse.success("동호회 수정 성공", response));
51+
}
7352

74-
@PutMapping("/{clubId}")
75-
@Operation(summary = "동호회 수정", description = "동호회 정보를 수정합니다. 동호회장만 수정할 수 있습니다.")
76-
public ResponseEntity<BaseResponse<ClubResponse>> updateClub(
77-
@PathVariable Long clubId,
78-
@RequestBody @Valid ClubUpdateRequest request,
79-
@AuthenticationPrincipal User user) {
80-
ClubResponse response = clubService.updateClub(clubId, request, user);
81-
return ResponseEntity.ok(BaseResponse.success("동호회 수정 성공", response));
82-
}
83-
84-
@PostMapping(value = "/{clubId}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
85-
@Operation(summary = "동호회 사진 수정", description = "동호회 사진을 수정합니다. 동호회장만 수정할 수 있습니다.")
86-
public ResponseEntity<BaseResponse<ClubImageResponse>> updateClubImage(
87-
@Parameter(description = "동호회 ID") @PathVariable Long clubId,
88-
@RequestPart("image") MultipartFile image,
89-
@AuthenticationPrincipal User user) {
90-
ClubImageResponse response = clubService.updateClubImage(clubId, image, user);
91-
return ResponseEntity.ok(BaseResponse.success("동호회 사진 수정 성공", response));
92-
}
53+
@PostMapping(value = "/{clubId}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
54+
@Operation(summary = "동호회 사진 수정", description = "동호회 사진을 수정합니다. 동호회장만 수정할 수 있습니다.")
55+
public ResponseEntity<BaseResponse<ClubImageResponse>> updateClubImage(
56+
@Parameter(description = "동호회 ID") @PathVariable Long clubId,
57+
@RequestPart("image") MultipartFile image,
58+
@AuthenticationPrincipal UserAuthInfo userAuthInfo) {
59+
ClubImageResponse response = clubService.updateClubImage(clubId, image, userAuthInfo.getId());
60+
return ResponseEntity.ok(BaseResponse.success("동호회 사진 수정 성공", response));
61+
}
9362
}

src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.be.sportizebe.domain.club.entity.Club;
44
import com.be.sportizebe.domain.user.entity.SportType;
5-
import com.be.sportizebe.domain.user.entity.User;
65
import io.swagger.v3.oas.annotations.media.Schema;
76
import jakarta.validation.constraints.NotBlank;
87

@@ -13,14 +12,13 @@ public record ClubCreateRequest(
1312
@Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType,
1413
@Schema(description = "최대 정원", example = "20") Integer maxMembers) {
1514

16-
public Club toEntity(User user, String clubImage) {
15+
public Club toEntity(String clubImage) {
1716
return Club.builder()
1817
.name(name)
1918
.introduce(introduce)
2019
.clubType(clubType)
2120
.maxMembers(maxMembers)
2221
.clubImage(clubImage)
23-
.leader(user)
2422
.build();
2523
}
2624
}

src/main/java/com/be/sportizebe/domain/club/entity/Club.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,40 @@ public class Club extends BaseTimeEntity {
3939

4040
private String clubImage; // 동호회 사진 URL
4141

42-
@ManyToOne(fetch = FetchType.LAZY)
43-
@JoinColumn(name = "leader_id", nullable = false)
44-
private User leader; // 동호회장
45-
4642
@OneToOne(mappedBy = "club", fetch = FetchType.LAZY)
4743
private ChatRoom chatRoom; // 동호회 채팅방
4844

4945
@OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true)
5046
@Builder.Default
5147
private List<ClubMember> members = new ArrayList<>();
5248

49+
/**
50+
* 동호회장(LEADER) 조회
51+
* ClubMember에서 LEADER 역할을 가진 멤버를 찾아 반환
52+
*/
53+
public ClubMember getLeaderMember() {
54+
return members.stream()
55+
.filter(member -> member.getRole() == ClubMember.ClubRole.LEADER)
56+
.findFirst()
57+
.orElse(null);
58+
}
59+
60+
/**
61+
* 동호회장 User 조회
62+
*/
63+
public User getLeader() {
64+
ClubMember leaderMember = getLeaderMember();
65+
return leaderMember != null ? leaderMember.getUser() : null;
66+
}
67+
68+
/**
69+
* 특정 사용자가 동호회장인지 확인
70+
*/
71+
public boolean isLeader(Long userId) {
72+
ClubMember leaderMember = getLeaderMember();
73+
return leaderMember != null && leaderMember.getUser().getId() == userId;
74+
}
75+
5376
public void update(String name, String introduce, Integer maxMembers, SportType clubType) {
5477
this.name = name;
5578
this.introduce = introduce;

src/main/java/com/be/sportizebe/domain/club/entity/ClubMember.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
@Builder
1414
@AllArgsConstructor
1515
@NoArgsConstructor
16-
@Table(name = "club_members")
16+
@Table(name = "club_members", uniqueConstraints = {
17+
@UniqueConstraint(columnNames = {"club_id","user_id"}) // 같은 사용자가 같은 동호회에 중복 가입을 방지
18+
})
1719
public class ClubMember extends BaseTimeEntity {
1820

1921
@Id
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.be.sportizebe.domain.club.repository;
22

3+
import com.be.sportizebe.domain.club.entity.Club;
34
import com.be.sportizebe.domain.club.entity.ClubMember;
5+
import com.be.sportizebe.domain.user.entity.User;
46
import org.springframework.data.jpa.repository.JpaRepository;
57

68
public interface ClubMemberRepository extends JpaRepository<ClubMember, Long> {
9+
10+
// 특정 사용자가 특정 동호회에 이미 가입했는지 확인
11+
boolean existsByClubAndUser(Club club, User user);
712
int countByClubId(Long clubId);
813
}

src/main/java/com/be/sportizebe/domain/club/service/ClubService.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010
import org.springframework.web.multipart.MultipartFile;
1111

1212
public interface ClubService {
13-
ClubResponse createClub(ClubCreateRequest request, MultipartFile image, User user); // 동호회 생성
13+
ClubResponse createClub(ClubCreateRequest request, MultipartFile image, Long userId); // 동호회 생성
1414

15-
ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user); // 동호회 수정
16-
17-
ClubImageResponse updateClubImage(Long clubId, MultipartFile image, User user); // 동호회 사진 수정
15+
ClubResponse updateClub(Long clubId, ClubUpdateRequest request, Long userId); // 동호회 수정
1816

1917
ClubDetailResponse getClub(Long clubId); // 동호회 개별 조회
2018

2119
ClubScrollResponse getClubsByScroll(Long cursor, int size); // 동호회 전체 조회 (무한 스크롤)
2220

2321
ClubScrollResponse getMyClubsByScroll(Long cursor, int size, User user); // 내가 가입한 동호회 조회 (무한 스크롤)
2422

23+
ClubImageResponse updateClubImage(Long clubId, MultipartFile image, Long userId); // 동호회 사진 수정
2524
}

0 commit comments

Comments
 (0)