Skip to content

Commit 13dbe67

Browse files
authored
[Feature] 퀘스트 생성 과정을 리팩토링하고 그룹 퀘스트 생성을 구현한다 (#103)
* feature: 그룹 매니저 검증을 구현한다 * refactor: Quest 도메인의 생성자를 수정하고, 세부 사항 설정 메서드를 추가한다 * refactor: 퀘스트 수락 시 퀘스트가 수행 가능 상태인지 검증한다 * fix: 일반 퀘스트 생성 시 카테고리를 정상화한다 * test: 퀘스트 변경으로 인해 깨진 테스트를 수정한다 * refactor: 퀘스트 수락 시 검증 로직을 구체화 한다 * refactor: 퀘스트 생성 요청에 유효성 검증을 추가한다 * feature: 퀘스트 상세 정보 설정을 구현한다 * refactor: 퀘스트 수행 가능 여부 검증 로직을 Participant 도메인 내로 이동시킨다 * test: 검증 로직 변경 사항을 테스트에 적용한다
1 parent 1701994 commit 13dbe67

23 files changed

+554
-188
lines changed

src/main/java/daybyquest/global/error/ExceptionCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public enum ExceptionCode {
6767
ALREADY_LABELED("QUE-16", BAD_REQUEST, "이미 라벨링된 퀘스트입니다"),
6868
INVALID_QUEST_IMAGE("QUE-17", BAD_REQUEST, "퀘스트 사진이 없습니다"),
6969
ALREADY_EXIST_REWARD("QUE-18", BAD_REQUEST, "이미 보상으로 설정된 뱃지입니다"),
70+
CANNOT_PARTICIPATE("QUE-19", BAD_REQUEST, "수락할 수 없는 퀘스트입니다"),
7071

7172
// Group
7273
NOT_EXIST_GROUP("GRP-00", BAD_REQUEST, "존재하지 않는 그룹입니다"),
@@ -79,6 +80,7 @@ public enum ExceptionCode {
7980
INVALID_GROUP_NAME("GRP-07", BAD_REQUEST, "그룹 이름은 1~15자여야 합니다"),
8081
INVALID_GROUP_DESCRIPTION("GRP-08", BAD_REQUEST, "그룹 설명은 200자 이하여야 합니다"),
8182
NOT_DELETABLE_GROUP_USER("GRP-09", BAD_REQUEST, "매니저는 탈퇴할 수 없습니다"),
83+
NOT_GROUP_MANAGER("GRP-10", BAD_REQUEST, "그룹 매니저만 가능한 권한입니다"),
8284

8385
// Badge
8486
NOT_EXIST_BADGE("BDE-00", BAD_REQUEST, "존재하지 않는 뱃지입니다"),

src/main/java/daybyquest/group/domain/GroupUsers.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package daybyquest.group.domain;
22

33
import static daybyquest.global.error.ExceptionCode.NOT_DELETABLE_GROUP_USER;
4+
import static daybyquest.global.error.ExceptionCode.NOT_GROUP_MANAGER;
45

56
import daybyquest.global.error.exception.InvalidDomainException;
67
import daybyquest.global.error.exception.NotExistGroupUserException;
@@ -15,11 +16,24 @@ public class GroupUsers {
1516
this.groupUserRepository = groupUserRepository;
1617
}
1718

19+
public void validateExistentByUserIdAndGroupId(final Long userId, final Long groupId) {
20+
if (!groupUserRepository.existsByUserIdAndGroupId(userId, groupId)) {
21+
throw new NotExistGroupUserException();
22+
}
23+
}
24+
1825
public GroupUser getByUserIdAndGroupId(final Long userId, final Long groupId) {
1926
return groupUserRepository.findByUserIdAndGroupId(userId, groupId)
2027
.orElseThrow(NotExistGroupUserException::new);
2128
}
2229

30+
public void validateGroupManager(final Long userId, final Long groupId) {
31+
final GroupUser groupUser = getByUserIdAndGroupId(userId, groupId);
32+
if (!groupUser.isManager()) {
33+
throw new InvalidDomainException(NOT_GROUP_MANAGER);
34+
}
35+
}
36+
2337
public void delete(final GroupUser groupUser) {
2438
if (groupUser.isManager()) {
2539
throw new InvalidDomainException(NOT_DELETABLE_GROUP_USER);

src/main/java/daybyquest/participant/domain/Participant.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public Participant(Long userId, Quest quest) {
4545
this.quest = quest;
4646
this.state = DOING;
4747
this.linkedCount = 0L;
48+
quest.validateCanParticipate();
4849
}
4950

5051
public Long getQuestId() {

src/main/java/daybyquest/participant/domain/Participants.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static daybyquest.global.error.ExceptionCode.NOT_ACCEPTED_QUEST;
55

66
import daybyquest.global.error.exception.InvalidDomainException;
7+
import daybyquest.group.domain.GroupUsers;
78
import daybyquest.quest.domain.Quest;
89
import daybyquest.quest.domain.Quests;
910
import daybyquest.user.domain.Users;
@@ -18,21 +19,31 @@ public class Participants {
1819

1920
private final Quests quests;
2021

22+
private final GroupUsers groupUsers;
23+
2124
Participants(final ParticipantRepository participantRepository, final Users users,
22-
final Quests quests) {
25+
final Quests quests, final GroupUsers groupUsers) {
2326
this.participantRepository = participantRepository;
2427
this.users = users;
2528
this.quests = quests;
29+
this.groupUsers = groupUsers;
2630
}
2731

2832
public void saveWithUserIdAndQuestId(final Long userId, final Long questId) {
2933
users.validateExistentById(userId);
3034
final Quest quest = quests.getById(questId);
35+
validateGroupQuest(userId, quest);
3136
final Participant participant = new Participant(userId, quest);
3237
validateNotExistent(participant);
3338
participantRepository.save(participant);
3439
}
3540

41+
private void validateGroupQuest(final Long userId, final Quest quest) {
42+
if (quest.isGroupQuest()) {
43+
groupUsers.validateExistentByUserIdAndGroupId(userId, quest.getGroupId());
44+
}
45+
}
46+
3647
private void validateNotExistent(final Participant participant) {
3748
if (participantRepository.existsByUserIdAndQuestId(participant.getUserId(),
3849
participant.getQuestId())) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package daybyquest.quest.application;
2+
3+
import daybyquest.group.domain.GroupUsers;
4+
import daybyquest.quest.domain.Quest;
5+
import daybyquest.quest.domain.Quests;
6+
import daybyquest.quest.dto.request.SaveGroupQuestDetailRequest;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
10+
@Service
11+
public class SaveGroupQuestDetailService {
12+
13+
private final Quests quests;
14+
15+
private final GroupUsers groupUsers;
16+
17+
public SaveGroupQuestDetailService(final Quests quests, final GroupUsers groupUsers) {
18+
this.quests = quests;
19+
this.groupUsers = groupUsers;
20+
}
21+
22+
@Transactional
23+
public void invoke(final Long loginId, final Long questId, final SaveGroupQuestDetailRequest request) {
24+
final Quest quest = quests.getById(questId);
25+
groupUsers.validateGroupManager(loginId, quest.getGroupId());
26+
quest.setDetail(request.getTitle(), request.getContent(), request.getInterest(),
27+
request.getExpiredAt(), request.getLabel(), null);
28+
}
29+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package daybyquest.quest.application;
2+
3+
import daybyquest.global.utils.MultipartFileUtils;
4+
import daybyquest.group.domain.Group;
5+
import daybyquest.group.domain.GroupUsers;
6+
import daybyquest.group.domain.Groups;
7+
import daybyquest.image.domain.Image;
8+
import daybyquest.image.domain.ImageIdentifierGenerator;
9+
import daybyquest.image.domain.Images;
10+
import daybyquest.quest.domain.Quest;
11+
import daybyquest.quest.domain.Quests;
12+
import daybyquest.quest.dto.request.SaveGroupQuestRequest;
13+
import java.util.List;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
import org.springframework.web.multipart.MultipartFile;
17+
18+
@Service
19+
public class SaveGroupQuestService {
20+
21+
private static final String CATEGORY = "QUEST";
22+
23+
private final Quests quests;
24+
25+
private final Groups groups;
26+
27+
private final GroupUsers groupUsers;
28+
29+
private final Images images;
30+
31+
private final ImageIdentifierGenerator generator;
32+
33+
private final QuestClient questClient;
34+
35+
public SaveGroupQuestService(final Quests quests, final Groups groups, final GroupUsers groupUsers,
36+
final Images images,
37+
final ImageIdentifierGenerator generator, final QuestClient questClient) {
38+
this.quests = quests;
39+
this.groups = groups;
40+
this.groupUsers = groupUsers;
41+
this.images = images;
42+
this.generator = generator;
43+
this.questClient = questClient;
44+
}
45+
46+
@Transactional
47+
public Long invoke(final Long loginId, final SaveGroupQuestRequest request,
48+
final List<MultipartFile> files) {
49+
groupUsers.validateGroupManager(loginId, request.getGroupId());
50+
final Quest quest = toEntity(request, toImageList(files));
51+
final Long questId = quests.save(quest);
52+
questClient.requestLabels(questId, quest.getImages().stream().map(Image::getIdentifier).toList());
53+
return questId;
54+
}
55+
56+
private List<Image> toImageList(final List<MultipartFile> files) {
57+
return files.stream().map((file) -> {
58+
final String identifier = generator.generate(CATEGORY, file.getOriginalFilename());
59+
return images.upload(identifier, MultipartFileUtils.getInputStream(file));
60+
}).toList();
61+
}
62+
63+
private Quest toEntity(final SaveGroupQuestRequest request, final List<Image> images) {
64+
final Group group = groups.getById(request.getGroupId());
65+
return Quest.createGroupQuest(group.getId(), request.getImageDescription(), images,
66+
group.getImage());
67+
}
68+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package daybyquest.quest.application;
2+
3+
import daybyquest.quest.domain.Quest;
4+
import daybyquest.quest.domain.Quests;
5+
import daybyquest.quest.dto.request.SaveQuestDetailRequest;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.transaction.annotation.Transactional;
8+
9+
@Service
10+
public class SaveQuestDetailService {
11+
12+
private final Quests quests;
13+
14+
public SaveQuestDetailService(final Quests quests) {
15+
this.quests = quests;
16+
}
17+
18+
@Transactional
19+
public void invoke(final Long questId, final SaveQuestDetailRequest request) {
20+
final Quest quest = quests.getById(questId);
21+
quest.setDetail(request.getTitle(), request.getContent(), request.getInterest(),
22+
null, request.getLabel(), request.getRewardCount());
23+
}
24+
}

src/main/java/daybyquest/quest/application/SaveQuestService.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ private List<Image> toImageList(final List<MultipartFile> files) {
5555

5656
private Quest toEntity(final SaveQuestRequest request, final List<Image> images) {
5757
final Badge badge = badges.getById(request.getBadgeId());
58-
return Quest.createNormalQuest(badge.getId(), request.getInterest(), request.getTitle(),
59-
request.getContent(), request.getRewardCount(), request.getImageDescription(), images,
58+
return Quest.createNormalQuest(badge.getId(), request.getImageDescription(), images,
6059
badge.getImage());
6160
}
6261
}

src/main/java/daybyquest/quest/domain/Quest.java

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package daybyquest.quest.domain;
22

33
import static daybyquest.global.error.ExceptionCode.ALREADY_LABELED;
4+
import static daybyquest.global.error.ExceptionCode.CANNOT_PARTICIPATE;
45
import static daybyquest.global.error.ExceptionCode.INVALID_QUEST_CONTENT;
56
import static daybyquest.global.error.ExceptionCode.INVALID_QUEST_EXPIRED_AT;
67
import static daybyquest.global.error.ExceptionCode.INVALID_QUEST_IMAGES;
@@ -9,6 +10,8 @@
910
import static daybyquest.global.error.ExceptionCode.INVALID_QUEST_REWARD;
1011
import static daybyquest.global.error.ExceptionCode.INVALID_QUEST_REWARD_COUNT;
1112
import static daybyquest.global.error.ExceptionCode.NOT_EXIST_GROUP;
13+
import static daybyquest.quest.domain.QuestCategory.GROUP;
14+
import static daybyquest.quest.domain.QuestCategory.NORMAL;
1215
import static daybyquest.quest.domain.QuestState.ACTIVE;
1316
import static daybyquest.quest.domain.QuestState.NEED_LABEL;
1417
import static jakarta.persistence.GenerationType.IDENTITY;
@@ -62,7 +65,7 @@ public class Quest {
6265

6366
private String interestName;
6467

65-
@Column(nullable = false, length = MAX_TITLE_LENGTH)
68+
@Column(length = MAX_TITLE_LENGTH)
6669
private String title;
6770

6871
@Column(length = MAX_CONTENT_LENGTH)
@@ -96,38 +99,28 @@ public class Quest {
9699
@Embedded
97100
private Image image;
98101

99-
private Quest(final Long groupId, final Long badgeId, final String interestName, final String title,
100-
final String content, final QuestCategory category, final Long rewardCount,
101-
final LocalDateTime expiredAt, final String imageDescription, final List<Image> images,
102-
final Image image) {
102+
private Quest(final Long groupId, final Long badgeId, final QuestCategory category,
103+
final String imageDescription, final List<Image> images, final Image image) {
103104
this.groupId = groupId;
104105
this.badgeId = badgeId;
105-
this.interestName = interestName;
106-
this.title = title;
107-
this.content = content;
108106
this.category = category;
109-
this.rewardCount = rewardCount;
110-
this.expiredAt = expiredAt;
111107
this.state = NEED_LABEL;
112108
this.imageDescription = imageDescription;
113109
this.images = images;
114110
this.image = image;
115-
validate();
111+
validateImageDescription();
112+
validateImageCount();
116113
}
117114

118-
public static Quest createNormalQuest(final Long badgeId, final String interestName, final String title,
119-
final String content, final Long rewardCount, final String imageDescription,
115+
public static Quest createNormalQuest(final Long badgeId, final String imageDescription,
120116
final List<Image> images, final Image image) {
121-
return new Quest(null, badgeId, interestName, title, content, QuestCategory.NORMAL, rewardCount
122-
, null, imageDescription, images, image);
117+
return new Quest(null, badgeId, NORMAL, imageDescription, images, image);
123118
}
124119

125-
public static Quest createGroupQuest(final Long groupId, final String interestName, final String title,
126-
final String content, final LocalDateTime expiredAt, final String imageDescription,
120+
public static Quest createGroupQuest(final Long groupId, final String imageDescription,
127121
final List<Image> images, final Image image) {
128122
validateGroupId(groupId);
129-
return new Quest(groupId, null, interestName, title, content, QuestCategory.GROUP, null
130-
, expiredAt, imageDescription, images, image);
123+
return new Quest(groupId, null, GROUP, imageDescription, images, image);
131124
}
132125

133126
private static void validateGroupId(final Long groupId) {
@@ -191,11 +184,28 @@ private void validateExpiredAt() {
191184
}
192185
}
193186

194-
public void setLabel(final String label) {
187+
public void setDetail(final String title, final String content, final String interestName,
188+
final LocalDateTime expiredAt, final String label, final Long rewardCount) {
195189
if (this.state != NEED_LABEL) {
196190
throw new InvalidDomainException(ALREADY_LABELED);
197191
}
192+
this.title = title;
193+
this.content = content;
194+
this.interestName = interestName;
195+
this.expiredAt = expiredAt;
198196
this.label = label;
197+
this.rewardCount = rewardCount;
199198
this.state = ACTIVE;
199+
validate();
200+
}
201+
202+
public void validateCanParticipate() {
203+
if (this.state != ACTIVE) {
204+
throw new InvalidDomainException(CANNOT_PARTICIPATE);
205+
}
206+
}
207+
208+
public boolean isGroupQuest() {
209+
return category == GROUP;
200210
}
201211
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package daybyquest.quest.dto.request;
2+
3+
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
5+
import jakarta.validation.constraints.NotBlank;
6+
import java.time.LocalDateTime;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@NoArgsConstructor
11+
@Getter
12+
public class SaveGroupQuestDetailRequest {
13+
14+
@NotBlank
15+
private String title;
16+
17+
private String content;
18+
19+
@NotBlank
20+
private String interest;
21+
22+
@NotBlank
23+
private String label;
24+
25+
@JsonFormat(shape = Shape.STRING, pattern = "yyyy-MM-dd HH:mm")
26+
private LocalDateTime expiredAt;
27+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package daybyquest.quest.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@NoArgsConstructor
8+
@Getter
9+
public class SaveGroupQuestRequest {
10+
11+
@NotBlank
12+
private Long groupId;
13+
14+
@NotBlank
15+
private String imageDescription;
16+
}

0 commit comments

Comments
 (0)