diff --git a/src/main/java/com/stempo/api/domain/application/event/AchievementDeletedEvent.java b/src/main/java/com/stempo/api/domain/application/event/AchievementDeletedEvent.java new file mode 100644 index 00000000..415b44b2 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/event/AchievementDeletedEvent.java @@ -0,0 +1,15 @@ +package com.stempo.api.domain.application.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class AchievementDeletedEvent extends ApplicationEvent { + + private final Long achievementId; + + public AchievementDeletedEvent(Object source, Long achievementId) { + super(source); + this.achievementId = achievementId; + } +} diff --git a/src/main/java/com/stempo/api/domain/application/event/AchievementEventDispatcher.java b/src/main/java/com/stempo/api/domain/application/event/AchievementEventDispatcher.java new file mode 100644 index 00000000..941bd45b --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/event/AchievementEventDispatcher.java @@ -0,0 +1,24 @@ +package com.stempo.api.domain.application.event; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class AchievementEventDispatcher { + + private final List processors; + + @EventListener + public void handleAchievementDeletedEvent(AchievementDeletedEvent event) { + processors.forEach(processor -> processor.processAchievementDeleted(event.getAchievementId())); + } + + @EventListener + public void handleAchievementUpdatedEvent(AchievementUpdatedEvent event) { + processors.forEach(processor -> processor.processAchievementUpdated(event.getAchievementId())); + } +} diff --git a/src/main/java/com/stempo/api/domain/application/event/AchievementEventProcessor.java b/src/main/java/com/stempo/api/domain/application/event/AchievementEventProcessor.java new file mode 100644 index 00000000..487e6594 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/event/AchievementEventProcessor.java @@ -0,0 +1,8 @@ +package com.stempo.api.domain.application.event; + +public interface AchievementEventProcessor { + + void processAchievementDeleted(Long achievementId); + + void processAchievementUpdated(Long achievementId); +} diff --git a/src/main/java/com/stempo/api/domain/application/event/AchievementEventProcessorRegistry.java b/src/main/java/com/stempo/api/domain/application/event/AchievementEventProcessorRegistry.java new file mode 100644 index 00000000..eda0b45d --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/event/AchievementEventProcessorRegistry.java @@ -0,0 +1,21 @@ +package com.stempo.api.domain.application.event; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Component +public class AchievementEventProcessorRegistry { + + private final List processors = new ArrayList<>(); + + public void register(AchievementEventProcessor processor) { + processors.add(processor); + } + + public List getProcessors() { + return Collections.unmodifiableList(processors); + } +} diff --git a/src/main/java/com/stempo/api/domain/application/event/AchievementUpdatedEvent.java b/src/main/java/com/stempo/api/domain/application/event/AchievementUpdatedEvent.java new file mode 100644 index 00000000..81da03ef --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/event/AchievementUpdatedEvent.java @@ -0,0 +1,15 @@ +package com.stempo.api.domain.application.event; + +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class AchievementUpdatedEvent extends ApplicationEvent { + + private final Long achievementId; + + public AchievementUpdatedEvent(Object source, Long achievementId) { + super(source); + this.achievementId = achievementId; + } +} diff --git a/src/main/java/com/stempo/api/domain/application/event/UserAchievementEventProcessor.java b/src/main/java/com/stempo/api/domain/application/event/UserAchievementEventProcessor.java new file mode 100644 index 00000000..fa93b307 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/event/UserAchievementEventProcessor.java @@ -0,0 +1,29 @@ +package com.stempo.api.domain.application.event; + +import com.stempo.api.domain.domain.model.UserAchievement; +import com.stempo.api.domain.domain.repository.UserAchievementRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class UserAchievementEventProcessor implements AchievementEventProcessor { + + private final UserAchievementRepository userAchievementRepository; + + @Override + @Transactional + public void processAchievementDeleted(Long achievementId) { + List achievements = userAchievementRepository.findByAchievementId(achievementId); + achievements.forEach(UserAchievement::delete); + userAchievementRepository.saveAll(achievements); + } + + @Override + public void processAchievementUpdated(Long achievementId) { + // do nothing + } +} diff --git a/src/main/java/com/stempo/api/domain/application/service/AchievementService.java b/src/main/java/com/stempo/api/domain/application/service/AchievementService.java new file mode 100644 index 00000000..7cd984b7 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/AchievementService.java @@ -0,0 +1,18 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.presentation.dto.request.AchievementRequestDto; +import com.stempo.api.domain.presentation.dto.request.AchievementUpdateRequestDto; +import com.stempo.api.domain.presentation.dto.response.AchievementResponseDto; + +import java.util.List; + +public interface AchievementService { + + Long registerAchievement(AchievementRequestDto requestDto); + + List getAchievements(); + + Long updateAchievement(Long achievementId, AchievementUpdateRequestDto requestDto); + + Long deleteAchievement(Long achievementId); +} diff --git a/src/main/java/com/stempo/api/domain/application/service/AchievementServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/AchievementServiceImpl.java new file mode 100644 index 00000000..209c4f28 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/AchievementServiceImpl.java @@ -0,0 +1,54 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.application.event.AchievementDeletedEvent; +import com.stempo.api.domain.application.event.AchievementUpdatedEvent; +import com.stempo.api.domain.domain.model.Achievement; +import com.stempo.api.domain.domain.repository.AchievementRepository; +import com.stempo.api.domain.presentation.dto.request.AchievementRequestDto; +import com.stempo.api.domain.presentation.dto.request.AchievementUpdateRequestDto; +import com.stempo.api.domain.presentation.dto.response.AchievementResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class AchievementServiceImpl implements AchievementService { + + private final AchievementRepository repository; + private final ApplicationEventPublisher eventPublisher; + + @Override + public Long registerAchievement(AchievementRequestDto requestDto) { + Achievement achievement = AchievementRequestDto.toDomain(requestDto); + return repository.save(achievement).getId(); + } + + @Override + public List getAchievements() { + List achievements = repository.findAll(); + return achievements.stream() + .map(AchievementResponseDto::toDto) + .toList(); + } + + @Override + public Long updateAchievement(Long achievementId, AchievementUpdateRequestDto requestDto) { + Achievement achievement = repository.findByIdOrThrow(achievementId); + achievement.update(requestDto); + repository.save(achievement); + eventPublisher.publishEvent(new AchievementUpdatedEvent(this, achievement.getId())); + return achievement.getId(); + } + + @Override + public Long deleteAchievement(Long achievementId) { + Achievement achievement = repository.findByIdOrThrow(achievementId); + achievement.delete(); + repository.save(achievement); + eventPublisher.publishEvent(new AchievementDeletedEvent(this, achievement.getId())); + return achievement.getId(); + } +} diff --git a/src/main/java/com/stempo/api/domain/application/service/UserAchievementService.java b/src/main/java/com/stempo/api/domain/application/service/UserAchievementService.java new file mode 100644 index 00000000..ed196846 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/UserAchievementService.java @@ -0,0 +1,12 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.presentation.dto.response.UserAchievementResponseDto; + +import java.util.List; + +public interface UserAchievementService { + + Long registerUserAchievement(Long achievementId); + + List getUserAchievements(); +} diff --git a/src/main/java/com/stempo/api/domain/application/service/UserAchievementServiceImpl.java b/src/main/java/com/stempo/api/domain/application/service/UserAchievementServiceImpl.java new file mode 100644 index 00000000..6e9670ac --- /dev/null +++ b/src/main/java/com/stempo/api/domain/application/service/UserAchievementServiceImpl.java @@ -0,0 +1,49 @@ +package com.stempo.api.domain.application.service; + +import com.stempo.api.domain.domain.model.Achievement; +import com.stempo.api.domain.domain.model.UserAchievement; +import com.stempo.api.domain.domain.repository.AchievementRepository; +import com.stempo.api.domain.domain.repository.UserAchievementRepository; +import com.stempo.api.domain.persistence.entity.UserAchievementEntity; +import com.stempo.api.domain.presentation.dto.response.UserAchievementResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class UserAchievementServiceImpl implements UserAchievementService { + + private final UserService userService; + private final UserAchievementRepository userAchievementRepository; + private final AchievementRepository achievementRepository; + + @Override + public Long registerUserAchievement(Long achievementId) { + String deviceTag = userService.getCurrentDeviceTag(); + Optional existingUserAchievement = userAchievementRepository.findByDeviceTagAndAchievementId(deviceTag, achievementId); + + if (existingUserAchievement.isPresent()) { + return existingUserAchievement.get().getId(); + } + + UserAchievement userAchievement = UserAchievement.create(achievementId, deviceTag); + return userAchievementRepository.save(userAchievement).getId(); + } + + @Override + public List getUserAchievements() { + String deviceTag = userService.getCurrentDeviceTag(); + List userAchievements = userAchievementRepository.findByDeviceTag(deviceTag); + return userAchievements.stream() + .map(this::getUserAchievementResponseDto) + .toList(); + } + + private UserAchievementResponseDto getUserAchievementResponseDto(UserAchievementEntity ua) { + Achievement achievement = achievementRepository.findByIdOrThrow(ua.getAchievementId()); + return UserAchievementResponseDto.toDto(achievement); + } +} diff --git a/src/main/java/com/stempo/api/domain/domain/model/Achievement.java b/src/main/java/com/stempo/api/domain/domain/model/Achievement.java new file mode 100644 index 00000000..6d1e8c34 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/model/Achievement.java @@ -0,0 +1,35 @@ +package com.stempo.api.domain.domain.model; + +import com.stempo.api.domain.presentation.dto.request.AchievementUpdateRequestDto; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.Optional; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Achievement { + + private Long id; + private String name; + private String description; + private String imageUrl; + private boolean deleted; + + public void update(AchievementUpdateRequestDto requestDto) { + Optional.ofNullable(requestDto.getName()).ifPresent(this::setName); + Optional.ofNullable(requestDto.getDescription()).ifPresent(this::setDescription); + Optional.ofNullable(requestDto.getImageUrl()).ifPresent(this::setImageUrl); + } + + public void delete() { + setDeleted(true); + } +} diff --git a/src/main/java/com/stempo/api/domain/domain/model/UserAchievement.java b/src/main/java/com/stempo/api/domain/domain/model/UserAchievement.java new file mode 100644 index 00000000..8e806e56 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/model/UserAchievement.java @@ -0,0 +1,35 @@ +package com.stempo.api.domain.domain.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UserAchievement { + + private Long id; + private String deviceTag; + private Long achievementId; + private LocalDateTime createdAt; + private boolean deleted; + + public static UserAchievement create(Long achievementId, String deviceTag) { + return UserAchievement.builder() + .achievementId(achievementId) + .deviceTag(deviceTag) + .build(); + } + + public void delete() { + setDeleted(true); + } +} diff --git a/src/main/java/com/stempo/api/domain/domain/repository/AchievementRepository.java b/src/main/java/com/stempo/api/domain/domain/repository/AchievementRepository.java new file mode 100644 index 00000000..4717deb6 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/repository/AchievementRepository.java @@ -0,0 +1,14 @@ +package com.stempo.api.domain.domain.repository; + +import com.stempo.api.domain.domain.model.Achievement; + +import java.util.List; + +public interface AchievementRepository { + + Achievement findByIdOrThrow(Long achievementId); + + List findAll(); + + Achievement save(Achievement achievement); +} diff --git a/src/main/java/com/stempo/api/domain/domain/repository/UserAchievementRepository.java b/src/main/java/com/stempo/api/domain/domain/repository/UserAchievementRepository.java new file mode 100644 index 00000000..cdc8b41b --- /dev/null +++ b/src/main/java/com/stempo/api/domain/domain/repository/UserAchievementRepository.java @@ -0,0 +1,20 @@ +package com.stempo.api.domain.domain.repository; + +import com.stempo.api.domain.domain.model.UserAchievement; +import com.stempo.api.domain.persistence.entity.UserAchievementEntity; + +import java.util.List; +import java.util.Optional; + +public interface UserAchievementRepository { + + UserAchievement save(UserAchievement userAchievement); + + void saveAll(List achievements); + + List findByDeviceTag(String deviceTag); + + Optional findByDeviceTagAndAchievementId(String deviceTag, Long achievementId); + + List findByAchievementId(Long achievementId); +} diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/AchievementEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/AchievementEntity.java index d6faf21e..40a135c4 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/AchievementEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/AchievementEntity.java @@ -36,4 +36,7 @@ public class AchievementEntity extends BaseEntity { @Column(nullable = false) private String imageUrl; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/ArticleEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/ArticleEntity.java index 29edb343..53092855 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/ArticleEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/ArticleEntity.java @@ -40,4 +40,7 @@ public class ArticleEntity extends BaseEntity { @Column(nullable = false) @URL(message = "Invalid URL") private String articleUrl; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/BaseEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/BaseEntity.java index 886579ec..acd6b404 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/BaseEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/BaseEntity.java @@ -24,9 +24,6 @@ public class BaseEntity { @LastModifiedDate private LocalDateTime updatedAt; - @Column(nullable = false) - private boolean deleted = false; - @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/BoardEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/BoardEntity.java index 707d6810..04a623f9 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/BoardEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/BoardEntity.java @@ -44,4 +44,7 @@ public class BoardEntity extends BaseEntity { @Column(nullable = false) @Size(min = 1, max = 10000, message = "Content must be between 1 and 10000 characters") private String content; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/RecordEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/RecordEntity.java index 2c0bb473..8cb2bd31 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/RecordEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/RecordEntity.java @@ -34,4 +34,7 @@ public class RecordEntity extends BaseEntity { private Integer duration; private Integer steps; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/UploadedFileEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/UploadedFileEntity.java index a3058603..0cabbf5a 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/UploadedFileEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/UploadedFileEntity.java @@ -40,4 +40,7 @@ public class UploadedFileEntity extends BaseEntity { @Column(nullable = false) private Long fileSize; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/UserAchievementEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/UserAchievementEntity.java new file mode 100644 index 00000000..55a98682 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/entity/UserAchievementEntity.java @@ -0,0 +1,41 @@ +package com.stempo.api.domain.persistence.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Table(name = "user_achievement", indexes = { + @Index(name = "idx_user_achievement_device_tag", columnList = "deviceTag"), + @Index(name = "idx_user_achievement_achievement_id", columnList = "achievementId") +}) +public class UserAchievementEntity extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String deviceTag; + + @Column(nullable = false) + private Long achievementId; + + @Column(nullable = false) + private boolean deleted = false; +} diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java index e9d7dbf2..c4bd8cb9 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/UserEntity.java @@ -31,4 +31,7 @@ public class UserEntity extends BaseEntity { @Enumerated(EnumType.STRING) private Role role; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/entity/VideoEntity.java b/src/main/java/com/stempo/api/domain/persistence/entity/VideoEntity.java index 89b2cf21..346e84f8 100644 --- a/src/main/java/com/stempo/api/domain/persistence/entity/VideoEntity.java +++ b/src/main/java/com/stempo/api/domain/persistence/entity/VideoEntity.java @@ -40,4 +40,7 @@ public class VideoEntity extends BaseEntity { @Column(nullable = false) @URL(message = "Invalid URL") private String videoUrl; + + @Column(nullable = false) + private boolean deleted = false; } diff --git a/src/main/java/com/stempo/api/domain/persistence/mappper/AchievementMapper.java b/src/main/java/com/stempo/api/domain/persistence/mappper/AchievementMapper.java new file mode 100644 index 00000000..ef2e2af2 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/mappper/AchievementMapper.java @@ -0,0 +1,29 @@ +package com.stempo.api.domain.persistence.mappper; + +import com.stempo.api.domain.domain.model.Achievement; +import com.stempo.api.domain.persistence.entity.AchievementEntity; +import org.springframework.stereotype.Component; + +@Component +public class AchievementMapper { + + public AchievementEntity toEntity(Achievement achievement) { + return AchievementEntity.builder() + .id(achievement.getId()) + .name(achievement.getName()) + .description(achievement.getDescription()) + .imageUrl(achievement.getImageUrl()) + .deleted(achievement.isDeleted()) + .build(); + } + + public Achievement toDomain(AchievementEntity entity) { + return Achievement.builder() + .id(entity.getId()) + .name(entity.getName()) + .description(entity.getDescription()) + .imageUrl(entity.getImageUrl()) + .deleted(entity.isDeleted()) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/domain/persistence/mappper/RecordMapper.java b/src/main/java/com/stempo/api/domain/persistence/mappper/RecordMapper.java index f3f39baa..9efa1c7e 100644 --- a/src/main/java/com/stempo/api/domain/persistence/mappper/RecordMapper.java +++ b/src/main/java/com/stempo/api/domain/persistence/mappper/RecordMapper.java @@ -17,14 +17,14 @@ public RecordEntity toEntity(Record record) { .build(); } - public Record toDomain(RecordEntity recordEntity) { + public Record toDomain(RecordEntity entity) { return Record.builder() - .id(recordEntity.getId()) - .deviceTag(recordEntity.getDeviceTag()) - .accuracy(recordEntity.getAccuracy()) - .duration(recordEntity.getDuration()) - .steps(recordEntity.getSteps()) - .createdAt(recordEntity.getCreatedAt()) + .id(entity.getId()) + .deviceTag(entity.getDeviceTag()) + .accuracy(entity.getAccuracy()) + .duration(entity.getDuration()) + .steps(entity.getSteps()) + .createdAt(entity.getCreatedAt()) .build(); } } diff --git a/src/main/java/com/stempo/api/domain/persistence/mappper/UserAchievementMapper.java b/src/main/java/com/stempo/api/domain/persistence/mappper/UserAchievementMapper.java new file mode 100644 index 00000000..1087bd99 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/mappper/UserAchievementMapper.java @@ -0,0 +1,28 @@ +package com.stempo.api.domain.persistence.mappper; + +import com.stempo.api.domain.domain.model.UserAchievement; +import com.stempo.api.domain.persistence.entity.UserAchievementEntity; +import org.springframework.stereotype.Component; + +@Component +public class UserAchievementMapper { + + public UserAchievementEntity toEntity(UserAchievement userAchievement) { + return UserAchievementEntity.builder() + .id(userAchievement.getId()) + .deviceTag(userAchievement.getDeviceTag()) + .achievementId(userAchievement.getAchievementId()) + .deleted(userAchievement.isDeleted()) + .build(); + } + + public UserAchievement toDomain(UserAchievementEntity userAchievementEntity) { + return UserAchievement.builder() + .id(userAchievementEntity.getId()) + .deviceTag(userAchievementEntity.getDeviceTag()) + .achievementId(userAchievementEntity.getAchievementId()) + .createdAt(userAchievementEntity.getCreatedAt()) + .deleted(userAchievementEntity.isDeleted()) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/AchievementJpaRepository.java b/src/main/java/com/stempo/api/domain/persistence/repository/AchievementJpaRepository.java new file mode 100644 index 00000000..3a2f7efa --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/AchievementJpaRepository.java @@ -0,0 +1,23 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.persistence.entity.AchievementEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface AchievementJpaRepository extends JpaRepository { + + @Query("SELECT a " + + "FROM AchievementEntity a " + + "WHERE a.deleted = false " + + "ORDER BY a.createdAt ASC") + List findAllActiveAchievements(); + + @Query("SELECT a " + + "FROM AchievementEntity a " + + "WHERE a.id = :achievementId AND a.deleted = false") + Optional findByIdAndNotDeleted(@Param("achievementId") Long achievementId); +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/AchievementRepositoryImpl.java b/src/main/java/com/stempo/api/domain/persistence/repository/AchievementRepositoryImpl.java new file mode 100644 index 00000000..69c08f78 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/AchievementRepositoryImpl.java @@ -0,0 +1,42 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.domain.model.Achievement; +import com.stempo.api.domain.domain.repository.AchievementRepository; +import com.stempo.api.domain.persistence.entity.AchievementEntity; +import com.stempo.api.domain.persistence.mappper.AchievementMapper; +import com.stempo.api.global.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class AchievementRepositoryImpl implements AchievementRepository { + + private final AchievementJpaRepository repository; + private final AchievementMapper mapper; + + + @Override + public Achievement findByIdOrThrow(Long achievementId) { + AchievementEntity entity = repository.findByIdAndNotDeleted(achievementId) + .orElseThrow(() -> new NotFoundException("[Achievement] id: " + achievementId + " not found")); + return mapper.toDomain(entity); + } + + @Override + public List findAll() { + List entities = repository.findAllActiveAchievements(); + return entities.stream() + .map(mapper::toDomain) + .toList(); + } + + @Override + public Achievement save(Achievement achievement) { + AchievementEntity jpaEntity = mapper.toEntity(achievement); + AchievementEntity savedEntity = repository.save(jpaEntity); + return mapper.toDomain(savedEntity); + } +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/UserAchievementJpaRepository.java b/src/main/java/com/stempo/api/domain/persistence/repository/UserAchievementJpaRepository.java new file mode 100644 index 00000000..c1fc1c6d --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/UserAchievementJpaRepository.java @@ -0,0 +1,21 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.persistence.entity.UserAchievementEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface UserAchievementJpaRepository extends JpaRepository { + + @Query("SELECT ua " + + "FROM UserAchievementEntity ua " + + "WHERE ua.deviceTag = :deviceTag AND ua.deleted = false " + + "ORDER BY ua.createdAt DESC") + List findByDeviceTag(String deviceTag); + + Optional findByDeviceTagAndAchievementId(String deviceTag, Long achievementId); + + List findByAchievementId(Long achievementId); +} diff --git a/src/main/java/com/stempo/api/domain/persistence/repository/UserAchievementRepositoryImpl.java b/src/main/java/com/stempo/api/domain/persistence/repository/UserAchievementRepositoryImpl.java new file mode 100644 index 00000000..715393ac --- /dev/null +++ b/src/main/java/com/stempo/api/domain/persistence/repository/UserAchievementRepositoryImpl.java @@ -0,0 +1,52 @@ +package com.stempo.api.domain.persistence.repository; + +import com.stempo.api.domain.domain.model.UserAchievement; +import com.stempo.api.domain.domain.repository.UserAchievementRepository; +import com.stempo.api.domain.persistence.entity.UserAchievementEntity; +import com.stempo.api.domain.persistence.mappper.UserAchievementMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class UserAchievementRepositoryImpl implements UserAchievementRepository { + + private final UserAchievementJpaRepository repository; + private final UserAchievementMapper mapper; + + @Override + public UserAchievement save(UserAchievement userAchievement) { + UserAchievementEntity jpaEntity = mapper.toEntity(userAchievement); + UserAchievementEntity savedEntity = repository.save(jpaEntity); + return mapper.toDomain(savedEntity); + } + + @Override + public void saveAll(List achievements) { + List jpaEntities = achievements.stream() + .map(mapper::toEntity) + .toList(); + repository.saveAll(jpaEntities); + } + + @Override + public List findByDeviceTag(String deviceTag) { + return repository.findByDeviceTag(deviceTag); + } + + @Override + public Optional findByDeviceTagAndAchievementId(String deviceTag, Long achievementId) { + return repository.findByDeviceTagAndAchievementId(deviceTag, achievementId); + } + + @Override + public List findByAchievementId(Long achievementId) { + List jpaEntities = repository.findByAchievementId(achievementId); + return jpaEntities.stream() + .map(mapper::toDomain) + .toList(); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/AchievementController.java b/src/main/java/com/stempo/api/domain/presentation/AchievementController.java new file mode 100644 index 00000000..6ceaf137 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/AchievementController.java @@ -0,0 +1,69 @@ +package com.stempo.api.domain.presentation; + +import com.stempo.api.domain.application.service.AchievementService; +import com.stempo.api.domain.presentation.dto.request.AchievementRequestDto; +import com.stempo.api.domain.presentation.dto.request.AchievementUpdateRequestDto; +import com.stempo.api.domain.presentation.dto.response.AchievementResponseDto; +import com.stempo.api.global.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@Tag(name = "Achievement", description = "업적") +public class AchievementController { + + private final AchievementService achievementService; + + @Operation(summary = "[A] 업적 추가", description = "ROLE_ADMIN 이상의 권한이 필요함") + @Secured({ "ROLE_ADMIN" }) + @PostMapping("/api/v1/achievements") + public ApiResponse registerAchievement( + @Valid @RequestBody AchievementRequestDto requestDto + ) { + Long id = achievementService.registerAchievement(requestDto); + return ApiResponse.success(id); + } + + @Operation(summary = "[U] 업적 목록 조회", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({ "ROLE_USER", "ROLE_ADMIN" }) + @GetMapping("/api/v1/achievements") + public ApiResponse> getAchievements() { + List achievements = achievementService.getAchievements(); + return ApiResponse.success(achievements); + } + + @Operation(summary = "[A] 업적 수정", description = "ROLE_ADMIN 이상의 권한이 필요함") + @Secured({ "ROLE_ADMIN" }) + @PatchMapping("/api/v1/achievements/{achievementId}") + public ApiResponse updateAchievement( + @PathVariable(name = "achievementId") Long achievementId, + @Valid @RequestBody AchievementUpdateRequestDto requestDto + ) { + Long id = achievementService.updateAchievement(achievementId, requestDto); + return ApiResponse.success(id); + } + + @Operation(summary = "[A] 업적 삭제", description = "ROLE_ADMIN 이상의 권한이 필요함") + @Secured({ "ROLE_ADMIN" }) + @DeleteMapping("/api/v1/achievements/{achievementId}") + public ApiResponse deleteAchievement( + @PathVariable(name = "achievementId") Long achievementId + ) { + Long id = achievementService.deleteAchievement(achievementId); + return ApiResponse.success(id); + } + +} diff --git a/src/main/java/com/stempo/api/domain/presentation/UserAchievementController.java b/src/main/java/com/stempo/api/domain/presentation/UserAchievementController.java new file mode 100644 index 00000000..b7b1103a --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/UserAchievementController.java @@ -0,0 +1,41 @@ +package com.stempo.api.domain.presentation; + +import com.stempo.api.domain.application.service.UserAchievementService; +import com.stempo.api.domain.presentation.dto.response.UserAchievementResponseDto; +import com.stempo.api.global.common.dto.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@Tag(name = "Achievement - User", description = "유저 업적") +public class UserAchievementController { + + private final UserAchievementService userAchievementService; + + @Operation(summary = "[U] 유저 업적 등록", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({ "ROLE_USER", "ROLE_ADMIN" }) + @PostMapping("/api/v1/achievements/user/{achievementId}") + public ApiResponse registerUserAchievement( + @PathVariable(name = "achievementId") Long achievementId + ) { + Long id = userAchievementService.registerUserAchievement(achievementId); + return ApiResponse.success(id); + } + + @Operation(summary = "[U] 내 업적 조회", description = "ROLE_USER 이상의 권한이 필요함") + @Secured({ "ROLE_USER", "ROLE_ADMIN" }) + @GetMapping("/api/v1/achievements/user") + public ApiResponse> getUserAchievements() { + List myAchievements = userAchievementService.getUserAchievements(); + return ApiResponse.success(myAchievements); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/request/AchievementRequestDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/request/AchievementRequestDto.java new file mode 100644 index 00000000..9fe4f45b --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/request/AchievementRequestDto.java @@ -0,0 +1,32 @@ +package com.stempo.api.domain.presentation.dto.request; + +import com.stempo.api.domain.domain.model.Achievement; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AchievementRequestDto { + + @NotNull + @Schema(description = "업적 이름", example = "업적 이름", minLength = 1, maxLength = 100) + private String name; + + @NotNull + @Schema(description = "업적 설명", example = "업적 설명", minLength = 1, maxLength = 255) + private String description; + + @NotNull + @Schema(description = "이미지 URL", example = "https://example.com/image.jpg", minLength = 1, maxLength = 255) + private String imageUrl; + + public static Achievement toDomain(AchievementRequestDto requestDto) { + return Achievement.builder() + .name(requestDto.getName()) + .description(requestDto.getDescription()) + .imageUrl(requestDto.getImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/request/AchievementUpdateRequestDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/request/AchievementUpdateRequestDto.java new file mode 100644 index 00000000..82549518 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/request/AchievementUpdateRequestDto.java @@ -0,0 +1,19 @@ +package com.stempo.api.domain.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AchievementUpdateRequestDto { + + @Schema(description = "업적 이름", example = "업적 이름", minLength = 1, maxLength = 100) + private String name; + + @Schema(description = "업적 설명", example = "업적 설명", minLength = 1, maxLength = 255) + private String description; + + @Schema(description = "이미지 URL", example = "https://example.com/image.jpg", minLength = 1, maxLength = 255) + private String imageUrl; +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/response/AchievementResponseDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/response/AchievementResponseDto.java new file mode 100644 index 00000000..fba8dd3d --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/response/AchievementResponseDto.java @@ -0,0 +1,24 @@ +package com.stempo.api.domain.presentation.dto.response; + +import com.stempo.api.domain.domain.model.Achievement; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class AchievementResponseDto { + + private Long id; + private String name; + private String description; + private String imageUrl; + + public static AchievementResponseDto toDto(Achievement achievement) { + return AchievementResponseDto.builder() + .id(achievement.getId()) + .name(achievement.getName()) + .description(achievement.getDescription()) + .imageUrl(achievement.getImageUrl()) + .build(); + } +} diff --git a/src/main/java/com/stempo/api/domain/presentation/dto/response/UserAchievementResponseDto.java b/src/main/java/com/stempo/api/domain/presentation/dto/response/UserAchievementResponseDto.java new file mode 100644 index 00000000..42ba6851 --- /dev/null +++ b/src/main/java/com/stempo/api/domain/presentation/dto/response/UserAchievementResponseDto.java @@ -0,0 +1,22 @@ +package com.stempo.api.domain.presentation.dto.response; + +import com.stempo.api.domain.domain.model.Achievement; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class UserAchievementResponseDto { + + private final String name; + private final String description; + private final String imageUrl; + + public static UserAchievementResponseDto toDto(Achievement achievement) { + return UserAchievementResponseDto.builder() + .name(achievement.getName()) + .description(achievement.getDescription()) + .imageUrl(achievement.getImageUrl()) + .build(); + } +}