Skip to content

Commit ed5ea28

Browse files
authored
[Feature] 뱃지 설정, 사용자 이름을 통한 뱃지 목록 조회를 구현한다 (#97)
* feature: Owning 도메인에 여러 뱃지 ID 보유 여부 검증을 추가한다 * refactor: 뱃지 리스트가 비어있을 경우 빈 문자열로 변환하도록 리팩토링한다 * feature: ProfileSettings를 작성한다 * refactor: ProfileSetting에 검증을 추가한다 * test: 프로필 설정 도메인 테스트를 작성한다 * refactor: 사용자 생성 시 프로필 설정도 함께 생성한다 * feature: 프로필 뱃지 설정을 구현한다 * refactor: BadgeList 네이밍을 BadgeIds로 변경한다 * feature: 사용자 이름을 통한 미리 설정된 뱃지 목록 조회를 구현한다
1 parent e6592cb commit ed5ea28

24 files changed

+431
-20
lines changed

src/main/java/daybyquest/badge/domain/Owning.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ public Owning(final Long userId, final Badge badge) {
3535
this.userId = userId;
3636
this.badge = badge;
3737
}
38+
39+
public Long getBadgeId() {
40+
return badge.getId();
41+
}
3842
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package daybyquest.badge.domain;
22

3-
import java.util.Optional;
3+
import java.util.Collection;
4+
import java.util.List;
5+
import org.springframework.data.jpa.repository.Query;
46
import org.springframework.data.repository.Repository;
57

68
interface OwningRepository extends Repository<Owning, OwningId> {
79

810
Owning save(final Owning owning);
911

10-
Optional<Owning> findByUserId(final Long userId);
11-
12+
@Query("SELECT o FROM Owning o WHERE o.userId=:userId and o.badge.id IN :badgeIds")
13+
List<Owning> findAllByUserIdAndBadgeIdIn(final Long userId, final Collection<Long> badgeIds);
1214
}

src/main/java/daybyquest/badge/domain/Ownings.java

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

3+
import static daybyquest.global.error.ExceptionCode.NOT_OWNING_BADGE;
4+
5+
import daybyquest.global.error.exception.InvalidDomainException;
36
import daybyquest.user.domain.Users;
7+
import java.util.Collection;
8+
import java.util.List;
49
import org.springframework.stereotype.Component;
510

611
@Component
@@ -26,4 +31,20 @@ public void saveByUserIdAndBadgeId(final Long userId, final Long badgeId) {
2631
private void save(final Owning owning) {
2732
owningRepository.save(owning);
2833
}
34+
35+
public void validateOwningByBadgeIds(final Long userId, final List<Long> badgeIds) {
36+
final List<Owning> ownings = owningRepository.findAllByUserIdAndBadgeIdIn(userId, badgeIds);
37+
if (ownings.size() != badgeIds.size()) {
38+
throw new InvalidDomainException(NOT_OWNING_BADGE);
39+
}
40+
ownings.forEach(
41+
owning -> validateContainOwning(badgeIds, owning)
42+
);
43+
}
44+
45+
private void validateContainOwning(final Collection<Long> badgeIds, final Owning owning) {
46+
if (!badgeIds.contains(owning.getBadgeId())) {
47+
throw new InvalidDomainException(NOT_OWNING_BADGE);
48+
}
49+
}
2950
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package daybyquest.badge.dto.response;
2+
3+
import java.util.List;
4+
5+
public record MultipleBadgesResponse(List<BadgeResponse> badges) {
6+
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package daybyquest.badge.query;
22

33
import daybyquest.global.query.NoOffsetTimePage;
4+
import java.util.Collection;
45
import java.util.List;
56

67
public interface BadgeDao {
78

89
List<BadgeData> getBadgePageByUserIds(final Long userId, final NoOffsetTimePage page);
10+
11+
List<BadgeData> findAllByIdIn(final Collection<Long> ids);
912
}

src/main/java/daybyquest/badge/query/BadgeDaoQuerydslImpl.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import static daybyquest.badge.domain.QBadge.badge;
44
import static daybyquest.badge.domain.QOwning.owning;
55

6+
import com.querydsl.core.types.ConstructorExpression;
67
import com.querydsl.core.types.Projections;
78
import com.querydsl.core.types.dsl.BooleanExpression;
89
import com.querydsl.jpa.impl.JPAQueryFactory;
910
import daybyquest.global.query.NoOffsetTimePage;
1011
import java.time.LocalDateTime;
12+
import java.util.Collection;
1113
import java.util.List;
1214
import org.springframework.stereotype.Repository;
1315

@@ -22,11 +24,7 @@ public BadgeDaoQuerydslImpl(final JPAQueryFactory factory) {
2224

2325
@Override
2426
public List<BadgeData> getBadgePageByUserIds(final Long userId, final NoOffsetTimePage page) {
25-
return factory.select(Projections.constructor(BadgeData.class,
26-
badge.id,
27-
badge.name,
28-
badge.image,
29-
owning.acquiredAt))
27+
return factory.select(projectBadgeData())
3028
.from(owning)
3129
.innerJoin(owning.badge, badge)
3230
.where(owning.userId.eq(userId), ltAcquiredAt(page.lastTime()))
@@ -35,10 +33,27 @@ public List<BadgeData> getBadgePageByUserIds(final Long userId, final NoOffsetTi
3533
.fetch();
3634
}
3735

36+
private static ConstructorExpression<BadgeData> projectBadgeData() {
37+
return Projections.constructor(BadgeData.class,
38+
badge.id,
39+
badge.name,
40+
badge.image,
41+
owning.acquiredAt);
42+
}
43+
3844
private BooleanExpression ltAcquiredAt(final LocalDateTime localDateTime) {
3945
if (localDateTime == null) {
4046
return null;
4147
}
4248
return owning.acquiredAt.lt(localDateTime);
4349
}
50+
51+
@Override
52+
public List<BadgeData> findAllByIdIn(final Collection<Long> ids) {
53+
return factory.select(projectBadgeData())
54+
.from(owning)
55+
.innerJoin(owning.badge, badge)
56+
.where(badge.id.in(ids))
57+
.fetch();
58+
}
4459
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public enum ExceptionCode {
8080
// Badge
8181
NOT_EXIST_BADGE("BDE-00", BAD_REQUEST, "존재하지 않는 뱃지입니다"),
8282
NOT_OWNING_BADGE("BDE-01", BAD_REQUEST, "보유하지 않은 뱃지는 프로필에 배치할 수 없습니다"),
83-
EXCEED_BADGE("BDE-02", BAD_REQUEST, "뱃지는 최대 15개만 지정가능합니다"),
83+
EXCEED_BADGE("BDE-02", BAD_REQUEST, "뱃지는 최대 10개만 지정가능합니다"),
8484
INVALID_BADGE_NAME("BDE-03", BAD_REQUEST, "뱃지 이름은 1~15글자여야 합니다."),
8585

8686
// Interest
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package daybyquest.profile.application;
2+
3+
import daybyquest.badge.dto.response.BadgeResponse;
4+
import daybyquest.badge.dto.response.MultipleBadgesResponse;
5+
import daybyquest.badge.query.BadgeDao;
6+
import daybyquest.profile.domain.ProfileSettings;
7+
import daybyquest.user.domain.Users;
8+
import java.util.List;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
11+
12+
@Service
13+
public class GetPresetBadgeService {
14+
15+
private final Users users;
16+
17+
private final ProfileSettings profileSettings;
18+
19+
private final BadgeDao badgeDao;
20+
21+
public GetPresetBadgeService(final Users users, final ProfileSettings profileSettings,
22+
final BadgeDao badgeDao) {
23+
this.users = users;
24+
this.profileSettings = profileSettings;
25+
this.badgeDao = badgeDao;
26+
}
27+
28+
@Transactional(readOnly = true)
29+
public MultipleBadgesResponse invoke(final Long loginId, final String username) {
30+
final Long userId = users.getUserIdByUsername(username);
31+
final List<Long> badgeIds = profileSettings.getById(loginId).getBadgeIds();
32+
final List<BadgeResponse> responses = badgeDao.findAllByIdIn(badgeIds)
33+
.stream().map(BadgeResponse::of).toList();
34+
return new MultipleBadgesResponse(responses);
35+
}
36+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package daybyquest.profile.application;
2+
3+
import daybyquest.badge.domain.Ownings;
4+
import daybyquest.profile.domain.ProfileSetting;
5+
import daybyquest.profile.domain.ProfileSettings;
6+
import daybyquest.profile.dto.request.SaveBadgeListRequest;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
10+
@Service
11+
public class SaveBadgeListService {
12+
13+
private final ProfileSettings profileSettings;
14+
15+
private final Ownings ownings;
16+
17+
public SaveBadgeListService(final ProfileSettings profileSettings, final Ownings ownings) {
18+
this.profileSettings = profileSettings;
19+
this.ownings = ownings;
20+
}
21+
22+
@Transactional
23+
public void invoke(final Long loginId, final SaveBadgeListRequest request) {
24+
if (request.getBadgeIds() != null) {
25+
ownings.validateOwningByBadgeIds(loginId, request.getBadgeIds());
26+
}
27+
final ProfileSetting profileSetting = profileSettings.getById(loginId);
28+
profileSetting.updateBadgeList(request.getBadgeIds());
29+
}
30+
}

src/main/java/daybyquest/profile/domain/ProfileBadgeListConverter.java renamed to src/main/java/daybyquest/profile/domain/ProfileBadgeIdsConverter.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@
66
import java.util.List;
77

88
@Converter
9-
public class ProfileBadgeListConverter implements AttributeConverter<List<Long>, String> {
9+
public class ProfileBadgeIdsConverter implements AttributeConverter<List<Long>, String> {
1010

1111
@Override
1212
public String convertToDatabaseColumn(List<Long> attribute) {
13-
if (attribute == null) {
14-
return null;
13+
if (attribute == null || attribute.isEmpty()) {
14+
return "";
1515
}
1616
final List<String> attributeStrings = attribute.stream().map(String::valueOf).toList();
1717
return String.join(",", attributeStrings);
1818
}
1919

2020
@Override
2121
public List<Long> convertToEntityAttribute(String dbData) {
22-
if (dbData == null) {
22+
if (dbData == null || dbData.isEmpty()) {
2323
return Collections.emptyList();
2424
}
2525
final List<String> attributeStrings = List.of(dbData.split(","));
Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package daybyquest.profile.domain;
22

3+
import static daybyquest.global.error.ExceptionCode.EXCEED_BADGE;
34
import static lombok.AccessLevel.PROTECTED;
45

6+
import daybyquest.global.error.exception.InvalidDomainException;
57
import jakarta.persistence.Column;
68
import jakarta.persistence.Convert;
7-
import jakarta.persistence.Embeddable;
89
import jakarta.persistence.Entity;
910
import jakarta.persistence.Id;
11+
import java.util.Collections;
1012
import java.util.List;
1113
import lombok.Getter;
1214
import lombok.NoArgsConstructor;
@@ -16,11 +18,32 @@
1618
@NoArgsConstructor(access = PROTECTED)
1719
public class ProfileSetting {
1820

21+
private static final int MAX_BADGE_LIST_SIZE = 10;
22+
1923
@Id
2024
private Long userId;
2125

2226
@Column
23-
@Convert(converter = ProfileBadgeListConverter.class)
24-
private List<Long> badgeList;
27+
@Convert(converter = ProfileBadgeIdsConverter.class)
28+
private List<Long> badgeIds;
29+
30+
public ProfileSetting(final Long userId) {
31+
this.userId = userId;
32+
this.badgeIds = Collections.emptyList();
33+
}
34+
35+
public void updateBadgeList(final List<Long> badgeIds) {
36+
if (badgeIds == null) {
37+
this.badgeIds = Collections.emptyList();
38+
return;
39+
}
40+
this.badgeIds = badgeIds;
41+
validateBadgeList();
42+
}
2543

44+
private void validateBadgeList() {
45+
if (badgeIds.size() > MAX_BADGE_LIST_SIZE) {
46+
throw new InvalidDomainException(EXCEED_BADGE);
47+
}
48+
}
2649
}

src/main/java/daybyquest/profile/domain/ProfileSettingRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import java.util.Optional;
44
import org.springframework.data.repository.Repository;
55

6-
public interface ProfileSettingRepository extends Repository<ProfileSetting, Long> {
6+
interface ProfileSettingRepository extends Repository<ProfileSetting, Long> {
77

88
ProfileSetting save(ProfileSetting profileSetting);
99

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package daybyquest.profile.domain;
2+
3+
import daybyquest.global.error.exception.NotExistUserException;
4+
import org.springframework.stereotype.Component;
5+
6+
@Component
7+
public class ProfileSettings {
8+
9+
private final ProfileSettingRepository profileSettingRepository;
10+
11+
ProfileSettings(final ProfileSettingRepository profileSettingRepository) {
12+
this.profileSettingRepository = profileSettingRepository;
13+
}
14+
15+
public void save(final ProfileSetting profileSetting) {
16+
profileSettingRepository.save(profileSetting);
17+
}
18+
19+
public ProfileSetting getById(final Long userId) {
20+
return profileSettingRepository.findByUserId(userId).orElseThrow(NotExistUserException::new);
21+
}
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package daybyquest.profile.dto.request;
2+
3+
import java.util.List;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
public class SaveBadgeListRequest {
10+
11+
private List<Long> badgeIds;
12+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package daybyquest.profile.listener;
2+
3+
import daybyquest.profile.domain.ProfileSetting;
4+
import daybyquest.profile.domain.ProfileSettings;
5+
import daybyquest.user.domain.UserSavedEvent;
6+
import org.springframework.context.event.EventListener;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class SaveProfileSettingListener {
11+
12+
private final ProfileSettings profileSettings;
13+
14+
public SaveProfileSettingListener(final ProfileSettings profileSettings) {
15+
this.profileSettings = profileSettings;
16+
}
17+
18+
@EventListener
19+
public void listenUserSavedEvent(final UserSavedEvent event) {
20+
final ProfileSetting profileSetting = new ProfileSetting(event.userId());
21+
profileSettings.save(profileSetting);
22+
}
23+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package daybyquest.profile.presentation;
2+
3+
import daybyquest.auth.Authorization;
4+
import daybyquest.auth.domain.AccessUser;
5+
import daybyquest.badge.dto.response.MultipleBadgesResponse;
6+
import daybyquest.profile.application.GetPresetBadgeService;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
@RestController
13+
public class ProfileQueryApi {
14+
15+
private final GetPresetBadgeService getPresetBadgeService;
16+
17+
public ProfileQueryApi(final GetPresetBadgeService getPresetBadgeService) {
18+
this.getPresetBadgeService = getPresetBadgeService;
19+
}
20+
21+
@GetMapping("/badge/{username}")
22+
@Authorization
23+
public ResponseEntity<MultipleBadgesResponse> getBadges(final AccessUser user,
24+
@PathVariable final String username) {
25+
final MultipleBadgesResponse response = getPresetBadgeService.invoke(user.getId(), username);
26+
return ResponseEntity.ok(response);
27+
}
28+
}

0 commit comments

Comments
 (0)