Skip to content

Commit 8574e39

Browse files
committed
✨Feat: 동호회 생성 기능 구현
1 parent b229556 commit 8574e39

File tree

11 files changed

+262
-1
lines changed

11 files changed

+262
-1
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.be.sportizebe.domain.club.controller;
2+
3+
import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest;
4+
import com.be.sportizebe.domain.club.dto.response.ClubResponse;
5+
import com.be.sportizebe.domain.club.service.ClubServiceImpl;
6+
import com.be.sportizebe.domain.user.entity.SportType;
7+
import com.be.sportizebe.domain.user.entity.User;
8+
import com.be.sportizebe.global.response.BaseResponse;
9+
import io.swagger.v3.oas.annotations.Operation;
10+
import io.swagger.v3.oas.annotations.Parameter;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import jakarta.validation.Valid;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
17+
import org.springframework.web.bind.annotation.*;
18+
19+
@RestController
20+
@RequiredArgsConstructor
21+
@RequestMapping("/api/clubs")
22+
@Tag(name = "club", description = "동호회 관련 API")
23+
public class ClubController {
24+
25+
private final ClubServiceImpl clubService;
26+
27+
@PostMapping("/{sportType}")
28+
@Operation(summary = "동호회 생성", description = "종목별 동호회를 생성합니다. 생성한 사용자가 동호회장이 됩니다.")
29+
public ResponseEntity<BaseResponse<ClubResponse>> createClub(
30+
@Parameter(description = "종목 (SOCCER, BASKETBALL)") @PathVariable SportType sportType,
31+
@RequestBody @Valid ClubCreateRequest request,
32+
@AuthenticationPrincipal User user) {
33+
ClubResponse response = clubService.createClub(sportType, request, user);
34+
return ResponseEntity.status(HttpStatus.CREATED)
35+
.body(BaseResponse.success("동호회 생성 성공", response));
36+
}
37+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.be.sportizebe.domain.club.dto.request;
2+
3+
import com.be.sportizebe.domain.club.entity.Club;
4+
import com.be.sportizebe.domain.user.entity.SportType;
5+
import com.be.sportizebe.domain.user.entity.User;
6+
import jakarta.validation.constraints.NotBlank;
7+
8+
public record ClubCreateRequest(
9+
@NotBlank(message = "동호회 이름은 필수 입니다.")
10+
String name,
11+
String introduce,
12+
Integer maxMembers) {
13+
// 관련 종목은 파라미터로 받음
14+
// TODO : S3 세팅 후 imgUrl은 multipartform으로 변경
15+
16+
public Club toEntity(SportType sportType, User user) {
17+
return Club.builder()
18+
.name(name)
19+
.introduce(introduce)
20+
.maxMembers(maxMembers)
21+
.sportType(sportType)
22+
.leader(user)
23+
.build();
24+
}
25+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.be.sportizebe.domain.club.dto.response;
2+
3+
import com.be.sportizebe.domain.club.entity.Club;
4+
import com.be.sportizebe.domain.user.entity.SportType;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import lombok.Builder;
7+
8+
@Builder
9+
@Schema(title = "ClubResponse DTO", description = "동호회 관련 응답")
10+
public record ClubResponse(
11+
@Schema(description = "동호회 ID", example = "1") Long clubId,
12+
@Schema(description = "동호회 이름", example = "축구 동호회") String name,
13+
@Schema(description = "동호회 소개", example = "매주 토요일 축구합니다") String introduce,
14+
@Schema(description = "종목", example = "SOCCER") SportType sportType,
15+
@Schema(description = "최대 정원", example = "20") Integer maxMembers,
16+
@Schema(description = "동호회장 닉네임", example = "닉네임") String leaderNickname) {
17+
18+
public static ClubResponse from(Club club) {
19+
return ClubResponse.builder()
20+
.clubId(club.getId())
21+
.name(club.getName())
22+
.introduce(club.getIntroduce())
23+
.sportType(club.getSportType())
24+
.maxMembers(club.getMaxMembers())
25+
.leaderNickname(club.getLeader().getNickname())
26+
.build();
27+
}
28+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.be.sportizebe.domain.club.entity;
2+
3+
import com.be.sportizebe.domain.user.entity.SportType;
4+
import com.be.sportizebe.domain.user.entity.User;
5+
import com.be.sportizebe.global.common.BaseTimeEntity;
6+
import jakarta.persistence.*;
7+
import lombok.AllArgsConstructor;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
import java.util.ArrayList;
13+
import java.util.List;
14+
15+
@Entity
16+
@Getter
17+
@Builder
18+
@AllArgsConstructor
19+
@NoArgsConstructor
20+
@Table(name = "clubs")
21+
public class Club extends BaseTimeEntity {
22+
@Id
23+
@GeneratedValue(strategy = GenerationType.IDENTITY)
24+
private Long id;
25+
26+
@Column(nullable = false, unique = true)
27+
private String name; // 동호회 이름
28+
29+
@Column(columnDefinition = "TEXT")
30+
private String introduce; // 동호회 소개글
31+
32+
@Enumerated(EnumType.STRING)
33+
@Column(nullable = false)
34+
private SportType sportType; // 동호회의 종목
35+
36+
@Column(nullable = false)
37+
private Integer maxMembers; // 최대 정원
38+
39+
@ManyToOne(fetch = FetchType.LAZY)
40+
@JoinColumn(name = "leader_id", nullable = false)
41+
private User leader; // 동호회장
42+
43+
@OneToMany(mappedBy = "club", cascade = CascadeType.ALL, orphanRemoval = true)
44+
@Builder.Default
45+
private List<ClubMember> members = new ArrayList<>();
46+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.be.sportizebe.domain.club.entity;
2+
3+
import com.be.sportizebe.domain.user.entity.User;
4+
import com.be.sportizebe.global.common.BaseTimeEntity;
5+
import jakarta.persistence.*;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
@Entity
12+
@Getter
13+
@Builder
14+
@AllArgsConstructor
15+
@NoArgsConstructor
16+
@Table(name = "club_members")
17+
public class ClubMember extends BaseTimeEntity {
18+
19+
@Id
20+
@GeneratedValue(strategy = GenerationType.IDENTITY)
21+
private Long id;
22+
23+
@ManyToOne(fetch = FetchType.LAZY)
24+
@JoinColumn(name = "club_id", nullable = false)
25+
private Club club;
26+
27+
@ManyToOne(fetch = FetchType.LAZY)
28+
@JoinColumn(name = "user_id", nullable = false)
29+
private User user;
30+
31+
@Enumerated(EnumType.STRING)
32+
@Column(nullable = false)
33+
@Builder.Default
34+
private ClubRole role = ClubRole.MEMBER;
35+
36+
public enum ClubRole {
37+
LEADER, MEMBER
38+
}
39+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.be.sportizebe.domain.club.exception;
2+
3+
import com.be.sportizebe.global.exception.model.BaseErrorCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import org.springframework.http.HttpStatus;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public enum ClubErrorCode implements BaseErrorCode {
11+
CLUB_NOT_FOUND("CLUB_001", "동호회를 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
12+
CLUB_NAME_DUPLICATED("CLUB_002", "이미 존재하는 동호회 이름입니다.", HttpStatus.CONFLICT);
13+
14+
private final String code;
15+
private final String message;
16+
private final HttpStatus status;
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.be.sportizebe.domain.club.repository;
2+
3+
import com.be.sportizebe.domain.club.entity.ClubMember;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface ClubMemberRepository extends JpaRepository<ClubMember, Long> {
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.be.sportizebe.domain.club.repository;
2+
3+
import com.be.sportizebe.domain.club.entity.Club;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface ClubRepository extends JpaRepository<Club, Long> {
7+
boolean existsByName(String name);
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.be.sportizebe.domain.club.service;
2+
3+
import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest;
4+
import com.be.sportizebe.domain.club.dto.response.ClubResponse;
5+
import com.be.sportizebe.domain.user.entity.SportType;
6+
import com.be.sportizebe.domain.user.entity.User;
7+
8+
public interface ClubService {
9+
ClubResponse createClub(SportType sportType, ClubCreateRequest request, User user); // 동호회 생성
10+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.be.sportizebe.domain.club.service;
2+
3+
import com.be.sportizebe.domain.club.dto.request.ClubCreateRequest;
4+
import com.be.sportizebe.domain.club.dto.response.ClubResponse;
5+
import com.be.sportizebe.domain.club.entity.Club;
6+
import com.be.sportizebe.domain.club.entity.ClubMember;
7+
import com.be.sportizebe.domain.club.exception.ClubErrorCode;
8+
import com.be.sportizebe.domain.club.repository.ClubMemberRepository;
9+
import com.be.sportizebe.domain.club.repository.ClubRepository;
10+
import com.be.sportizebe.domain.user.entity.SportType;
11+
import com.be.sportizebe.domain.user.entity.User;
12+
import com.be.sportizebe.global.exception.CustomException;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
@Transactional(readOnly = true)
20+
public class ClubServiceImpl implements ClubService {
21+
22+
private final ClubRepository clubRepository;
23+
private final ClubMemberRepository clubMemberRepository;
24+
25+
@Override
26+
@Transactional
27+
public ClubResponse createClub(SportType sportType, ClubCreateRequest request, User user) {
28+
if (clubRepository.existsByName(request.name())) {
29+
throw new CustomException(ClubErrorCode.CLUB_NAME_DUPLICATED);
30+
}
31+
32+
Club club = request.toEntity(sportType, user);
33+
clubRepository.save(club);
34+
35+
ClubMember leaderMember = ClubMember.builder()
36+
.club(club)
37+
.user(user)
38+
.role(ClubMember.ClubRole.LEADER)
39+
.build();
40+
clubMemberRepository.save(leaderMember);
41+
42+
return ClubResponse.from(club);
43+
}
44+
}

0 commit comments

Comments
 (0)