Skip to content

Commit b97d9df

Browse files
authored
[Feat] 새 그룹 생성 API 개발 (#62)
* feat: 새그룹 생성 요청 및 응답 DTO 구현 (#52) * feat: 그룹 생성 입력값 유효성 검증 로직 및 커스텀 예외 추가 (#52) * fix: 그룹/투표 이미지 URL에 도메인 prefix 검증 로직 추가 (#52) - VoteValidator.validateUrl() 로직을 도메인 기반으로 변경 - application.yml에 file.upload-url-prefix 추가 * feat: 새그룹 생성 서비스 로직 구현 (#52) * feat: 초대 코드 생성 로직 추가 (#52) * feat: 새그룹 생성 컨트롤러 구현 (#52) * fix: 투표/그룹 이미지 URL prefix를 application 설정 대신 하드코딩으로 변경 (#52) * feat: 그룹 생성 시 이름 중복 방지 로직 추가 (#52)
1 parent 4d5c325 commit b97d9df

File tree

12 files changed

+138
-13
lines changed

12 files changed

+138
-13
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
implementation 'org.springframework.boot:spring-boot-starter-web'
3030
implementation 'com.mysql:mysql-connector-j'
3131
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
32+
implementation 'org.apache.commons:commons-lang3:3.12.0'
3233
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
3334
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
3435
compileOnly 'org.projectlombok:lombok'

src/main/java/com/moa/moa_server/domain/group/controller/GroupController.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package com.moa.moa_server.domain.group.controller;
22

33
import com.moa.moa_server.domain.global.dto.ApiResponse;
4+
import com.moa.moa_server.domain.group.dto.request.GroupCreateRequest;
45
import com.moa.moa_server.domain.group.dto.request.GroupJoinRequest;
6+
import com.moa.moa_server.domain.group.dto.response.GroupCreateResponse;
57
import com.moa.moa_server.domain.group.dto.response.GroupJoinResponse;
68
import com.moa.moa_server.domain.group.service.GroupService;
7-
import com.moa.moa_server.domain.vote.dto.request.VoteCreateRequest;
89
import lombok.RequiredArgsConstructor;
910
import org.springframework.http.ResponseEntity;
1011
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -30,4 +31,15 @@ public ResponseEntity<ApiResponse> joinGroup(
3031
.status(201)
3132
.body(new ApiResponse("SUCCESS", response));
3233
}
34+
35+
@PostMapping
36+
public ResponseEntity<ApiResponse> createGroup(
37+
@AuthenticationPrincipal Long userId,
38+
@RequestBody GroupCreateRequest request
39+
) {
40+
GroupCreateResponse response = groupService.createGroup(userId, request);
41+
return ResponseEntity
42+
.status(201)
43+
.body(new ApiResponse("SUCCESS", response));
44+
}
3345
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.moa.moa_server.domain.group.dto.request;
2+
3+
public record GroupCreateRequest(
4+
String name,
5+
String description,
6+
String imageUrl
7+
) {}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.moa.moa_server.domain.group.dto.response;
2+
3+
import java.time.LocalDateTime;
4+
5+
public record GroupCreateResponse(
6+
Long groupId,
7+
String name,
8+
String description,
9+
String imageUrl,
10+
String inviteCode,
11+
LocalDateTime createdAt
12+
) {}

src/main/java/com/moa/moa_server/domain/group/entity/Group.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class Group extends BaseTimeEntity {
2929
@JoinColumn(name = "user_id", nullable = false)
3030
private User user;
3131

32-
@Column(name = "name", length = 50, nullable = false)
32+
@Column(name = "name", length = 50, nullable = false, unique = true)
3333
private String name;
3434

3535
@Column(name = "description", length = 100, nullable = false)
@@ -44,11 +44,12 @@ public class Group extends BaseTimeEntity {
4444
@Column(name = "deleted_at")
4545
private java.time.LocalDateTime deletedAt;
4646

47-
public static Group create(User user, String name, String description, String inviteCode) {
47+
public static Group create(User user, String name, String description, String imageUrl, String inviteCode) {
4848
return Group.builder()
4949
.user(user)
5050
.name(name)
5151
.description(description)
52+
.imageUrl(imageUrl)
5253
.inviteCode(inviteCode)
5354
.build();
5455
}

src/main/java/com/moa/moa_server/domain/group/entity/GroupMember.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,13 @@ public void rejoin() {
6565
this.joinedAt = LocalDateTime.now();
6666
this.role = Role.MEMBER;
6767
}
68+
69+
public static GroupMember createAsOwner(User user, Group group) {
70+
return GroupMember.builder()
71+
.user(user)
72+
.group(group)
73+
.role(Role.OWNER)
74+
.joinedAt(LocalDateTime.now())
75+
.build();
76+
}
6877
}

src/main/java/com/moa/moa_server/domain/group/handler/GroupErrorCode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ public enum GroupErrorCode implements BaseErrorCode {
88
INVALID_CODE_FORMAT(HttpStatus.BAD_REQUEST),
99
INVITE_CODE_NOT_FOUND(HttpStatus.NOT_FOUND),
1010
ALREADY_JOINED(HttpStatus.CONFLICT),
11-
CANNOT_JOIN_PUBLIC_GROUP(HttpStatus.BAD_REQUEST),;
11+
CANNOT_JOIN_PUBLIC_GROUP(HttpStatus.BAD_REQUEST),
12+
INVALID_INPUT(HttpStatus.BAD_REQUEST),
13+
INVITE_CODE_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR),
14+
DUPLICATE_NAME(HttpStatus.CONFLICT);
1215

1316
private final HttpStatus status;
1417

src/main/java/com/moa/moa_server/domain/group/repository/GroupRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@
77

88
public interface GroupRepository extends JpaRepository<Group, Long> {
99
Optional<Group> findByInviteCode(String inviteCode);
10+
boolean existsByInviteCode(String inviteCode);
11+
boolean existsByName(String groupName);
1012
}

src/main/java/com/moa/moa_server/domain/group/service/GroupService.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.moa.moa_server.domain.group.service;
22

3+
import com.moa.moa_server.domain.group.dto.request.GroupCreateRequest;
34
import com.moa.moa_server.domain.group.dto.request.GroupJoinRequest;
5+
import com.moa.moa_server.domain.group.dto.response.GroupCreateResponse;
46
import com.moa.moa_server.domain.group.dto.response.GroupJoinResponse;
57
import com.moa.moa_server.domain.group.entity.Group;
68
import com.moa.moa_server.domain.group.entity.GroupMember;
@@ -17,6 +19,7 @@
1719
import com.moa.moa_server.domain.vote.handler.VoteErrorCode;
1820
import com.moa.moa_server.domain.vote.handler.VoteException;
1921
import lombok.RequiredArgsConstructor;
22+
import org.apache.commons.lang3.RandomStringUtils;
2023
import org.springframework.stereotype.Service;
2124
import org.springframework.transaction.annotation.Transactional;
2225

@@ -26,6 +29,8 @@
2629
@RequiredArgsConstructor
2730
public class GroupService {
2831

32+
private static final int MAX_INVITE_CODE_RETRY = 10;
33+
2934
private final GroupRepository groupRepository;
3035
private final UserRepository userRepository;
3136
private final GroupMemberRepository groupMemberRepository;
@@ -78,4 +83,55 @@ public GroupJoinResponse joinGroup(Long userId, GroupJoinRequest request) {
7883

7984
return new GroupJoinResponse(group.getId(), group.getName(), member.getRole().name());
8085
}
86+
87+
@Transactional
88+
public GroupCreateResponse createGroup(Long userId, GroupCreateRequest request) {
89+
// 유저 조회 및 검증
90+
User user = userRepository.findById(userId)
91+
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
92+
AuthUserValidator.validateActive(user);
93+
94+
// 입력 검증
95+
GroupValidator.validateGroupName(request.name());
96+
GroupValidator.validateDescription(request.description());
97+
if (!request.imageUrl().isBlank()) {
98+
GroupValidator.validateImageUrl(request.imageUrl()); // 업로드 도메인 검증
99+
}
100+
String imageUrl = request.imageUrl().isBlank() ? null : request.imageUrl().trim();
101+
102+
// 그룹 이름 중복 검사
103+
if (groupRepository.existsByName(request.name())) {
104+
throw new GroupException(GroupErrorCode.DUPLICATE_NAME);
105+
}
106+
107+
// 초대 코드 생성
108+
String inviteCode = generateUniqueInviteCode();
109+
110+
// 그룹 생성
111+
Group group = Group.create(user, request.name(), request.description(), imageUrl, inviteCode);
112+
groupRepository.save(group);
113+
114+
// 그룹 멤버 등록
115+
GroupMember member = GroupMember.createAsOwner(user, group);
116+
groupMemberRepository.save(member);
117+
118+
return new GroupCreateResponse(
119+
group.getId(),
120+
group.getName(),
121+
group.getDescription(),
122+
group.getImageUrl(),
123+
group.getInviteCode(),
124+
group.getCreatedAt()
125+
);
126+
}
127+
128+
private String generateUniqueInviteCode() {
129+
for (int i = 0; i < MAX_INVITE_CODE_RETRY; i++) {
130+
String code = RandomStringUtils.randomAlphanumeric(6, 8).toUpperCase();
131+
if (!groupRepository.existsByInviteCode(code)) {
132+
return code;
133+
}
134+
}
135+
throw new GroupException(GroupErrorCode.INVITE_CODE_GENERATION_FAILED);
136+
}
81137
}

src/main/java/com/moa/moa_server/domain/group/util/GroupValidator.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
public class GroupValidator {
99

1010
private static final Pattern INVITE_CODE_PATTERN = Pattern.compile("^[A-Z0-9]{6,8}$");
11+
private static final Pattern NAME_PATTERN = Pattern.compile("^(?![\\d\\s])[가-힣a-zA-Z0-9 ]{2,12}$");
12+
private static final String UPLOAD_URL_PREFIX = "https://upload-domain/group";
1113

1214
private GroupValidator() {
1315
throw new AssertionError("유틸 클래스는 인스턴스화할 수 없습니다.");
@@ -18,4 +20,23 @@ public static void validateInviteCode(String inviteCode) {
1820
throw new GroupException(GroupErrorCode.INVALID_CODE_FORMAT);
1921
}
2022
}
23+
24+
public static void validateGroupName(String name) {
25+
if (name == null || name.isBlank() || !NAME_PATTERN.matcher(name).matches()) {
26+
throw new GroupException(GroupErrorCode.INVALID_INPUT);
27+
}
28+
}
29+
30+
31+
public static void validateDescription(String description) {
32+
if (description == null || description.isBlank() || description.length() < 2 || description.length() > 50) {
33+
throw new GroupException(GroupErrorCode.INVALID_INPUT);
34+
}
35+
}
36+
37+
public static void validateImageUrl(String imageUrl) {
38+
if (imageUrl != null && !imageUrl.isBlank() && !imageUrl.startsWith(UPLOAD_URL_PREFIX)) {
39+
throw new GroupException(GroupErrorCode.INVALID_INPUT);
40+
}
41+
}
2142
}

0 commit comments

Comments
 (0)