From 8574e392be9d02a0e655eb556c964c7de2a4ae5b Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Mon, 26 Jan 2026 21:11:00 +0900 Subject: [PATCH 1/6] =?UTF-8?q?:sparkles:Feat:=20=EB=8F=99=ED=98=B8?= =?UTF-8?q?=ED=9A=8C=20=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/controller/ClubController.java | 37 +++++++++++++++ .../club/dto/request/ClubCreateRequest.java | 25 ++++++++++ .../club/dto/response/ClubResponse.java | 28 +++++++++++ .../sportizebe/domain/club/entity/Club.java | 46 +++++++++++++++++++ .../domain/club/entity/ClubMember.java | 39 ++++++++++++++++ .../domain/club/exception/ClubErrorCode.java | 17 +++++++ .../club/repository/ClubMemberRepository.java | 7 +++ .../club/repository/ClubRepository.java | 8 ++++ .../domain/club/service/ClubService.java | 10 ++++ .../domain/club/service/ClubServiceImpl.java | 44 ++++++++++++++++++ .../post/dto/request/CreatePostRequest.java | 2 +- 11 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/entity/Club.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/entity/ClubMember.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/repository/ClubMemberRepository.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/repository/ClubRepository.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/service/ClubService.java create mode 100644 src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java diff --git a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java new file mode 100644 index 0000000..da21813 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java @@ -0,0 +1,37 @@ +package com.be.sportizebe.domain.club.controller; + +import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; +import com.be.sportizebe.domain.club.dto.response.ClubResponse; +import com.be.sportizebe.domain.club.service.ClubServiceImpl; +import com.be.sportizebe.domain.user.entity.SportType; +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.global.response.BaseResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/clubs") +@Tag(name = "club", description = "동호회 관련 API") +public class ClubController { + + private final ClubServiceImpl clubService; + + @PostMapping("/{sportType}") + @Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다.") + public ResponseEntity> createClub( + @Parameter(description = "종목 (SOCCER, BASKETBALL)") @PathVariable SportType sportType, + @RequestBody @Valid ClubCreateRequest request, + @AuthenticationPrincipal User user) { + ClubResponse response = clubService.createClub(sportType, request, user); + return ResponseEntity.status(HttpStatus.CREATED) + .body(BaseResponse.success("동호회 생성 성공", response)); + } +} diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java new file mode 100644 index 0000000..9a9e00f --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java @@ -0,0 +1,25 @@ +package com.be.sportizebe.domain.club.dto.request; + +import com.be.sportizebe.domain.club.entity.Club; +import com.be.sportizebe.domain.user.entity.SportType; +import com.be.sportizebe.domain.user.entity.User; +import jakarta.validation.constraints.NotBlank; + +public record ClubCreateRequest( + @NotBlank(message = "동호회 이름은 필수 입니다.") + String name, + String introduce, + Integer maxMembers) { + // 관련 종목은 파라미터로 받음 + // TODO : S3 세팅 후 imgUrl은 multipartform으로 변경 + + public Club toEntity(SportType sportType, User user) { + return Club.builder() + .name(name) + .introduce(introduce) + .maxMembers(maxMembers) + .sportType(sportType) + .leader(user) + .build(); + } +} diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java b/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java new file mode 100644 index 0000000..0b620af --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java @@ -0,0 +1,28 @@ +package com.be.sportizebe.domain.club.dto.response; + +import com.be.sportizebe.domain.club.entity.Club; +import com.be.sportizebe.domain.user.entity.SportType; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; + +@Builder +@Schema(title = "ClubResponse DTO", description = "동호회 관련 응답") +public record ClubResponse( + @Schema(description = "동호회 ID", example = "1") Long clubId, + @Schema(description = "동호회 이름", example = "축구 동호회") String name, + @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, + @Schema(description = "종목", example = "SOCCER") SportType sportType, + @Schema(description = "최대 정원", example = "20") Integer maxMembers, + @Schema(description = "동호회장 닉네임", example = "닉네임") String leaderNickname) { + + public static ClubResponse from(Club club) { + return ClubResponse.builder() + .clubId(club.getId()) + .name(club.getName()) + .introduce(club.getIntroduce()) + .sportType(club.getSportType()) + .maxMembers(club.getMaxMembers()) + .leaderNickname(club.getLeader().getNickname()) + .build(); + } +} diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java new file mode 100644 index 0000000..0d25ebd --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -0,0 +1,46 @@ +package com.be.sportizebe.domain.club.entity; + +import com.be.sportizebe.domain.user.entity.SportType; +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "clubs") +public class Club extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private String name; // 동호회 이름 + + @Column(columnDefinition = "TEXT") + private String introduce; // 동호회 소개글 + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SportType sportType; // 동호회의 종목 + + @Column(nullable = false) + private Integer maxMembers; // 최대 정원 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "leader_id", nullable = false) + private User leader; // 동호회장 + + @OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true) + @Builder.Default + private List members = new ArrayList<>(); +} diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/ClubMember.java b/src/main/java/com/be/sportizebe/domain/club/entity/ClubMember.java new file mode 100644 index 0000000..597c1b4 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/entity/ClubMember.java @@ -0,0 +1,39 @@ +package com.be.sportizebe.domain.club.entity; + +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.global.common.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "club_members") +public class ClubMember extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "club_id", nullable = false) + private Club club; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private ClubRole role = ClubRole.MEMBER; + + public enum ClubRole { + LEADER, MEMBER + } +} diff --git a/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java b/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java new file mode 100644 index 0000000..51f1f08 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java @@ -0,0 +1,17 @@ +package com.be.sportizebe.domain.club.exception; + +import com.be.sportizebe.global.exception.model.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ClubErrorCode implements BaseErrorCode { + CLUB_NOT_FOUND("CLUB_001", "동호회를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + CLUB_NAME_DUPLICATED("CLUB_002", "이미 존재하는 동호회 이름입니다.", HttpStatus.CONFLICT); + + private final String code; + private final String message; + private final HttpStatus status; +} diff --git a/src/main/java/com/be/sportizebe/domain/club/repository/ClubMemberRepository.java b/src/main/java/com/be/sportizebe/domain/club/repository/ClubMemberRepository.java new file mode 100644 index 0000000..c904683 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/repository/ClubMemberRepository.java @@ -0,0 +1,7 @@ +package com.be.sportizebe.domain.club.repository; + +import com.be.sportizebe.domain.club.entity.ClubMember; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ClubMemberRepository extends JpaRepository { +} diff --git a/src/main/java/com/be/sportizebe/domain/club/repository/ClubRepository.java b/src/main/java/com/be/sportizebe/domain/club/repository/ClubRepository.java new file mode 100644 index 0000000..f5f5110 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/repository/ClubRepository.java @@ -0,0 +1,8 @@ +package com.be.sportizebe.domain.club.repository; + +import com.be.sportizebe.domain.club.entity.Club; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ClubRepository extends JpaRepository { + boolean existsByName(String name); +} diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java new file mode 100644 index 0000000..53da1a2 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java @@ -0,0 +1,10 @@ +package com.be.sportizebe.domain.club.service; + +import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; +import com.be.sportizebe.domain.club.dto.response.ClubResponse; +import com.be.sportizebe.domain.user.entity.SportType; +import com.be.sportizebe.domain.user.entity.User; + +public interface ClubService { + ClubResponse createClub(SportType sportType, ClubCreateRequest request, User user); // 동호회 생성 +} diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java new file mode 100644 index 0000000..ea9fa35 --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -0,0 +1,44 @@ +package com.be.sportizebe.domain.club.service; + +import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; +import com.be.sportizebe.domain.club.dto.response.ClubResponse; +import com.be.sportizebe.domain.club.entity.Club; +import com.be.sportizebe.domain.club.entity.ClubMember; +import com.be.sportizebe.domain.club.exception.ClubErrorCode; +import com.be.sportizebe.domain.club.repository.ClubMemberRepository; +import com.be.sportizebe.domain.club.repository.ClubRepository; +import com.be.sportizebe.domain.user.entity.SportType; +import com.be.sportizebe.domain.user.entity.User; +import com.be.sportizebe.global.exception.CustomException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ClubServiceImpl implements ClubService { + + private final ClubRepository clubRepository; + private final ClubMemberRepository clubMemberRepository; + + @Override + @Transactional + public ClubResponse createClub(SportType sportType, ClubCreateRequest request, User user) { + if (clubRepository.existsByName(request.name())) { + throw new CustomException(ClubErrorCode.CLUB_NAME_DUPLICATED); + } + + Club club = request.toEntity(sportType, user); + clubRepository.save(club); + + ClubMember leaderMember = ClubMember.builder() + .club(club) + .user(user) + .role(ClubMember.ClubRole.LEADER) + .build(); + clubMemberRepository.save(leaderMember); + + return ClubResponse.from(club); + } +} diff --git a/src/main/java/com/be/sportizebe/domain/post/dto/request/CreatePostRequest.java b/src/main/java/com/be/sportizebe/domain/post/dto/request/CreatePostRequest.java index baa2ebb..ed609fa 100644 --- a/src/main/java/com/be/sportizebe/domain/post/dto/request/CreatePostRequest.java +++ b/src/main/java/com/be/sportizebe/domain/post/dto/request/CreatePostRequest.java @@ -12,7 +12,7 @@ public record CreatePostRequest( @NotBlank(message = "내용은 필수입니다.") String content, boolean isAnonymous, String imgUrl) { - +// TODO : S3 세팅 후 imgUrl은 multipartform으로 변경 public Post toEntity(PostProperty property, User user) { // DTO -> Entity 변환 return Post.builder() .title(title) From 9f4210711eb54f7a55f68d0206084d8f8c4e47aa Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Wed, 28 Jan 2026 18:43:24 +0900 Subject: [PATCH 2/6] =?UTF-8?q?:recycel:Refactor:=20Club=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=B0=B8=EC=A1=B0=EC=97=90=20=EB=94=B0=EB=A5=B8=20?= =?UTF-8?q?ChatRoom=20=ED=95=84=EB=93=9C=20=EC=B5=9C=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/response/ChatRoomResponse.java | 20 +++++++++---------- .../domain/chat/entity/ChatRoom.java | 16 +++++++-------- .../chat/repository/ChatRoomRepository.java | 6 +++--- .../domain/chat/service/ChatRoomService.java | 8 ++------ .../sportizebe/domain/club/entity/Club.java | 4 ++++ .../domain/club/service/ClubServiceImpl.java | 1 + 6 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java b/src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java index 0410dd2..07ed074 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java +++ b/src/main/java/com/be/sportizebe/domain/chat/dto/response/ChatRoomResponse.java @@ -11,7 +11,6 @@ public class ChatRoomResponse { private String name; private boolean active; private String chatRoomType; - private Integer maxMembers; // 1대1 채팅방(쪽지)용 필드 private Long postId; @@ -23,19 +22,20 @@ public class ChatRoomResponse { public static ChatRoomResponse from(ChatRoom r) { ChatRoomResponseBuilder builder = ChatRoomResponse.builder() .roomId(r.getId()) - .name(r.getName()) .active(r.isActive()) - .chatRoomType(r.getChatRoomType() != null ? r.getChatRoomType().name() : null) - .maxMembers(r.getMaxMembers()); + .chatRoomType(r.getChatRoomType() != null ? r.getChatRoomType().name() : null); - // 1대1 채팅방인 경우 추가 정보 설정 - if (r.getPost() != null) { + // 타입별 name 파생 + if (r.getChatRoomType() == ChatRoom.ChatRoomType.NOTE && r.getPost() != null) { + builder.name(r.getPost().getTitle()); builder.postId(r.getPost().getId()); + // hostUser는 게시글 작성자로 파생 + builder.hostUserId(r.getPost().getUser().getId()) + .hostUsername(r.getPost().getUser().getUsername()); + } else if (r.getChatRoomType() == ChatRoom.ChatRoomType.GROUP && r.getClub() != null) { + builder.name(r.getClub().getName()); } - if (r.getHostUser() != null) { - builder.hostUserId(r.getHostUser().getId()) - .hostUsername(r.getHostUser().getUsername()); - } + if (r.getGuestUser() != null) { builder.guestUserId(r.getGuestUser().getId()) .guestUsername(r.getGuestUser().getUsername()); diff --git a/src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java b/src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java index 8c47ddd..7204dd0 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java +++ b/src/main/java/com/be/sportizebe/domain/chat/entity/ChatRoom.java @@ -1,6 +1,7 @@ package com.be.sportizebe.domain.chat.entity; +import com.be.sportizebe.domain.club.entity.Club; import com.be.sportizebe.domain.post.entity.Post; import com.be.sportizebe.domain.user.entity.User; import jakarta.persistence.*; @@ -25,12 +26,8 @@ public enum ChatRoomType { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name="name", nullable = false, length = 100) - private String name; - + @Enumerated(EnumType.STRING) @Column(nullable = false) - private Integer maxMembers; // 채팅방 최대 정원 - private ChatRoomType chatRoomType; // 채팅방 타입 (단체, 1:1) @Column(name="is_active", nullable = false) @@ -39,15 +36,16 @@ public enum ChatRoomType { @Column(name="created_at", nullable = false, updatable = false) private Instant createdAt; + // 동호회(GROUP) 채팅방용 필드 + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "club_id") + private Club club; + // 1대1 채팅(쪽지)용 필드 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id") private Post post; // 연관 게시글 - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "host_user_id") - private User hostUser; // 게시글 작성자 - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "guest_user_id") private User guestUser; // 채팅 요청자 diff --git a/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java b/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java index f4c5939..1fc70ea 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/be/sportizebe/domain/chat/repository/ChatRoomRepository.java @@ -11,8 +11,8 @@ public interface ChatRoomRepository extends JpaRepository { // 특정 게시글에 대해 두 사용자 간의 1대1 채팅방이 이미 존재하는지 확인 - Optional findByPostAndHostUserAndGuestUser(Post post, User hostUser, User guestUser); + Optional findByPostAndGuestUser(Post post, User guestUser); - // 사용자가 참여한 1대1 채팅방 목록 조회 (host 또는 guest로 참여) - List findByHostUserOrGuestUser(User hostUser, User guestUser); + // 사용자가 참여한 1대1 채팅방 목록 조회 (게시글 작성자 또는 채팅 요청자로 참여) + List findByPost_UserOrGuestUser(User postUser, User guestUser); } diff --git a/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java b/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java index 0f83f68..7b7385d 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java +++ b/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java @@ -21,7 +21,6 @@ public class ChatRoomService { @Transactional public ChatRoom createGroup(String name) { ChatRoom room = ChatRoom.builder() - .name(name) .chatRoomType(ChatRoom.ChatRoomType.GROUP) .build(); return chatRoomRepository.save(room); @@ -37,14 +36,11 @@ public ChatRoom createNote(Post post, User guestUser) { } // 이미 존재하는 채팅방이 있으면 반환 - return chatRoomRepository.findByPostAndHostUserAndGuestUser(post, hostUser, guestUser) + return chatRoomRepository.findByPostAndGuestUser(post, guestUser) .orElseGet(() -> { ChatRoom room = ChatRoom.builder() - .name(post.getTitle()) .chatRoomType(ChatRoom.ChatRoomType.NOTE) - .maxMembers(2) .post(post) - .hostUser(hostUser) .guestUser(guestUser) .build(); return chatRoomRepository.save(room); @@ -53,7 +49,7 @@ public ChatRoom createNote(Post post, User guestUser) { // 사용자가 참여한 1대1 채팅방 목록 조회 public List findMyNoteRooms(User user) { - return chatRoomRepository.findByHostUserOrGuestUser(user, user); + return chatRoomRepository.findByPost_UserOrGuestUser(user, user); } public List findAll() { diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java index 0d25ebd..7a84bd6 100644 --- a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.club.entity; +import com.be.sportizebe.domain.chat.entity.ChatRoom; import com.be.sportizebe.domain.user.entity.SportType; import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.common.BaseTimeEntity; @@ -40,6 +41,9 @@ public class Club extends BaseTimeEntity { @JoinColumn(name = "leader_id", nullable = false) private User leader; // 동호회장 + @OneToOne(mappedBy = "club", fetch = FetchType.LAZY) + private ChatRoom chatRoom; // 동호회 채팅방 + @OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private List members = new ArrayList<>(); diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java index ea9fa35..84eaaad 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -29,6 +29,7 @@ public ClubResponse createClub(SportType sportType, ClubCreateRequest request, U throw new CustomException(ClubErrorCode.CLUB_NAME_DUPLICATED); } + Club club = request.toEntity(sportType, user); clubRepository.save(club); From 5e17619ef52d09e6beea50a57cfb50c6e8c9977c Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Wed, 28 Jan 2026 19:23:41 +0900 Subject: [PATCH 3/6] =?UTF-8?q?:recycle:Refactor:=20=EB=8F=99=ED=98=B8?= =?UTF-8?q?=ED=9A=8C=20=EC=83=9D=EC=84=B1=EA=B3=BC=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EA=B0=99=EC=9D=80=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=95=88=EC=97=90=EC=84=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/stomp-test.html | 18 +- .../domain/auth/dto/request/LoginRequest.java | 3 + .../domain/chat/service/ChatRoomService.java | 8 +- .../club/controller/ClubController.java | 3 +- .../club/dto/request/ClubCreateRequest.java | 10 +- .../club/dto/response/ClubResponse.java | 2 - .../sportizebe/domain/club/entity/Club.java | 6 +- .../domain/club/service/ClubService.java | 2 +- .../domain/club/service/ClubServiceImpl.java | 11 +- test/stomp-test.html | 547 ------------------ 10 files changed, 33 insertions(+), 577 deletions(-) delete mode 100644 test/stomp-test.html diff --git a/assets/stomp-test.html b/assets/stomp-test.html index d03f1fb..efd0dc6 100644 --- a/assets/stomp-test.html +++ b/assets/stomp-test.html @@ -413,7 +413,7 @@ } const socket = new SockJS( - "http://localhost:8080/ws", + "http://localhost:8080/ws-stomp", null, { transports: ["xhr-streaming", "xhr-polling"] } // ✅ opCode=7 우회 ); @@ -442,7 +442,7 @@ } const rid = roomIdValue(); - const dest = `/topic/chat/rooms/${rid}`; + const dest = `/sub/chat/rooms/${rid}`; $("roomTitle").textContent = `room: ${rid}`; currentSub = stompClient.subscribe(dest, (frame) => { @@ -459,10 +459,10 @@ }); } }); - stompClient.send("/app/chat.join", {}, JSON.stringify({ + stompClient.send("/pub/chat.join", {}, JSON.stringify({ roomId: Number(roomIdValue()), - userId: myUserId(), - nickname: myNickname() + senderUserId: myUserId(), + senderNickname: myNickname() })); setSub("subscribed: " + dest); debug("[SUBSCRIBED] " + dest); @@ -490,7 +490,7 @@ content: text }; - stompClient.send("/app/chat.send", {}, JSON.stringify(payload)); + stompClient.send("/pub/chat.send", {}, JSON.stringify(payload)); debug("[SENT] " + JSON.stringify(payload)); contentEl.value = ""; @@ -501,10 +501,10 @@ // 🔽 여기 먼저 if (stompClient && stompClient.connected) { - stompClient.send("/app/chat.leave", {}, JSON.stringify({ + stompClient.send("/pub/chat.leave", {}, JSON.stringify({ roomId: Number(roomIdValue()), - userId: myUserId(), - nickname: myNickname() + senderUserId: myUserId(), + senderNickname: myNickname() })); } diff --git a/src/main/java/com/be/sportizebe/domain/auth/dto/request/LoginRequest.java b/src/main/java/com/be/sportizebe/domain/auth/dto/request/LoginRequest.java index bdd17ca..24f16b1 100644 --- a/src/main/java/com/be/sportizebe/domain/auth/dto/request/LoginRequest.java +++ b/src/main/java/com/be/sportizebe/domain/auth/dto/request/LoginRequest.java @@ -1,14 +1,17 @@ package com.be.sportizebe.domain.auth.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; public record LoginRequest( @NotBlank(message = "아이디를 입력해주세요.") @Email(message = "아이디는 이메일 형식만 지원합니다.") + @Schema(description = "사용자 아이디(이메일 형식)", example = "user@example.com") String username, @NotBlank(message = "비밀번호를 입력해주세요.") + @Schema(description = "비밀번호", example = "password123") String password ) { } \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java b/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java index 7b7385d..2363281 100644 --- a/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java +++ b/src/main/java/com/be/sportizebe/domain/chat/service/ChatRoomService.java @@ -3,6 +3,7 @@ import com.be.sportizebe.domain.chat.entity.ChatRoom; import com.be.sportizebe.domain.chat.exception.ChatErrorCode; import com.be.sportizebe.domain.chat.repository.ChatRoomRepository; +import com.be.sportizebe.domain.club.entity.Club; import com.be.sportizebe.domain.post.entity.Post; import com.be.sportizebe.domain.user.entity.User; import com.be.sportizebe.global.exception.CustomException; @@ -19,10 +20,11 @@ public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; @Transactional - public ChatRoom createGroup(String name) { + public ChatRoom createGroup(Club club) { ChatRoom room = ChatRoom.builder() - .chatRoomType(ChatRoom.ChatRoomType.GROUP) - .build(); + .chatRoomType(ChatRoom.ChatRoomType.GROUP) + .club(club) + .build(); return chatRoomRepository.save(room); } diff --git a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java index da21813..3f825ef 100644 --- a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java +++ b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java @@ -27,10 +27,9 @@ public class ClubController { @PostMapping("/{sportType}") @Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다.") public ResponseEntity> createClub( - @Parameter(description = "종목 (SOCCER, BASKETBALL)") @PathVariable SportType sportType, @RequestBody @Valid ClubCreateRequest request, @AuthenticationPrincipal User user) { - ClubResponse response = clubService.createClub(sportType, request, user); + ClubResponse response = clubService.createClub(request, user); return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("동호회 생성 성공", response)); } diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java index 9a9e00f..7df8a9b 100644 --- a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java @@ -3,22 +3,22 @@ import com.be.sportizebe.domain.club.entity.Club; import com.be.sportizebe.domain.user.entity.SportType; import com.be.sportizebe.domain.user.entity.User; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; public record ClubCreateRequest( @NotBlank(message = "동호회 이름은 필수 입니다.") - String name, - String introduce, - Integer maxMembers) { + @Schema(description = "동호회 이름", example = "축구 동호회") String name, + @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, + @Schema(description = "최대 정원", example = "20") Integer maxMembers) { // 관련 종목은 파라미터로 받음 // TODO : S3 세팅 후 imgUrl은 multipartform으로 변경 - public Club toEntity(SportType sportType, User user) { + public Club toEntity(User user) { return Club.builder() .name(name) .introduce(introduce) .maxMembers(maxMembers) - .sportType(sportType) .leader(user) .build(); } diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java b/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java index 0b620af..95e9de5 100644 --- a/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java +++ b/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java @@ -11,7 +11,6 @@ public record ClubResponse( @Schema(description = "동호회 ID", example = "1") Long clubId, @Schema(description = "동호회 이름", example = "축구 동호회") String name, @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, - @Schema(description = "종목", example = "SOCCER") SportType sportType, @Schema(description = "최대 정원", example = "20") Integer maxMembers, @Schema(description = "동호회장 닉네임", example = "닉네임") String leaderNickname) { @@ -20,7 +19,6 @@ public static ClubResponse from(Club club) { .clubId(club.getId()) .name(club.getName()) .introduce(club.getIntroduce()) - .sportType(club.getSportType()) .maxMembers(club.getMaxMembers()) .leaderNickname(club.getLeader().getNickname()) .build(); diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java index 7a84bd6..3e71ec4 100644 --- a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -25,15 +25,11 @@ public class Club extends BaseTimeEntity { private Long id; @Column(nullable = false, unique = true) - private String name; // 동호회 이름 + private String name; // 동호회 이름 == 단테 채팅방 이름 @Column(columnDefinition = "TEXT") private String introduce; // 동호회 소개글 - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private SportType sportType; // 동호회의 종목 - @Column(nullable = false) private Integer maxMembers; // 최대 정원 diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java index 53da1a2..0d21564 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java @@ -6,5 +6,5 @@ import com.be.sportizebe.domain.user.entity.User; public interface ClubService { - ClubResponse createClub(SportType sportType, ClubCreateRequest request, User user); // 동호회 생성 + ClubResponse createClub(ClubCreateRequest request, User user); // 동호회 생성 } diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java index 84eaaad..1c7eff0 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.club.service; +import com.be.sportizebe.domain.chat.service.ChatRoomService; import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; import com.be.sportizebe.domain.club.dto.response.ClubResponse; import com.be.sportizebe.domain.club.entity.Club; @@ -20,17 +21,18 @@ public class ClubServiceImpl implements ClubService { private final ClubRepository clubRepository; + private final ChatRoomService chatRoomService; private final ClubMemberRepository clubMemberRepository; @Override @Transactional - public ClubResponse createClub(SportType sportType, ClubCreateRequest request, User user) { + public ClubResponse createClub(ClubCreateRequest request, User user) { if (clubRepository.existsByName(request.name())) { throw new CustomException(ClubErrorCode.CLUB_NAME_DUPLICATED); } - - Club club = request.toEntity(sportType, user); + // 동호회 엔티티 생성 + Club club = request.toEntity(user); clubRepository.save(club); ClubMember leaderMember = ClubMember.builder() @@ -40,6 +42,9 @@ public ClubResponse createClub(SportType sportType, ClubCreateRequest request, U .build(); clubMemberRepository.save(leaderMember); + // 동호회 단체 채팅방 생성 + chatRoomService.createGroup(club); + return ClubResponse.from(club); } } diff --git a/test/stomp-test.html b/test/stomp-test.html deleted file mode 100644 index efd0dc6..0000000 --- a/test/stomp-test.html +++ /dev/null @@ -1,547 +0,0 @@ - - - - - - Group Chat UI (SockJS + STOMP) - - - - - - - - - -
- -
-
-
- 채팅 설정 -
멀티창 테스트용 (각 창에서 닉네임 다르게)
-
- -
- -
-
-
- - -
-
- - -
-
- -
- - -
- -
- - -
-
- -
-
- - DISCONNECTED -
-
not subscribed
-
-
- - -
-
-
- 단체 채팅방 -
room: 1
-
-
Enter로 전송 / Subscribe 후 수신
-
- -
- -
- - -
- -
- 디버그 로그 보기 -
-
-
-
- - - - - \ No newline at end of file From 702717d57cb601a32360f0b9a7362a323c4776f7 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Wed, 28 Jan 2026 19:36:49 +0900 Subject: [PATCH 4/6] =?UTF-8?q?:sparkles:Feat:=20=EB=8F=99=ED=98=B8?= =?UTF-8?q?=ED=9A=8C=20=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../club/controller/ClubController.java | 13 +++++++++- .../club/dto/request/ClubUpdateRequest.java | 11 +++++++++ .../sportizebe/domain/club/entity/Club.java | 6 +++++ .../domain/club/exception/ClubErrorCode.java | 4 +++- .../domain/club/service/ClubService.java | 3 +++ .../domain/club/service/ClubServiceImpl.java | 24 +++++++++++++++++++ 6 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java diff --git a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java index 3f825ef..e443795 100644 --- a/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java +++ b/src/main/java/com/be/sportizebe/domain/club/controller/ClubController.java @@ -1,6 +1,7 @@ package com.be.sportizebe.domain.club.controller; import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; +import com.be.sportizebe.domain.club.dto.request.ClubUpdateRequest; import com.be.sportizebe.domain.club.dto.response.ClubResponse; import com.be.sportizebe.domain.club.service.ClubServiceImpl; import com.be.sportizebe.domain.user.entity.SportType; @@ -24,7 +25,7 @@ public class ClubController { private final ClubServiceImpl clubService; - @PostMapping("/{sportType}") + @PostMapping("") @Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다.") public ResponseEntity> createClub( @RequestBody @Valid ClubCreateRequest request, @@ -33,4 +34,14 @@ public ResponseEntity> createClub( return ResponseEntity.status(HttpStatus.CREATED) .body(BaseResponse.success("동호회 생성 성공", response)); } + + @PutMapping("/{clubId}") + @Operation(summary = "동호회 수정", description = "동호회 정보를 수정합니다. 동호회장만 수정할 수 있습니다.") + public ResponseEntity> updateClub( + @PathVariable Long clubId, + @RequestBody @Valid ClubUpdateRequest request, + @AuthenticationPrincipal User user) { + ClubResponse response = clubService.updateClub(clubId, request, user); + return ResponseEntity.ok(BaseResponse.success("동호회 수정 성공", response)); + } } diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java new file mode 100644 index 0000000..2d1966e --- /dev/null +++ b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java @@ -0,0 +1,11 @@ +package com.be.sportizebe.domain.club.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record ClubUpdateRequest( + @NotBlank(message = "동호회 이름은 필수 입니다.") + @Schema(description = "동호회 이름", example = "축구 동호회") String name, + @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, + @Schema(description = "최대 정원", example = "20") Integer maxMembers) { +} diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java index 3e71ec4..d7bacef 100644 --- a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -43,4 +43,10 @@ public class Club extends BaseTimeEntity { @OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private List members = new ArrayList<>(); + + public void update(String name, String introduce, Integer maxMembers) { + this.name = name; + this.introduce = introduce; + this.maxMembers = maxMembers; + } } diff --git a/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java b/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java index 51f1f08..3043725 100644 --- a/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java +++ b/src/main/java/com/be/sportizebe/domain/club/exception/ClubErrorCode.java @@ -9,7 +9,9 @@ @AllArgsConstructor public enum ClubErrorCode implements BaseErrorCode { CLUB_NOT_FOUND("CLUB_001", "동호회를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), - CLUB_NAME_DUPLICATED("CLUB_002", "이미 존재하는 동호회 이름입니다.", HttpStatus.CONFLICT); + CLUB_NAME_DUPLICATED("CLUB_002", "이미 존재하는 동호회 이름입니다.", HttpStatus.CONFLICT), + CLUB_UPDATE_DENIED("CLUB_003", "동호회 수정 권한이 없습니다.", HttpStatus.FORBIDDEN), + CLUB_MAX_MEMBERS_TOO_SMALL("CLUB_004", "최대 정원은 현재 참여 인원보다 적을 수 없습니다.", HttpStatus.BAD_REQUEST); private final String code; private final String message; diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java index 0d21564..89c19f6 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubService.java @@ -1,10 +1,13 @@ package com.be.sportizebe.domain.club.service; import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; +import com.be.sportizebe.domain.club.dto.request.ClubUpdateRequest; import com.be.sportizebe.domain.club.dto.response.ClubResponse; import com.be.sportizebe.domain.user.entity.SportType; import com.be.sportizebe.domain.user.entity.User; public interface ClubService { ClubResponse createClub(ClubCreateRequest request, User user); // 동호회 생성 + + ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user); // 동호회 수정 } diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java index 1c7eff0..23358cf 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -2,6 +2,7 @@ import com.be.sportizebe.domain.chat.service.ChatRoomService; import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest; +import com.be.sportizebe.domain.club.dto.request.ClubUpdateRequest; import com.be.sportizebe.domain.club.dto.response.ClubResponse; import com.be.sportizebe.domain.club.entity.Club; import com.be.sportizebe.domain.club.entity.ClubMember; @@ -47,4 +48,27 @@ public ClubResponse createClub(ClubCreateRequest request, User user) { return ClubResponse.from(club); } + + @Override + @Transactional + public ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user) { + Club club = clubRepository.findById(clubId) + .orElseThrow(() -> new CustomException(ClubErrorCode.CLUB_NOT_FOUND)); + + if (club.getLeader().getId() != user.getId()) { + throw new CustomException(ClubErrorCode.CLUB_UPDATE_DENIED); + } + + if (!club.getName().equals(request.name()) && clubRepository.existsByName(request.name())) { + throw new CustomException(ClubErrorCode.CLUB_NAME_DUPLICATED); + } + + if (request.maxMembers() != null && request.maxMembers() < club.getMembers().size()) { + throw new CustomException(ClubErrorCode.CLUB_MAX_MEMBERS_TOO_SMALL); + } + + club.update(request.name(), request.introduce(), request.maxMembers()); + + return ClubResponse.from(club); + } } From cd6f533b4ff73e6850ebfe91eac247a663642eb2 Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Thu, 29 Jan 2026 00:49:08 +0900 Subject: [PATCH 5/6] =?UTF-8?q?:recycle:Refactor:=20=EB=8F=99=ED=98=B8?= =?UTF-8?q?=ED=9A=8C=20=EC=83=9D=EC=84=B1,=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=20=EB=8F=99=ED=98=B8=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=A2=85=EB=AA=A9=20=ED=83=9C=EA=B7=B8=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/club/dto/request/ClubCreateRequest.java | 2 ++ .../domain/club/dto/request/ClubUpdateRequest.java | 2 ++ .../domain/club/dto/response/ClubResponse.java | 14 ++++++++------ .../com/be/sportizebe/domain/club/entity/Club.java | 7 ++++++- .../domain/club/service/ClubServiceImpl.java | 3 ++- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java index 7df8a9b..6d486c8 100644 --- a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubCreateRequest.java @@ -10,6 +10,7 @@ public record ClubCreateRequest( @NotBlank(message = "동호회 이름은 필수 입니다.") @Schema(description = "동호회 이름", example = "축구 동호회") String name, @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, + @Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType, @Schema(description = "최대 정원", example = "20") Integer maxMembers) { // 관련 종목은 파라미터로 받음 // TODO : S3 세팅 후 imgUrl은 multipartform으로 변경 @@ -18,6 +19,7 @@ public Club toEntity(User user) { return Club.builder() .name(name) .introduce(introduce) + .clubType(clubType) .maxMembers(maxMembers) .leader(user) .build(); diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java index 2d1966e..711527b 100644 --- a/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java +++ b/src/main/java/com/be/sportizebe/domain/club/dto/request/ClubUpdateRequest.java @@ -1,5 +1,6 @@ package com.be.sportizebe.domain.club.dto.request; +import com.be.sportizebe.domain.user.entity.SportType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -7,5 +8,6 @@ public record ClubUpdateRequest( @NotBlank(message = "동호회 이름은 필수 입니다.") @Schema(description = "동호회 이름", example = "축구 동호회") String name, @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, + @Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType, @Schema(description = "최대 정원", example = "20") Integer maxMembers) { } diff --git a/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java b/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java index 95e9de5..21c9934 100644 --- a/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java +++ b/src/main/java/com/be/sportizebe/domain/club/dto/response/ClubResponse.java @@ -11,16 +11,18 @@ public record ClubResponse( @Schema(description = "동호회 ID", example = "1") Long clubId, @Schema(description = "동호회 이름", example = "축구 동호회") String name, @Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce, + @Schema(description = "동호회 관련 종목", example = "SOCCER") SportType clubType, @Schema(description = "최대 정원", example = "20") Integer maxMembers, @Schema(description = "동호회장 닉네임", example = "닉네임") String leaderNickname) { public static ClubResponse from(Club club) { return ClubResponse.builder() - .clubId(club.getId()) - .name(club.getName()) - .introduce(club.getIntroduce()) - .maxMembers(club.getMaxMembers()) - .leaderNickname(club.getLeader().getNickname()) - .build(); + .clubId(club.getId()) + .name(club.getName()) + .introduce(club.getIntroduce()) + .clubType(club.getClubType()) + .maxMembers(club.getMaxMembers()) + .leaderNickname(club.getLeader().getNickname()) + .build(); } } diff --git a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java index d7bacef..0d5ccc3 100644 --- a/src/main/java/com/be/sportizebe/domain/club/entity/Club.java +++ b/src/main/java/com/be/sportizebe/domain/club/entity/Club.java @@ -30,6 +30,10 @@ public class Club extends BaseTimeEntity { @Column(columnDefinition = "TEXT") private String introduce; // 동호회 소개글 + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private SportType clubType; // 동호회 관련 종목 (동호회 생성 시 선택) + @Column(nullable = false) private Integer maxMembers; // 최대 정원 @@ -44,9 +48,10 @@ public class Club extends BaseTimeEntity { @Builder.Default private List members = new ArrayList<>(); - public void update(String name, String introduce, Integer maxMembers) { + public void update(String name, String introduce, Integer maxMembers, SportType clubType) { this.name = name; this.introduce = introduce; this.maxMembers = maxMembers; + this.clubType = clubType; } } diff --git a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java index 23358cf..bf27de5 100644 --- a/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/club/service/ClubServiceImpl.java @@ -36,6 +36,7 @@ public ClubResponse createClub(ClubCreateRequest request, User user) { Club club = request.toEntity(user); clubRepository.save(club); + // 동호회 멤버 테이블에 방장(동호회 생성자) 추가 ClubMember leaderMember = ClubMember.builder() .club(club) .user(user) @@ -67,7 +68,7 @@ public ClubResponse updateClub(Long clubId, ClubUpdateRequest request, User user throw new CustomException(ClubErrorCode.CLUB_MAX_MEMBERS_TOO_SMALL); } - club.update(request.name(), request.introduce(), request.maxMembers()); + club.update(request.name(), request.introduce(), request.maxMembers(), request.clubType()); return ClubResponse.from(club); } From 4fba4ae1d6107ede6761f45e973b30635753066b Mon Sep 17 00:00:00 2001 From: imjuyongp Date: Thu, 29 Jan 2026 01:20:58 +0900 Subject: [PATCH 6/6] =?UTF-8?q?:sparkles:Feat:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/controller/PostController.java | 13 +++++++++++++ .../domain/post/repository/PostRepository.java | 7 ++++++- .../sportizebe/domain/post/service/PostService.java | 4 ++++ .../domain/post/service/PostServiceImpl.java | 8 ++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java b/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java index 74c4a3f..175ae5c 100644 --- a/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java +++ b/src/main/java/com/be/sportizebe/domain/post/controller/PostController.java @@ -12,6 +12,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -54,4 +58,13 @@ public ResponseEntity> deletePost( postService.deletePost(postId, user); return ResponseEntity.ok(BaseResponse.success("게시글 삭제 성공", null)); } + + @GetMapping("/posts/{property}") + @Operation(summary = "게시글 목록 조회", description = "게시판 종류별 게시글 목록을 페이징하여 조회합니다.") + public ResponseEntity>> getPosts( + @Parameter(description = "게시판 종류 (SOCCER, BASKETBALL, FREE)") @PathVariable PostProperty property, + @Parameter(hidden = true) @PageableDefault(size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) { + Page response = postService.getPosts(property, pageable); + return ResponseEntity.ok(BaseResponse.success("게시글 목록 조회 성공", response)); + } } diff --git a/src/main/java/com/be/sportizebe/domain/post/repository/PostRepository.java b/src/main/java/com/be/sportizebe/domain/post/repository/PostRepository.java index 391e6a8..29e7a29 100644 --- a/src/main/java/com/be/sportizebe/domain/post/repository/PostRepository.java +++ b/src/main/java/com/be/sportizebe/domain/post/repository/PostRepository.java @@ -1,6 +1,11 @@ package com.be.sportizebe.domain.post.repository; import com.be.sportizebe.domain.post.entity.Post; +import com.be.sportizebe.domain.post.entity.PostProperty; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -public interface PostRepository extends JpaRepository {} \ No newline at end of file +public interface PostRepository extends JpaRepository { + Page findByProperty(PostProperty property, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/com/be/sportizebe/domain/post/service/PostService.java b/src/main/java/com/be/sportizebe/domain/post/service/PostService.java index 2bff325..1490c63 100644 --- a/src/main/java/com/be/sportizebe/domain/post/service/PostService.java +++ b/src/main/java/com/be/sportizebe/domain/post/service/PostService.java @@ -5,6 +5,8 @@ import com.be.sportizebe.domain.post.dto.response.PostResponse; import com.be.sportizebe.domain.post.entity.PostProperty; import com.be.sportizebe.domain.user.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface PostService { PostResponse createPost(PostProperty property, CreatePostRequest request, User user); // 게시글 생성 @@ -12,4 +14,6 @@ public interface PostService { PostResponse updatePost(Long postId, UpdatePostRequest request, User user); // 게시글 수정 void deletePost(Long postId, User user); // 게시글 삭제 + + Page getPosts(PostProperty property, Pageable pageable); // 게시글 목록 조회 } diff --git a/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java b/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java index 909ecf4..af5ec47 100644 --- a/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/be/sportizebe/domain/post/service/PostServiceImpl.java @@ -12,6 +12,8 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @Slf4j @@ -60,4 +62,10 @@ public void deletePost(Long postId, User user) { postRepository.delete(post); } + + @Override + public Page getPosts(PostProperty property, Pageable pageable) { + return postRepository.findByProperty(property, pageable) + .map(PostResponse::from); + } }