Skip to content

Commit 583ee60

Browse files
authored
[FEAT] 모임 생성 기능 구현
[FEAT] 모임 생성 기능 구현
2 parents 3b6baef + 51e5fc5 commit 583ee60

25 files changed

+800
-24
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package team.wego.wegobackend.group.application.dto.request;
2+
3+
import jakarta.validation.constraints.Future;
4+
import jakarta.validation.constraints.FutureOrPresent;
5+
import jakarta.validation.constraints.Max;
6+
import jakarta.validation.constraints.Min;
7+
import jakarta.validation.constraints.NotBlank;
8+
import jakarta.validation.constraints.NotNull;
9+
import java.time.LocalDateTime;
10+
import java.util.List;
11+
12+
public record CreateGroupRequest(
13+
14+
@NotBlank(message = "모임: 제목은 필수 입니다.")
15+
String title,
16+
17+
@NotBlank(message = "모임: 모임 위치는 필수 입니다.")
18+
String location,
19+
20+
String locationDetail,
21+
22+
@NotNull(message = "모임: 시작 시간은 필수 입니다.")
23+
@FutureOrPresent(message = "모임: 시작 시간은 현재 이후여야 합니다.")
24+
LocalDateTime startTime,
25+
26+
@NotNull(message = "모임: 종료 시간은 필수 입니다.")
27+
@Future(message = "모임: 종료 시간은 현재 이후여야 합니다.")
28+
LocalDateTime endTime,
29+
30+
List<String> tags,
31+
32+
@NotBlank(message = "모임: 설명은 필수 입니다.")
33+
String description,
34+
35+
@NotNull(message = "모임: 최대 인원은 필수 입니다.")
36+
@Min(value = 2, message = "모임: 최대 인원은 최소 2명 이상이어야 합니다.")
37+
@Max(value = 12, message = "모임: 최대 인원은 최대 12명 이하이어야 합니다.")
38+
Integer maxParticipants
39+
) {
40+
41+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package team.wego.wegobackend.group.application.dto.response;
2+
3+
4+
import java.util.List;
5+
6+
public record CreateGroupImageResponse(
7+
Long groupId,
8+
List<GroupImageItemResponse> images
9+
) {
10+
}
11+
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package team.wego.wegobackend.group.application.dto.response;
2+
3+
import java.time.LocalDateTime;
4+
import java.util.List;
5+
import lombok.AccessLevel;
6+
import lombok.Builder;
7+
import team.wego.wegobackend.group.domain.entity.Group;
8+
import team.wego.wegobackend.group.domain.entity.GroupTag;
9+
import team.wego.wegobackend.group.domain.entity.GroupUserStatus;
10+
import team.wego.wegobackend.tag.domain.entity.Tag;
11+
import team.wego.wegobackend.user.domain.User;
12+
13+
@Builder(access = AccessLevel.PRIVATE)
14+
public record CreateGroupResponse(
15+
Long id,
16+
String title,
17+
String location,
18+
String locationDetail,
19+
LocalDateTime startTime,
20+
LocalDateTime endTime,
21+
List<String> tags,
22+
String description,
23+
long participantCount,
24+
int maxParticipants,
25+
CreatedBy createdBy,
26+
LocalDateTime createdAt,
27+
LocalDateTime updatedAt,
28+
int joinedCount
29+
) {
30+
31+
public static CreateGroupResponse from(Group group) {
32+
// 태그 이름 리스트
33+
List<String> tagNames = group.getGroupTags().stream()
34+
.map(GroupTag::getTag)
35+
.map(Tag::getName)
36+
.toList();
37+
38+
// 현재 참여 인원
39+
long attendUserCount = group.getUsers().stream()
40+
.filter(groupUser -> GroupUserStatus.ATTEND.equals(groupUser.getStatus()))
41+
.count();
42+
43+
// 모임 생성자 정보
44+
User host = group.getHost();
45+
46+
CreatedBy createdByHost = new CreatedBy(
47+
host.getId(),
48+
host.getNickName(),
49+
host.getProfileImage());
50+
51+
return CreateGroupResponse.builder()
52+
.id(group.getId())
53+
.title(group.getTitle())
54+
.location(group.getLocation())
55+
.locationDetail(group.getLocationDetail())
56+
.startTime(group.getStartTime())
57+
.endTime(group.getEndTime())
58+
.tags(tagNames)
59+
.description(group.getDescription())
60+
.participantCount(attendUserCount)
61+
.maxParticipants(group.getMaxParticipants())
62+
.createdBy(createdByHost)
63+
.createdAt(group.getCreatedAt())
64+
.updatedAt(group.getUpdatedAt())
65+
.joinedCount(1) // TODO: 다시 한 번 체크
66+
.build();
67+
68+
}
69+
70+
71+
public record CreatedBy(
72+
Long userId,
73+
String nickName,
74+
String profileImage
75+
) {
76+
77+
}
78+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package team.wego.wegobackend.group.application.dto.response;
2+
3+
import team.wego.wegobackend.group.domain.entity.GroupImage;
4+
5+
public record GroupImageItemResponse(
6+
Long id,
7+
int sortOrder,
8+
String imageUrl440x240,
9+
String imageUrl100x100
10+
) {
11+
12+
public static GroupImageItemResponse from(
13+
GroupImage entity,
14+
String imageUrl440x240,
15+
String imageUrl100x100
16+
) {
17+
return new GroupImageItemResponse(
18+
entity.getId(),
19+
entity.getSortOrder(),
20+
imageUrl440x240,
21+
imageUrl100x100
22+
);
23+
}
24+
}
25+
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package team.wego.wegobackend.group.application.service;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
8+
import org.springframework.web.multipart.MultipartFile;
9+
import team.wego.wegobackend.group.application.dto.response.CreateGroupImageResponse;
10+
import team.wego.wegobackend.group.application.dto.response.GroupImageItemResponse;
11+
import team.wego.wegobackend.group.domain.entity.Group;
12+
import team.wego.wegobackend.group.domain.entity.GroupImage;
13+
import team.wego.wegobackend.group.domain.exception.GroupErrorCode;
14+
import team.wego.wegobackend.group.domain.exception.GroupException;
15+
import team.wego.wegobackend.group.domain.repository.GroupImageRepository;
16+
import team.wego.wegobackend.group.domain.repository.GroupRepository;
17+
import team.wego.wegobackend.image.application.service.ImageUploadService;
18+
import team.wego.wegobackend.image.domain.ImageFile;
19+
import team.wego.wegobackend.user.domain.User;
20+
import team.wego.wegobackend.user.repository.UserRepository;
21+
22+
@RequiredArgsConstructor
23+
@Service
24+
public class GroupImageService {
25+
26+
// 인덱스 0 → 440x240, 인덱스 1 → 100x100
27+
private static final List<Integer> GROUP_WIDTHS = List.of(440, 100);
28+
private static final List<Integer> GROUP_HEIGHTS = List.of(240, 100);
29+
30+
private final GroupImageRepository groupImageRepository;
31+
private final UserRepository userRepository;
32+
private final GroupRepository groupRepository;
33+
private final ImageUploadService imageUploadService;
34+
35+
private void validateCreateGroupImageRequest(List<MultipartFile> images) {
36+
if (images == null || images.isEmpty()) {
37+
return; // TODO: 이미지가 필수가 아니라면 null/빈 리스트는 그대로 허용 -> 정책 확인 필요
38+
}
39+
40+
if (images.size() > 3) {
41+
throw new GroupException(GroupErrorCode.IMAGE_UPLOAD_EXCEED, images.size());
42+
}
43+
}
44+
45+
/**
46+
* 모임 이미지 저장: S3 업로드 + GroupImage 저장 + 응답 DTO 생성
47+
*/
48+
private List<GroupImageItemResponse> saveGroupImages(Group group, List<MultipartFile> images) {
49+
if (images == null || images.isEmpty()) {
50+
return List.of();
51+
}
52+
53+
List<GroupImage> entities = new ArrayList<>();
54+
List<ImageFile> mainFiles = new ArrayList<>(); // 440x240
55+
List<ImageFile> thumbFiles = new ArrayList<>(); // 100x100
56+
List<String> uploadedKeys = new ArrayList<>();
57+
58+
String dir = "groups/" + group.getId();
59+
60+
try {
61+
for (int i = 0; i < images.size(); i++) {
62+
MultipartFile file = images.get(i);
63+
if (file == null || file.isEmpty()) {
64+
continue;
65+
}
66+
67+
// 한 장당 2개(440x240, 100x100) 업로드
68+
List<ImageFile> variants = imageUploadService.uploadAsWebpWithSizes(
69+
dir,
70+
file,
71+
i,
72+
GROUP_WIDTHS,
73+
GROUP_HEIGHTS
74+
);
75+
76+
ImageFile main = variants.get(0); // 440x240
77+
ImageFile thumb = variants.get(1); // 100x100
78+
79+
// DB에는 대표(440x240) 기준 URL만 저장
80+
GroupImage image = GroupImage.create(group, main.url(), i);
81+
entities.add(image);
82+
mainFiles.add(main);
83+
thumbFiles.add(thumb);
84+
85+
// 보상 트랜잭션용
86+
uploadedKeys.add(main.key());
87+
uploadedKeys.add(thumb.key());
88+
}
89+
90+
if (!entities.isEmpty()) {
91+
groupImageRepository.saveAll(entities); // 여기서 IDENTITY ID 세팅
92+
}
93+
94+
// 엔티티 + 두 사이즈 URL을 묶어서 응답 DTO로 변환
95+
List<GroupImageItemResponse> responses = new ArrayList<>();
96+
for (int i = 0; i < entities.size(); i++) {
97+
98+
GroupImage entity = entities.get(i);
99+
ImageFile main = mainFiles.get(i);
100+
ImageFile thumb = thumbFiles.get(i);
101+
102+
responses.add(
103+
GroupImageItemResponse.from(
104+
entity,
105+
main.url(), // 440x240
106+
thumb.url() // 100x100
107+
)
108+
);
109+
}
110+
111+
return responses;
112+
} catch (RuntimeException e) {
113+
imageUploadService.deleteAll(uploadedKeys);
114+
throw new GroupException(GroupErrorCode.IMAGE_UPLOAD_FAILED, e);
115+
}
116+
}
117+
118+
@Transactional
119+
public CreateGroupImageResponse createGroupImage(
120+
Long userId,
121+
Long groupId,
122+
List<MultipartFile> images
123+
) {
124+
// 1. 회원 조회(HOST)
125+
User host = userRepository.findById(userId)
126+
.orElseThrow(() -> new GroupException(GroupErrorCode.HOST_USER_NOT_FOUND, userId));
127+
128+
// 2. 모임 조회
129+
Group group = groupRepository.findById(groupId)
130+
.orElseThrow(
131+
() -> new GroupException(GroupErrorCode.GROUP_NOT_FOUND_BY_ID, groupId));
132+
133+
// 3. 업로드하는 사람이 host인지 검증
134+
if (!group.getHost().getId().equals(host.getId())) {
135+
throw new GroupException(GroupErrorCode.HOST_USER_NOT_FOUND, userId);
136+
}
137+
138+
// 4. 비즈니스 유효성 검사
139+
validateCreateGroupImageRequest(images);
140+
141+
// 5. 업로드 + DB 저장 + 응답 DTO 생성
142+
List<GroupImageItemResponse> imageItems = saveGroupImages(group, images);
143+
144+
return new CreateGroupImageResponse(
145+
group.getId(),
146+
imageItems
147+
);
148+
}
149+
}
150+

0 commit comments

Comments
 (0)