diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/application/AccountService.java b/space-d/src/main/java/com/dnd/spaced/core/account/application/AccountService.java index 0a25d8d5..1a829b06 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/application/AccountService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/application/AccountService.java @@ -1,8 +1,8 @@ package com.dnd.spaced.core.account.application; import com.dnd.spaced.core.account.application.dto.mapper.AccountResponseMapper; -import com.dnd.spaced.core.account.application.dto.request.ChangeCareerInfoRequest; -import com.dnd.spaced.core.account.application.dto.request.ChangeProfileInfoRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeCareerRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeProfileRequest; import com.dnd.spaced.core.account.application.dto.response.AccountResponse; import com.dnd.spaced.core.account.application.exception.ForbiddenAccountException; import com.dnd.spaced.core.account.domain.Account; @@ -17,40 +17,65 @@ public class AccountService { private final AccountRepository accountRepository; + private final AccountResponseMapper mapper; @Transactional public void withdrawal(Long accountId) { - Account authorizedAccount = findAuthorizedAccount(accountId); + Account account = findAccount(accountId); - authorizedAccount.withdrawal(); + withdrawAccount(account); } @Transactional - public void changeCareerInfo(Long accountId, ChangeCareerInfoRequest request) { - Account authorizedAccount = findAuthorizedAccount(accountId); + public void changeCareer(Long accountId, ChangeCareerRequest request) { + Account account = findAccount(accountId); - authorizedAccount.changeCareerInfo( - request.changedJobGroupName(), - request.changedCompanyName(), - request.changedExperienceName() - ); + updateAccountCareer(account, request); } @Transactional - public void changeProfileInfo(Long accountId, ChangeProfileInfoRequest request) { - Account authorizedAccount = findAuthorizedAccount(accountId); - ProfileImageName changedProfileImageName = ProfileImageName.findBy(request.changedProfileImageKoreanName()); + public void changeProfile(Long accountId, ChangeProfileRequest request) { + Account account = findAccount(accountId); + ProfileImageName changedProfileImageName = findProfileImageName(request); - authorizedAccount.changeProfileInfo(request.changedNickname(), changedProfileImageName.getImageName()); + updateAccountProfile(account, request, changedProfileImageName); } public AccountResponse readAccount(Long accountId) { - Account authorizedAccount = findAuthorizedAccount(accountId); + Account account = findAccount(accountId); + + return convertAccountResponse(account); + } + + private void withdrawAccount(Account account) { + account.withdrawal(); + } + + private ProfileImageName findProfileImageName(ChangeProfileRequest request) { + return ProfileImageName.findByKorean(request.changedProfileImageKoreanName()); + } + + private void updateAccountProfile( + Account account, + ChangeProfileRequest request, + ProfileImageName changedProfileImageName + ) { + account.changeProfile(request.changedNickname(), changedProfileImageName); + } + + private void updateAccountCareer(Account account, ChangeCareerRequest request) { + account.changeCareer( + request.changedJobGroupName(), + request.changedCompanyName(), + request.changedExperienceName() + ); + } - return AccountResponseMapper.toDto(authorizedAccount); + private AccountResponse convertAccountResponse(Account account) { + return mapper.toDto(account); } - private Account findAuthorizedAccount(Long accountId) { + private Account findAccount(Long accountId) { return accountRepository.findBy(accountId) .orElseThrow(() -> new ForbiddenAccountException("존재하지 않는 회원이거나 이미 탈퇴한 회원입니다.")); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/mapper/AccountResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/mapper/AccountResponseMapper.java index 21a06a10..d7846a83 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/mapper/AccountResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/mapper/AccountResponseMapper.java @@ -2,24 +2,23 @@ import com.dnd.spaced.core.account.application.dto.response.AccountResponse; import com.dnd.spaced.core.account.domain.Account; -import com.dnd.spaced.core.account.domain.embed.CareerInfo; -import com.dnd.spaced.core.account.domain.embed.ProfileInfo; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import com.dnd.spaced.core.account.domain.embed.Career; +import com.dnd.spaced.core.account.domain.embed.Profile; +import com.dnd.spaced.global.mapper.Mapper; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class AccountResponseMapper { +@Mapper +public class AccountResponseMapper { - public static AccountResponse toDto(Account account) { - ProfileInfo profileInfo = account.getProfileInfo(); - CareerInfo careerInfo = account.getCareerInfo(); + public AccountResponse toDto(Account account) { + Profile profile = account.getProfile(); + Career career = account.getCareer(); return new AccountResponse( - profileInfo.getNickname(), - profileInfo.getProfileImage(), - careerInfo.getJobGroup().getName(), - careerInfo.getCompany().getName(), - careerInfo.getExperience().getName() + profile.getNickname(), + profile.getProfileImageName(), + career.getJobGroup().getName(), + career.getCompany().getName(), + career.getExperience().getName() ); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeCareerInfoRequest.java b/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeCareerRequest.java similarity index 87% rename from space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeCareerInfoRequest.java rename to space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeCareerRequest.java index 2131d760..fd127de3 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeCareerInfoRequest.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeCareerRequest.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotBlank; -public record ChangeCareerInfoRequest( +public record ChangeCareerRequest( @NotBlank String changedJobGroupName, diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeProfileInfoRequest.java b/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeProfileRequest.java similarity index 85% rename from space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeProfileInfoRequest.java rename to space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeProfileRequest.java index b2698be2..85a188a8 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeProfileInfoRequest.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/application/dto/request/ChangeProfileRequest.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotBlank; -public record ChangeProfileInfoRequest( +public record ChangeProfileRequest( @NotBlank String changedNickname, diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/domain/Account.java b/space-d/src/main/java/com/dnd/spaced/core/account/domain/Account.java index 58544375..f127583c 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/domain/Account.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/domain/Account.java @@ -1,8 +1,9 @@ package com.dnd.spaced.core.account.domain; -import com.dnd.spaced.core.account.domain.embed.CareerInfo; -import com.dnd.spaced.core.account.domain.embed.ProfileInfo; -import com.dnd.spaced.core.account.domain.embed.SocialInfo; +import com.dnd.spaced.core.account.domain.embed.Career; +import com.dnd.spaced.core.account.domain.embed.Profile; +import com.dnd.spaced.core.account.domain.embed.Social; +import com.dnd.spaced.core.account.domain.enums.ProfileImageName; import com.dnd.spaced.core.account.domain.enums.RegistrationId; import com.dnd.spaced.core.account.domain.enums.Role; import com.dnd.spaced.global.audit.BaseTimeEntity; @@ -37,44 +38,44 @@ public class Account extends BaseTimeEntity { private boolean deleted = false; @Embedded - private SocialInfo socialInfo; + private Social social; @Embedded - private ProfileInfo profileInfo; + private Profile profile; @Embedded - private CareerInfo careerInfo; + private Career career; @Builder private Account( String nickname, - String profileImage, + ProfileImageName profileImageName, Role role, RegistrationId registrationId, String socialIdentifier ) { - this.profileInfo = ProfileInfo.of(nickname, profileImage); + this.profile = Profile.of(nickname, profileImageName); this.role = role; - this.socialInfo = new SocialInfo(registrationId, socialIdentifier); + this.social = new Social(registrationId, socialIdentifier); } public void withdrawal() { this.deleted = true; } - public void changeCareerInfo( + public void changeCareer( String changedJobGroupName, String changedCompanyName, String changedExperienceName) { - this.careerInfo = CareerInfo.builder() - .jobGroupName(changedJobGroupName) - .companyName(changedCompanyName) - .experienceName(changedExperienceName) - .build(); + this.career = Career.builder() + .jobGroupName(changedJobGroupName) + .companyName(changedCompanyName) + .experienceName(changedExperienceName) + .build(); } - public void changeProfileInfo(String changedNickname, String changedProfileImage) { - this.profileInfo = ProfileInfo.of(changedNickname, changedProfileImage); + public void changeProfile(String changedNickname, ProfileImageName changedProfileImageName) { + this.profile = Profile.of(changedNickname, changedProfileImageName); } public boolean isEqualTo(Long id) { diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/CareerInfo.java b/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Career.java similarity index 88% rename from space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/CareerInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Career.java index b87637a5..be2352d5 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/CareerInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Career.java @@ -14,7 +14,7 @@ @Getter @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CareerInfo { +public class Career { @Enumerated(EnumType.STRING) private JobGroup jobGroup; @@ -26,7 +26,7 @@ public class CareerInfo { private Experience experience; @Builder - private CareerInfo(String jobGroupName, String companyName, String experienceName) { + private Career(String jobGroupName, String companyName, String experienceName) { this.jobGroup = JobGroup.findBy(jobGroupName); this.company = Company.findBy(companyName); this.experience = Experience.findBy(experienceName); diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/ProfileInfo.java b/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Profile.java similarity index 58% rename from space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/ProfileInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Profile.java index b4b7b870..28d12aaa 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/ProfileInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Profile.java @@ -2,6 +2,7 @@ import com.dnd.spaced.core.account.domain.embed.exception.InvalidNicknameException; import com.dnd.spaced.core.account.domain.embed.exception.InvalidProfileImageException; +import com.dnd.spaced.core.account.domain.enums.ProfileImageName; import jakarta.persistence.Embeddable; import lombok.AccessLevel; import lombok.Getter; @@ -10,7 +11,7 @@ @Getter @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ProfileInfo { +public class Profile { private static final int NICKNAME_MIN_LENGTH = 5; private static final int NICKNAME_MAX_LENGTH = 10; @@ -21,20 +22,24 @@ public class ProfileInfo { ); private String nickname; - private String profileImage; + private String profileImageName; - public static ProfileInfo of(String nickname, String profileImage) { - validateContent(nickname, profileImage); + public static Profile of(String nickname, ProfileImageName profileImageName) { + validateNickname(nickname); + validateProfileImageName(profileImageName); - return new ProfileInfo(nickname, profileImage); + return new Profile(nickname, profileImageName.getImageName()); } - private static void validateContent(String nickname, String profileImage) { + private static void validateNickname(String nickname) { if (isInvalidNickname(nickname)) { throw new InvalidNicknameException(NICKNAME_EXCEPTION_MESSAGE); } - if (isInvalidProfileImage(profileImage)) { - throw new InvalidProfileImageException("프로필 이미지 정보는 null이거나 비어 있을 수 없습니다."); + } + + private static void validateProfileImageName(ProfileImageName profileImageName) { + if (isInvalidProfileImageName(profileImageName)) { + throw new InvalidProfileImageException("프로필 이미지 정보는 null일 수 없습니다."); } } @@ -43,19 +48,12 @@ private static boolean isInvalidNickname(String nickname) { || nickname.length() < NICKNAME_MIN_LENGTH || nickname.length() > NICKNAME_MAX_LENGTH; } - private static boolean isInvalidProfileImage(String profileImage) { - return profileImage == null || profileImage.isBlank(); + private static boolean isInvalidProfileImageName(ProfileImageName profileImageName) { + return profileImageName == null; } - private ProfileInfo(String nickname, String profileImage) { + private Profile(String nickname, String profileImageName) { this.nickname = nickname; - this.profileImage = profileImage; - } - - public void changeProfileInfo(String changedNickname, String changedProfileImage) { - validateContent(changedNickname, changedProfileImage); - - this.nickname = changedNickname; - this.profileImage = changedProfileImage; + this.profileImageName = profileImageName; } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/SocialInfo.java b/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Social.java similarity index 72% rename from space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/SocialInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Social.java index e0ea4053..de556614 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/SocialInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/domain/embed/Social.java @@ -11,15 +11,15 @@ @Getter @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class SocialInfo { +public class Social { @Enumerated(EnumType.STRING) private RegistrationId registrationId; - private String socialIdentifier; + private String socialId; - public SocialInfo(RegistrationId registrationId, String socialIdentifier) { + public Social(RegistrationId registrationId, String socialId) { this.registrationId = registrationId; - this.socialIdentifier = socialIdentifier; + this.socialId = socialId; } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/domain/enums/ProfileImageName.java b/space-d/src/main/java/com/dnd/spaced/core/account/domain/enums/ProfileImageName.java index af53f745..0fd6f893 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/domain/enums/ProfileImageName.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/domain/enums/ProfileImageName.java @@ -32,10 +32,17 @@ public static ProfileImageName findRandom() { .orElse(EARTH); } - public static ProfileImageName findBy(String korean) { + public static ProfileImageName findByKorean(String korean) { return Arrays.stream(ProfileImageName.values()) .filter(profileImageName -> profileImageName.korean.equals(korean)) .findAny() .orElseThrow(() -> new InvalidProfileImageNameException(String.format(EXCEPTION_MESSAGE, korean))); } + + public static ProfileImageName findByImageName(String imageName) { + return Arrays.stream(ProfileImageName.values()) + .filter(profileImageName -> profileImageName.imageName.equals(imageName)) + .findAny() + .orElseThrow(() -> new InvalidProfileImageNameException(String.format(EXCEPTION_MESSAGE, imageName))); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/domain/repository/AccountRepository.java b/space-d/src/main/java/com/dnd/spaced/core/account/domain/repository/AccountRepository.java index 5cb41ec8..035f372f 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/domain/repository/AccountRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/domain/repository/AccountRepository.java @@ -1,7 +1,7 @@ package com.dnd.spaced.core.account.domain.repository; import com.dnd.spaced.core.account.domain.Account; -import com.dnd.spaced.core.account.domain.enums.RegistrationId; +import com.dnd.spaced.core.account.domain.embed.Social; import java.util.Optional; public interface AccountRepository { @@ -12,7 +12,7 @@ public interface AccountRepository { Optional findBy(Long accountId); - Optional findBy(RegistrationId registrationId, String socialIdentifier); + Optional findBy(Social social); Optional findPreInitializationAccountBy(Long accountId); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepository.java index 915e293e..fe11dc68 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepository.java @@ -3,7 +3,7 @@ import static com.dnd.spaced.core.account.domain.QAccount.account; import com.dnd.spaced.core.account.domain.Account; -import com.dnd.spaced.core.account.domain.enums.RegistrationId; +import com.dnd.spaced.core.account.domain.embed.Social; import com.dnd.spaced.core.account.domain.repository.AccountRepository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -21,9 +21,9 @@ public class AccountGatewayRepository implements AccountRepository { @Override public boolean existsBy(Long accountId) { Integer result = queryFactory.selectOne() - .from(account) - .where(account.id.eq(accountId)) - .fetchFirst(); + .from(account) + .where(eqAccountId(accountId)) + .fetchFirst(); return result != null; } @@ -36,20 +36,16 @@ public Account save(Account account) { @Override public Optional findBy(Long accountId) { Account result = queryFactory.selectFrom(account) - .where(eqAccountId(accountId), account.deleted.isFalse()) - .fetchOne(); + .where(eqAccountId(accountId)) + .fetchOne(); return Optional.ofNullable(result); } @Override - public Optional findBy(RegistrationId registrationId, String socialIdentifier) { + public Optional findBy(Social social) { Account result = queryFactory.selectFrom(account) - .where( - account.socialInfo.socialIdentifier.eq(socialIdentifier), - account.deleted.isFalse(), - account.socialInfo.registrationId.eq(registrationId) - ) + .where(eqSocial(social)) .fetchOne(); return Optional.ofNullable(result); @@ -58,27 +54,29 @@ public Optional findBy(RegistrationId registrationId, String socialIden @Override public Optional findPreInitializationAccountBy(Long accountId) { Account result = queryFactory.selectFrom(account) - .where( - account.id.eq(accountId), - account.deleted.isFalse(), - isNullCareerInfo() - ) + .where(eqAccountId(accountId), isNullCareer()) .fetchOne(); return Optional.ofNullable(result); } + private BooleanExpression eqSocial(Social social) { + return account.social.socialId.eq(social.getSocialId()) + .and(account.deleted.isFalse()) + .and(account.social.registrationId.eq(social.getRegistrationId())); + } + + private BooleanExpression isNullCareer() { + return account.career.company.isNull() + .and(account.career.experience.isNull()) + .and(account.career.jobGroup.isNull()); + } + private BooleanExpression eqAccountId(Long accountId) { if (accountId == null) { return null; } - return account.id.eq(accountId); - } - - private BooleanExpression isNullCareerInfo() { - return account.careerInfo.company.isNull() - .and(account.careerInfo.experience.isNull()) - .and(account.careerInfo.jobGroup.isNull()); + return account.id.eq(accountId).and(account.deleted.isFalse()); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/presentation/AccountController.java b/space-d/src/main/java/com/dnd/spaced/core/account/presentation/AccountController.java index efc32461..81b52bf6 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/account/presentation/AccountController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/account/presentation/AccountController.java @@ -1,11 +1,11 @@ package com.dnd.spaced.core.account.presentation; import com.dnd.spaced.core.account.application.AccountService; -import com.dnd.spaced.core.account.application.dto.request.ChangeCareerInfoRequest; -import com.dnd.spaced.core.account.application.dto.request.ChangeProfileInfoRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeCareerRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeProfileRequest; import com.dnd.spaced.core.account.application.dto.response.AccountResponse; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -25,35 +25,35 @@ public class AccountController { private final AccountService accountService; @DeleteMapping("/withdrawal") - public ResponseEntity withdrawal(@CurrentAccountInfo AuthAccountInfo accountInfo) { - accountService.withdrawal(accountInfo.accountId()); + public ResponseEntity withdrawal(@CurrentAccount AuthAccountId accountId) { + accountService.withdrawal(accountId.id()); return ResponseEntityConst.NO_CONTENT; } @PutMapping("/career-info") - public ResponseEntity changeCareerInfo( - @CurrentAccountInfo AuthAccountInfo accountInfo, - @Valid @RequestBody ChangeCareerInfoRequest request + public ResponseEntity changeCareer( + @CurrentAccount AuthAccountId accountId, + @Valid @RequestBody ChangeCareerRequest request ) { - accountService.changeCareerInfo(accountInfo.accountId(), request); + accountService.changeCareer(accountId.id(), request); return ResponseEntityConst.NO_CONTENT; } @PutMapping("/profile-info") - public ResponseEntity changeProfileInfo( - @CurrentAccountInfo AuthAccountInfo accountInfo, - @Valid @RequestBody ChangeProfileInfoRequest request + public ResponseEntity changeProfile( + @CurrentAccount AuthAccountId accountId, + @Valid @RequestBody ChangeProfileRequest request ) { - accountService.changeProfileInfo(accountInfo.accountId(), request); + accountService.changeProfile(accountId.id(), request); return ResponseEntityConst.NO_CONTENT; } @GetMapping - public ResponseEntity readAccount(@CurrentAccountInfo AuthAccountInfo accountInfo) { - AccountResponse response = accountService.readAccount(accountInfo.accountId()); + public ResponseEntity readAccount(@CurrentAccount AuthAccountId accountId) { + AccountResponse response = accountService.readAccount(accountId.id()); return ResponseEntity.ok(response); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/account/presentation/dto/response/AccountInfoResponse.java b/space-d/src/main/java/com/dnd/spaced/core/account/presentation/dto/response/AccountInfoResponse.java deleted file mode 100644 index ef1a0c6f..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/account/presentation/dto/response/AccountInfoResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.dnd.spaced.core.account.presentation.dto.response; - -import com.dnd.spaced.core.account.application.dto.response.AccountResponse; - -public record AccountInfoResponse( - String nickname, - String profileImage, - String jobGroupName, - String companyName, - String experienceName -) { - - public static AccountInfoResponse from(AccountResponse dto) { - return new AccountInfoResponse( - dto.nickname(), - dto.profileImage(), - dto.jobGroupName(), - dto.companyName(), - dto.experienceName() - ); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminReportService.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminReportService.java index 250fc370..1ec78588 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminReportService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminReportService.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.admin.application; -import com.dnd.spaced.core.admin.application.dto.mapper.ReportInfoMapper; +import com.dnd.spaced.core.admin.application.dto.mapper.ReportResponseMapper; import com.dnd.spaced.core.admin.application.dto.request.ProcessReportRequest; import com.dnd.spaced.core.admin.application.dto.request.ReadAllReportSearchRequest; import com.dnd.spaced.core.admin.application.dto.resposne.ReportCollectionResponse; @@ -22,6 +22,7 @@ public class AdminReportService { private final ReportRepository reportRepository; + private final ReportResponseMapper mapper; private final ApplicationEventPublisher eventPublisher; @Transactional @@ -29,7 +30,7 @@ public void processReport(Long reportId, ProcessReportRequest request) { Report report = findReport(reportId); ReportStatus reportStatus = findReportStatus(request); - report.process(reportStatus); + processReport(report, reportStatus); publishProcessedReportEvent(reportStatus, report); } @@ -37,7 +38,7 @@ public ReportCollectionResponse readReports(ReadAllReportSearchRequest request, ReportStatus reportStatus = findReportStatus(request); List reports = findAllReportsBy(request, reportStatus, pageable); - return ReportInfoMapper.toDto(reports); + return convertReportCollectionResponse(reports); } private Report findReport(Long reportId) { @@ -59,6 +60,10 @@ private ReportStatus findReportStatus(ReadAllReportSearchRequest request) { .orElse(null); } + private void processReport(Report report, ReportStatus reportStatus) { + report.process(reportStatus); + } + private void publishProcessedReportEvent(ReportStatus reportStatus, Report report) { eventPublisher.publishEvent(new ProcessedReportEvent(reportStatus, report.getCommentId())); } @@ -70,4 +75,8 @@ private List findAllReportsBy( ) { return reportRepository.findAllBy(reportStatus, request.lastReportId(), pageable); } + + private ReportCollectionResponse convertReportCollectionResponse(List reports) { + return mapper.toDto(reports); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceFacade.java new file mode 100644 index 00000000..700077ab --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceFacade.java @@ -0,0 +1,55 @@ +package com.dnd.spaced.core.admin.application; + +import com.dnd.spaced.core.quiz.application.event.dto.AddedTodayQuizQuestionEvent; +import com.dnd.spaced.core.quiz.domain.TodayQuiz; +import com.dnd.spaced.core.quiz.domain.dto.mapper.TodayQuizDtoMapper; +import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; +import com.dnd.spaced.global.consts.CacheConst; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AdminTodayQuizServiceFacade { + + private final CreateTodayQuizService createTodayQuizService; + private final CacheManager memoryCacheManager; + private final TodayQuizDtoMapper mapper; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public Long createTodayQuiz() { + TodayQuiz todayQuiz = generateRandomTodayQuiz(); + + publishAddedTodayQuizQuestionEvent(); + persistMemoryCache(todayQuiz); + return todayQuiz.getId(); + } + + private TodayQuiz generateRandomTodayQuiz() { + QuizCategory quizCategory = findRandomQuizCategory(); + + return createTodayQuizService.createTodayQuiz(quizCategory); + } + + private QuizCategory findRandomQuizCategory() { + return QuizCategory.findRandom(); + } + + private void publishAddedTodayQuizQuestionEvent() { + eventPublisher.publishEvent(new AddedTodayQuizQuestionEvent()); + } + + private void persistMemoryCache(TodayQuiz todayQuiz) { + Cache cache = memoryCacheManager.getCache(CacheConst.TODAY_QUIZ_CACHE_NAME); + + if (cache != null) { + cache.clear(); + cache.put(CacheConst.TODAY_QUIZ_CACHE_NAME, mapper.toDto(todayQuiz)); + } + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminWordServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminWordServiceFacade.java new file mode 100644 index 00000000..d508a17a --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminWordServiceFacade.java @@ -0,0 +1,66 @@ +package com.dnd.spaced.core.admin.application; + +import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; +import com.dnd.spaced.core.admin.application.dto.resposne.PersistWordDto; +import com.dnd.spaced.core.admin.application.event.dto.DeletedWordEvent; +import com.dnd.spaced.core.word.application.event.dto.PersistedWordEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class AdminWordServiceFacade { + + private final CreateWordService createWordservice; + private final UpdateWordService updateWordService; + private final DeleteWordService deleteWordService; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public Long createWord(CreateWordRequest createWordRequest) { + PersistWordDto wordDto = performWordCreation(createWordRequest); + + publishPersistedWordEvent(wordDto); + return wordDto.id(); + } + + @Transactional + public void updateWordExample(Long wordExampleId, String example) { + updateWordService.updateWordExample(wordExampleId, example); + } + + @Transactional + public void deleteWordExample(Long wordId, Long wordExampleId) { + deleteWordService.deleteWordExample(wordId, wordExampleId); + } + + @Transactional + public void deletePronunciation(Long wordId, Long pronunciationId) { + deleteWordService.deletePronunciation(wordId, pronunciationId); + } + + @Transactional + public void deleteWord(Long wordId) { + performWordDeletion(wordId); + + publishDeletedWordEvent(wordId); + } + + private PersistWordDto performWordCreation(CreateWordRequest createWordRequest) { + return createWordservice.createWord(createWordRequest); + } + + private void performWordDeletion(Long wordId) { + deleteWordService.deleteWord(wordId); + } + + private void publishDeletedWordEvent(Long wordId) { + eventPublisher.publishEvent(new DeletedWordEvent(wordId)); + } + + private void publishPersistedWordEvent(PersistWordDto wordDto) { + eventPublisher.publishEvent(new PersistedWordEvent(wordDto.id(), wordDto.category())); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminTodayQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/CreateTodayQuizService.java similarity index 50% rename from space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminTodayQuizService.java rename to space-d/src/main/java/com/dnd/spaced/core/admin/application/CreateTodayQuizService.java index 3b6c6d33..73000fbc 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminTodayQuizService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/CreateTodayQuizService.java @@ -1,39 +1,33 @@ package com.dnd.spaced.core.admin.application; import com.dnd.spaced.core.admin.application.exception.WordMetadataNotFoundException; -import com.dnd.spaced.core.quiz.application.enums.QuizWordCountValidator; -import com.dnd.spaced.core.quiz.application.event.dto.AddedTodayQuizQuestionEvent; import com.dnd.spaced.core.quiz.application.exception.InvalidTodayQuizWordCountException; import com.dnd.spaced.core.quiz.domain.TodayQuiz; import com.dnd.spaced.core.quiz.domain.TodayQuizOption; -import com.dnd.spaced.core.quiz.domain.dto.mapper.TodayQuizInfoMapper; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizAnswerOption; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizQuestion; import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.quiz.domain.repository.TodayQuizOptionRepository; import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; +import com.dnd.spaced.core.quiz.domain.service.QuizWordCountValidator; import com.dnd.spaced.core.word.domain.WordMetadata; -import com.dnd.spaced.core.word.domain.dto.SimpleWordInfo; +import com.dnd.spaced.core.word.domain.dto.SimpleWord; import com.dnd.spaced.core.word.domain.repository.WordMetadataRepository; import com.dnd.spaced.core.word.domain.repository.WordRandomRepository; import com.dnd.spaced.global.config.properties.QuizQuestionProperties; -import com.dnd.spaced.global.consts.CacheConst; -import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; -import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -public class AdminTodayQuizService { +class CreateTodayQuizService { private static final Long DEFAULT_WORD_METADATA_ID = 1L; - private static final int REQUIRED_QUIZ_WORD_COUNT = 4; + private static final int REQUIRED_TODAY_QUIZ_WORD_COUNT = 4; private static final int ANSWER_OPTION_INDEX = 0; private final TodayQuizRepository todayQuizRepository; @@ -41,26 +35,18 @@ public class AdminTodayQuizService { private final WordMetadataRepository wordMetadataRepository; private final TodayQuizOptionRepository todayQuizOptionRepository; private final QuizQuestionProperties quizQuestionProperties; - private final CacheManager memoryCacheManager; - private final ApplicationEventPublisher eventPublisher; @Transactional - public Long createTodayQuiz() { - QuizCategory quizCategory = QuizCategory.findRandom(); + public TodayQuiz createTodayQuiz(QuizCategory quizCategory) { + validateQuizCreationRequirements(quizCategory); - validateQuizCreation(quizCategory); - - TodayQuiz todayQuiz = createTodayQuiz(quizCategory); - - persistMemoryCache(todayQuiz); - - return todayQuiz.getId(); + return assembleTodayQuiz(quizCategory); } - private void validateQuizCreation(QuizCategory quizCategory) { + private void validateQuizCreationRequirements(QuizCategory quizCategory) { WordMetadata wordMetadata = findWordMetadata(); - validateQuizWordCount(quizCategory, wordMetadata); + validateWordCount(quizCategory, wordMetadata); } private WordMetadata findWordMetadata() { @@ -70,28 +56,29 @@ private WordMetadata findWordMetadata() { ); } - private void validateQuizWordCount(QuizCategory quizCategory, WordMetadata wordMetadata) { - if (QuizWordCountValidator.isInvalidate(quizCategory, wordMetadata, REQUIRED_QUIZ_WORD_COUNT)) { + private void validateWordCount(QuizCategory quizCategory, WordMetadata wordMetadata) { + QuizWordCountValidator validator = QuizWordCountValidator.create(); + + if (validator.isInvalidate(quizCategory, wordMetadata, REQUIRED_TODAY_QUIZ_WORD_COUNT)) { throw new InvalidTodayQuizWordCountException("오늘의 퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); } } - private TodayQuiz createTodayQuiz(QuizCategory quizCategory) { - List randomWords = findRandomWords(quizCategory); - TodayQuiz todayQuiz = initTodayQuiz(quizCategory, randomWords); - TodayQuiz savedTodayQuiz = todayQuizRepository.save(todayQuiz); + private TodayQuiz assembleTodayQuiz(QuizCategory quizCategory) { + List randomWords = findRandomWords(quizCategory); + TodayQuiz todayQuiz = buildTodayQuiz(quizCategory, randomWords); + TodayQuiz persistedTodayQuiz = persistTodayQuiz(todayQuiz); - persistTodayQuizOptions(randomWords, todayQuiz); - publishAddedTodayQuizQuestionEvent(); - return savedTodayQuiz; + setupTodayQuizOptions(randomWords, todayQuiz); + return persistedTodayQuiz; } - private List findRandomWords(QuizCategory quizCategory) { - return wordRandomRepository.findRandomAllBy(quizCategory, REQUIRED_QUIZ_WORD_COUNT); + private List findRandomWords(QuizCategory quizCategory) { + return wordRandomRepository.findRandomAllBy(quizCategory, REQUIRED_TODAY_QUIZ_WORD_COUNT); } - private TodayQuiz initTodayQuiz(QuizCategory quizCategory, List randomWords) { - SimpleWordInfo answerWord = randomWords.get(ANSWER_OPTION_INDEX); + private TodayQuiz buildTodayQuiz(QuizCategory quizCategory, List randomWords) { + SimpleWord answerWord = randomWords.get(ANSWER_OPTION_INDEX); TodayQuizAnswerOption todayQuizAnswerOption = new TodayQuizAnswerOption( answerWord.id(), answerWord.name() @@ -106,30 +93,36 @@ private TodayQuiz initTodayQuiz(QuizCategory quizCategory, List return new TodayQuiz(todayQuizQuestion); } - private void persistTodayQuizOptions(List randomWords, TodayQuiz todayQuiz) { - Collections.shuffle(randomWords); + private TodayQuiz persistTodayQuiz(TodayQuiz todayQuiz) { + return todayQuizRepository.save(todayQuiz); + } - List todayQuizOptions = new ArrayList<>(); - for (int i = 0; i < randomWords.size(); i++) { - SimpleWordInfo word = randomWords.get(i); + private void setupTodayQuizOptions(List randomWords, TodayQuiz todayQuiz) { + List shuffledWords = shuffleRandomWords(randomWords); + List todayQuizOptions = buildTodayQuizOptions(shuffledWords, todayQuiz); - TodayQuizOption todayQuizOption = TodayQuizOption.of(word.id(), word.name(), i, todayQuiz); - todayQuizOptions.add(todayQuizOption); - } + setupTodayQuizOptions(todayQuizOptions); + } - todayQuizOptionRepository.saveAll(todayQuizOptions); + private List shuffleRandomWords(List randomWords) { + Collections.shuffle(randomWords); + + return randomWords; } - private void publishAddedTodayQuizQuestionEvent() { - eventPublisher.publishEvent(new AddedTodayQuizQuestionEvent()); + private List buildTodayQuizOptions(List randomWords, TodayQuiz todayQuiz) { + return IntStream.range(0, randomWords.size()) + .mapToObj(i -> buildTodayQuizOption(randomWords, todayQuiz, i)) + .toList(); } - private void persistMemoryCache(TodayQuiz todayQuiz) { - Cache cache = memoryCacheManager.getCache(CacheConst.TODAY_QUIZ_CACHE_NAME); + private TodayQuizOption buildTodayQuizOption(List randomWords, TodayQuiz todayQuiz, int index) { + SimpleWord simpleWord = randomWords.get(index); - if (cache != null) { - cache.clear(); - cache.put(CacheConst.TODAY_QUIZ_CACHE_NAME, TodayQuizInfoMapper.toDto(todayQuiz)); - } + return TodayQuizOption.of(simpleWord.id(), simpleWord.name(), index, todayQuiz); + } + + private void setupTodayQuizOptions(List todayQuizOptions) { + todayQuizOptionRepository.saveAll(todayQuizOptions); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/CreateWordService.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/CreateWordService.java new file mode 100644 index 00000000..05675c9e --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/CreateWordService.java @@ -0,0 +1,107 @@ +package com.dnd.spaced.core.admin.application; + +import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; +import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest.CreatePronunciationRequest; +import com.dnd.spaced.core.admin.application.dto.resposne.PersistWordDto; +import com.dnd.spaced.core.word.domain.Pronunciation; +import com.dnd.spaced.core.word.domain.Word; +import com.dnd.spaced.core.word.domain.WordExample; +import com.dnd.spaced.core.word.domain.repository.PronunciationRepository; +import com.dnd.spaced.core.word.domain.repository.WordExampleRepository; +import com.dnd.spaced.core.word.domain.repository.WordRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +class CreateWordService { + + private final WordRepository wordRepository; + private final WordExampleRepository wordExampleRepository; + private final PronunciationRepository pronunciationRepository; + + @Transactional + public PersistWordDto createWord(CreateWordRequest createWordRequest) { + Word word = setupWord(createWordRequest); + + setupWordExamples(word, createWordRequest); + setupPronunciations(word, createWordRequest); + + return convertPersistedWordDto(word); + } + + private Word setupWord(CreateWordRequest createWordRequest) { + Word word = buildWord(createWordRequest); + + return persistWord(word); + } + + private Word persistWord(Word word) { + return wordRepository.save(word); + } + + private Word buildWord(CreateWordRequest request) { + return Word.builder() + .name(request.name()) + .meaning(request.meaning()) + .categoryName(request.categoryName()) + .build(); + } + + private void setupWordExamples(Word word, CreateWordRequest request) { + List wordExamples = buildWordExamples(word, request); + + persistWordExamples(wordExamples); + } + + private List buildWordExamples(Word word, CreateWordRequest request) { + return request.examples() + .stream() + .map(example -> buildWordExample(word, example)) + .toList(); + } + + private WordExample buildWordExample(Word word, String example) { + WordExample wordExample = WordExample.from(example); + + wordExample.initWord(word); + return wordExample; + } + + private void persistWordExamples(List wordExamples) { + wordExampleRepository.saveAll(wordExamples); + } + + private void setupPronunciations(Word word, CreateWordRequest request) { + List pronunciations = buildPronunciations(word, request); + + persistPronunciations(pronunciations); + } + + private void persistPronunciations(List pronunciations) { + pronunciationRepository.saveAll(pronunciations); + } + + private List buildPronunciations(Word word, CreateWordRequest wordRequest) { + return wordRequest.pronunciations() + .stream() + .map(pronunciationRequest -> buildPronunciation(word, pronunciationRequest)) + .toList(); + } + + private Pronunciation buildPronunciation(Word word, CreatePronunciationRequest pronunciationRequest) { + Pronunciation pronunciation = Pronunciation.of( + pronunciationRequest.pronunciation(), + pronunciationRequest.typeName() + ); + + pronunciation.initWord(word); + return pronunciation; + } + + private PersistWordDto convertPersistedWordDto(Word word) { + return new PersistWordDto(word.getId(), word.getCategory()); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminWordService.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/DeleteWordService.java similarity index 54% rename from space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminWordService.java rename to space-d/src/main/java/com/dnd/spaced/core/admin/application/DeleteWordService.java index 13f9655a..1df98052 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/application/AdminWordService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/DeleteWordService.java @@ -1,13 +1,9 @@ package com.dnd.spaced.core.admin.application; -import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; -import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest.CreatePronunciationRequest; -import com.dnd.spaced.core.admin.application.event.dto.DeletedWordEvent; import com.dnd.spaced.core.admin.application.exception.PronunciationDeletionNotAllowedException; import com.dnd.spaced.core.admin.application.exception.PronunciationNotFoundException; import com.dnd.spaced.core.admin.application.exception.WordExampleDeletionNotAllowedException; import com.dnd.spaced.core.admin.application.exception.WordExampleNotFoundException; -import com.dnd.spaced.core.word.application.event.dto.PersistedWordEvent; import com.dnd.spaced.core.word.application.exception.WordNotFoundException; import com.dnd.spaced.core.word.domain.Pronunciation; import com.dnd.spaced.core.word.domain.Word; @@ -15,16 +11,13 @@ import com.dnd.spaced.core.word.domain.repository.PronunciationRepository; import com.dnd.spaced.core.word.domain.repository.WordExampleRepository; import com.dnd.spaced.core.word.domain.repository.WordRepository; -import java.util.ArrayList; -import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -public class AdminWordService { +class DeleteWordService { private static final long WORD_EXAMPLE_MIN_COUNT = 1L; private static final long PRONUNCIATION_MIN_COUNT = 1L; @@ -32,33 +25,20 @@ public class AdminWordService { private final WordRepository wordRepository; private final WordExampleRepository wordExampleRepository; private final PronunciationRepository pronunciationRepository; - private final ApplicationEventPublisher eventPublisher; @Transactional - public Long createWord(CreateWordRequest createWordRequest) { - Word word = buildWordFromRequest(createWordRequest); - Word savedWord = wordRepository.save(word); - - persistExamples(savedWord, createWordRequest); - persistPronunciations(savedWord, createWordRequest); - publishPersistedEvent(savedWord); - - return savedWord.getId(); - } - - @Transactional - public void updateWordExample(Long wordExampleId, String example) { - WordExample wordExample = findWordExample(wordExampleId); + public void deleteWord(Long wordId) { + Word word = findWord(wordId); - wordExample.changeExample(example); + executeWordDeletion(word); } @Transactional public void deleteWordExample(Long wordId, Long wordExampleId) { WordExample wordExample = findWordExample(wordExampleId); - validateExampleCount(wordId); - wordExample.deleted(); + validateWordExampleCount(wordId); + executeWordExampleDeletion(wordExample); } @Transactional @@ -66,15 +46,7 @@ public void deletePronunciation(Long wordId, Long pronunciationId) { Pronunciation pronunciation = findPronunciation(pronunciationId); validatePronunciationCount(wordId); - pronunciation.deleted(); - } - - @Transactional - public void deleteWord(Long wordId) { - Word word = findWord(wordId); - - word.delete(); - publishDeletedWordEvent(wordId); + executePronunciationDeletion(pronunciation); } private Word findWord(Long wordId) { @@ -82,43 +54,6 @@ private Word findWord(Long wordId) { .orElseThrow(() -> new WordNotFoundException("지정한 용어를 찾을 수 없습니다.")); } - private Word buildWordFromRequest(CreateWordRequest request) { - return Word.builder() - .name(request.name()) - .meaning(request.meaning()) - .categoryName(request.categoryName()) - .build(); - } - - private void persistExamples(Word word, CreateWordRequest request) { - List wordExamples = new ArrayList<>(); - - for (String example : request.examples()) { - WordExample wordExample = WordExample.from(example); - - wordExample.initWord(word); - wordExamples.add(wordExample); - } - - wordExampleRepository.saveAll(wordExamples); - } - - private void persistPronunciations(Word word, CreateWordRequest request) { - List pronunciations = new ArrayList<>(); - - for (CreatePronunciationRequest pronunciationInfo : request.pronunciations()) { - Pronunciation pronunciation = Pronunciation.of( - pronunciationInfo.pronunciation(), - pronunciationInfo.typeName() - ); - - pronunciation.initWord(word); - pronunciations.add(pronunciation); - } - - pronunciationRepository.saveAll(pronunciations); - } - private WordExample findWordExample(Long wordExampleId) { return wordExampleRepository.findBy(wordExampleId) .orElseThrow(() -> new WordExampleNotFoundException( @@ -133,7 +68,7 @@ private Pronunciation findPronunciation(Long pronunciationId) { ); } - private void validateExampleCount(Long wordId) { + private void validateWordExampleCount(Long wordId) { if (wordExampleRepository.countBy(wordId) <= WORD_EXAMPLE_MIN_COUNT) { throw new WordExampleDeletionNotAllowedException("해당 용어의 예문 개수가 최소치입니다."); } @@ -145,11 +80,15 @@ private void validatePronunciationCount(Long wordId) { } } - private void publishDeletedWordEvent(Long wordId) { - eventPublisher.publishEvent(new DeletedWordEvent(wordId)); + private void executeWordDeletion(Word word) { + word.delete(); } - private void publishPersistedEvent(Word word) { - eventPublisher.publishEvent(new PersistedWordEvent(word.getId(), word.getCategory())); + private void executeWordExampleDeletion(WordExample wordExample) { + wordExample.deleted(); + } + + private void executePronunciationDeletion(Pronunciation pronunciation) { + pronunciation.deleted(); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/UpdateWordService.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/UpdateWordService.java new file mode 100644 index 00000000..0eb2f700 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/UpdateWordService.java @@ -0,0 +1,33 @@ +package com.dnd.spaced.core.admin.application; + +import com.dnd.spaced.core.admin.application.exception.WordExampleNotFoundException; +import com.dnd.spaced.core.word.domain.WordExample; +import com.dnd.spaced.core.word.domain.repository.WordExampleRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +class UpdateWordService { + + private final WordExampleRepository wordExampleRepository; + + @Transactional + public void updateWordExample(Long wordExampleId, String example) { + WordExample wordExample = findWordExample(wordExampleId); + + executeWordExampleUpdate(example, wordExample); + } + + private WordExample findWordExample(Long wordExampleId) { + return wordExampleRepository.findBy(wordExampleId) + .orElseThrow(() -> new WordExampleNotFoundException( + "지정한 용어 예문을 찾을 수 없습니다.") + ); + } + + private void executeWordExampleUpdate(String example, WordExample wordExample) { + wordExample.changeExample(example); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/mapper/ReportInfoMapper.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/mapper/ReportResponseMapper.java similarity index 71% rename from space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/mapper/ReportInfoMapper.java rename to space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/mapper/ReportResponseMapper.java index 3b550789..b7c22a95 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/mapper/ReportInfoMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/mapper/ReportResponseMapper.java @@ -3,26 +3,25 @@ import com.dnd.spaced.core.admin.application.dto.resposne.ReportCollectionResponse; import com.dnd.spaced.core.admin.application.dto.resposne.ReportCollectionResponse.ReportResponse; import com.dnd.spaced.core.report.domain.Report; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class ReportInfoMapper { +@Mapper +public class ReportResponseMapper { - public static ReportCollectionResponse toDto(List reports) { + public ReportCollectionResponse toDto(List reports) { if (reports.isEmpty()) { return new ReportCollectionResponse(List.of(), null); } List reportResponses = reports.stream() - .map(ReportInfoMapper::toReportDto) + .map(this::toReportDto) .toList(); return new ReportCollectionResponse(reportResponses, reports.get(reports.size() - 1).getId()); } - private static ReportResponse toReportDto(Report report) { + private ReportResponse toReportDto(Report report) { return new ReportResponse( report.getId(), report.getCommentId(), diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/resposne/PersistWordDto.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/resposne/PersistWordDto.java new file mode 100644 index 00000000..9d278d07 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/dto/resposne/PersistWordDto.java @@ -0,0 +1,6 @@ +package com.dnd.spaced.core.admin.application.dto.resposne; + +import com.dnd.spaced.core.word.domain.enums.Category; + +public record PersistWordDto(Long id, Category category) { +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminReportEventListener.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminReportEventListener.java index 9a6666bb..a3188655 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminReportEventListener.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminReportEventListener.java @@ -31,7 +31,7 @@ private Comment findComment(ProcessedReportEvent event) { } private void postProcessReportBy(ReportStatus reportStatus, Comment comment) { - if (reportStatus.isProcess()) { + if (reportStatus.isProcessed()) { comment.delete(); return; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListener.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListener.java index 28d42350..53c81fc6 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListener.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListener.java @@ -1,7 +1,7 @@ package com.dnd.spaced.core.admin.application.event.listener; import com.dnd.spaced.core.admin.application.event.dto.DeletedWordEvent; -import com.dnd.spaced.core.word.application.DeletedWordIdRepository; +import com.dnd.spaced.core.word.application.repository.DeletedWordIdRepository; import com.dnd.spaced.core.word.domain.repository.PronunciationRepository; import com.dnd.spaced.core.word.domain.repository.WordExampleRepository; import java.time.Clock; diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/schedule/CreateTodayQuizScheduler.java b/space-d/src/main/java/com/dnd/spaced/core/admin/application/schedule/CreateTodayQuizScheduler.java similarity index 84% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/application/schedule/CreateTodayQuizScheduler.java rename to space-d/src/main/java/com/dnd/spaced/core/admin/application/schedule/CreateTodayQuizScheduler.java index 9587af8d..8773d55a 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/schedule/CreateTodayQuizScheduler.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/application/schedule/CreateTodayQuizScheduler.java @@ -1,19 +1,19 @@ -package com.dnd.spaced.core.quiz.application.schedule; +package com.dnd.spaced.core.admin.application.schedule; import com.dnd.spaced.core.admin.application.exception.WordMetadataNotFoundException; -import com.dnd.spaced.core.quiz.application.enums.QuizWordCountValidator; import com.dnd.spaced.core.quiz.application.event.dto.AddedTodayQuizQuestionEvent; import com.dnd.spaced.core.quiz.application.exception.InvalidTodayQuizWordCountException; import com.dnd.spaced.core.quiz.domain.TodayQuiz; import com.dnd.spaced.core.quiz.domain.TodayQuizOption; -import com.dnd.spaced.core.quiz.domain.dto.mapper.TodayQuizInfoMapper; +import com.dnd.spaced.core.quiz.domain.dto.mapper.TodayQuizDtoMapper; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizAnswerOption; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizQuestion; import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.quiz.domain.repository.TodayQuizOptionRepository; import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; +import com.dnd.spaced.core.quiz.domain.service.QuizWordCountValidator; import com.dnd.spaced.core.word.domain.WordMetadata; -import com.dnd.spaced.core.word.domain.dto.SimpleWordInfo; +import com.dnd.spaced.core.word.domain.dto.SimpleWord; import com.dnd.spaced.core.word.domain.repository.WordMetadataRepository; import com.dnd.spaced.core.word.domain.repository.WordRandomRepository; import com.dnd.spaced.global.config.properties.QuizQuestionProperties; @@ -43,6 +43,7 @@ public class CreateTodayQuizScheduler { private final TodayQuizOptionRepository todayQuizOptionRepository; private final QuizQuestionProperties quizQuestionProperties; private final CacheManager memoryCacheManager; + private final TodayQuizDtoMapper mapper; private final ApplicationEventPublisher eventPublisher; @Transactional @@ -58,7 +59,7 @@ public void schedule() { } private TodayQuiz createTodayQuiz(QuizCategory quizCategory) { - List randomWords = findRandomWords(quizCategory); + List randomWords = findRandomWords(quizCategory); TodayQuiz todayQuiz = initTodayQuiz(quizCategory, randomWords); TodayQuiz savedTodayQuiz = todayQuizRepository.save(todayQuiz); @@ -67,12 +68,12 @@ private TodayQuiz createTodayQuiz(QuizCategory quizCategory) { return savedTodayQuiz; } - private List findRandomWords(QuizCategory quizCategory) { + private List findRandomWords(QuizCategory quizCategory) { return wordRandomRepository.findRandomAllBy(quizCategory, REQUIRED_QUIZ_WORD_COUNT); } - private TodayQuiz initTodayQuiz(QuizCategory quizCategory, List randomWords) { - SimpleWordInfo answerWord = randomWords.get(ANSWER_OPTION_INDEX); + private TodayQuiz initTodayQuiz(QuizCategory quizCategory, List randomWords) { + SimpleWord answerWord = randomWords.get(ANSWER_OPTION_INDEX); TodayQuizAnswerOption todayQuizAnswerOption = new TodayQuizAnswerOption( answerWord.id(), answerWord.name() @@ -87,12 +88,12 @@ private TodayQuiz initTodayQuiz(QuizCategory quizCategory, List return new TodayQuiz(todayQuizQuestion); } - private void persistTodayQuizOptions(List randomWords, TodayQuiz todayQuiz) { + private void persistTodayQuizOptions(List randomWords, TodayQuiz todayQuiz) { Collections.shuffle(randomWords); List todayQuizOptions = new ArrayList<>(); for (int i = 0; i < randomWords.size(); i++) { - SimpleWordInfo word = randomWords.get(i); + SimpleWord word = randomWords.get(i); TodayQuizOption todayQuizOption = TodayQuizOption.of(word.id(), word.name(), i, todayQuiz); todayQuizOptions.add(todayQuizOption); @@ -110,8 +111,9 @@ private void validateQuizCreation(QuizCategory quizCategory) { .orElseThrow(() -> new WordMetadataNotFoundException( "용어 메타데이터가 정상적으로 설정되지 않았습니다.") ); + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); - if (QuizWordCountValidator.isInvalidate(quizCategory, wordMetadata, REQUIRED_QUIZ_WORD_COUNT)) { + if (quizWordCountValidator.isInvalidate(quizCategory, wordMetadata, REQUIRED_QUIZ_WORD_COUNT)) { throw new InvalidTodayQuizWordCountException("오늘의 퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); } } @@ -121,7 +123,7 @@ private void persistMemoryCache(TodayQuiz todayQuiz) { if (cache != null) { cache.clear(); - cache.put(CacheConst.TODAY_QUIZ_CACHE_NAME, TodayQuizInfoMapper.toDto(todayQuiz)); + cache.put(CacheConst.TODAY_QUIZ_CACHE_NAME, mapper.toDto(todayQuiz)); } } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizController.java b/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizController.java index 0e654bf2..bc676a88 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizController.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.admin.presentation; -import com.dnd.spaced.core.admin.application.AdminTodayQuizService; +import com.dnd.spaced.core.admin.application.AdminTodayQuizServiceFacade; import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -14,11 +14,11 @@ @RequiredArgsConstructor public class AdminTodayQuizController { - private final AdminTodayQuizService adminTodayQuizService; + private final AdminTodayQuizServiceFacade adminTodayQuizServiceFacade; @PostMapping public ResponseEntity createTodayQuiz() { - Long todayQuizId = adminTodayQuizService.createTodayQuiz(); + Long todayQuizId = adminTodayQuizServiceFacade.createTodayQuiz(); URI location = UriComponentsBuilder.fromPath("/today-quizzes/{todayQuizId}") .buildAndExpand(todayQuizId) .toUri(); diff --git a/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminWordController.java b/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminWordController.java index 083a985c..793da365 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminWordController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/admin/presentation/AdminWordController.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.admin.presentation; -import com.dnd.spaced.core.admin.application.AdminWordService; +import com.dnd.spaced.core.admin.application.AdminWordServiceFacade; import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; import com.dnd.spaced.core.admin.application.dto.request.UpdateWordExampleRequest; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; @@ -22,11 +22,11 @@ @RequiredArgsConstructor public class AdminWordController { - private final AdminWordService adminWordService; + private final AdminWordServiceFacade adminWordServiceFacade; @PostMapping public ResponseEntity createWord(@Valid @RequestBody CreateWordRequest request) { - Long wordId = adminWordService.createWord(request); + Long wordId = adminWordServiceFacade.createWord(request); URI location = UriComponentsBuilder.fromPath("/words/{wordId}") .buildAndExpand(wordId) .toUri(); @@ -40,28 +40,28 @@ public ResponseEntity updateWordExample( @PathVariable Long wordExampleId, @Valid @RequestBody UpdateWordExampleRequest request ) { - adminWordService.updateWordExample(wordExampleId, request.content()); + adminWordServiceFacade.updateWordExample(wordExampleId, request.content()); return ResponseEntityConst.NO_CONTENT; } @DeleteMapping("/{wordId}") public ResponseEntity deleteWord(@PathVariable Long wordId) { - adminWordService.deleteWord(wordId); + adminWordServiceFacade.deleteWord(wordId); return ResponseEntityConst.NO_CONTENT; } @DeleteMapping("/{wordId}/examples/{wordExampleId}") public ResponseEntity deleteWordExample(@PathVariable Long wordId, @PathVariable Long wordExampleId) { - adminWordService.deleteWordExample(wordId, wordExampleId); + adminWordServiceFacade.deleteWordExample(wordId, wordExampleId); return ResponseEntityConst.NO_CONTENT; } @DeleteMapping("/{wordId}/pronunciations/{pronunciationId}") public ResponseEntity deletePronunciation(@PathVariable Long wordId, @PathVariable Long pronunciationId) { - adminWordService.deletePronunciation(wordId, pronunciationId); + adminWordServiceFacade.deletePronunciation(wordId, pronunciationId); return ResponseEntityConst.NO_CONTENT; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/GenerateTokenService.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/GenerateTokenService.java similarity index 95% rename from space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/GenerateTokenService.java rename to space-d/src/main/java/com/dnd/spaced/core/auth/application/GenerateTokenService.java index 8d1df029..707f5375 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/GenerateTokenService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/GenerateTokenService.java @@ -1,4 +1,4 @@ -package com.dnd.spaced.core.auth.application.internal; +package com.dnd.spaced.core.auth.application; import com.dnd.spaced.core.auth.application.dto.response.TokenDto; import com.dnd.spaced.core.auth.domain.enums.TokenScheme; diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/InitAccountCareerInfoService.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/InitAccountCareerService.java similarity index 66% rename from space-d/src/main/java/com/dnd/spaced/core/auth/application/InitAccountCareerInfoService.java rename to space-d/src/main/java/com/dnd/spaced/core/auth/application/InitAccountCareerService.java index e88ef11c..606e4a93 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/InitAccountCareerInfoService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/InitAccountCareerService.java @@ -2,7 +2,7 @@ import com.dnd.spaced.core.account.domain.Account; import com.dnd.spaced.core.account.domain.repository.AccountRepository; -import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerInfoRequest; +import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerRequest; import com.dnd.spaced.core.auth.application.exception.ForbiddenInitCareerInfoException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -10,18 +10,18 @@ @Service @RequiredArgsConstructor -public class InitAccountCareerInfoService { +public class InitAccountCareerService { private final AccountRepository accountRepository; @Transactional - public void initCareerInfo(Long accountId, InitAccountCareerInfoRequest request) { - Account account = findPreInitializationAccount(accountId); + public void initCareer(Long accountId, InitAccountCareerRequest request) { + Account account = findPreInitAccount(accountId); - account.changeCareerInfo(request.jobGroupName(), request.companyName(), request.experienceName()); + executeCareerInit(request, account); } - private Account findPreInitializationAccount(Long accountId) { + private Account findPreInitAccount(Long accountId) { return accountRepository.findPreInitializationAccountBy(accountId) .orElseThrow( () -> new ForbiddenInitCareerInfoException( @@ -29,4 +29,8 @@ private Account findPreInitializationAccount(Long accountId) { ) ); } + + private void executeCareerInit(InitAccountCareerRequest request, Account account) { + account.changeCareer(request.jobGroupName(), request.companyName(), request.experienceName()); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/LoginService.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/LoginService.java new file mode 100644 index 00000000..ea5fa24b --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/LoginService.java @@ -0,0 +1,36 @@ +package com.dnd.spaced.core.auth.application; + +import com.dnd.spaced.core.account.domain.Account; +import com.dnd.spaced.core.account.domain.embed.Social; +import com.dnd.spaced.core.account.domain.enums.RegistrationId; +import com.dnd.spaced.core.account.domain.repository.AccountRepository; +import com.dnd.spaced.core.auth.application.dto.response.LoggedInAccountDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoginService { + + private final SignUpService signUpService; + private final AccountRepository accountRepository; + + public LoggedInAccountDto login(String registrationIdName, String socialIdentifier) { + RegistrationId registrationId = RegistrationId.findBy(registrationIdName); + Social social = new Social(registrationId, socialIdentifier); + + return accountRepository.findBy(social) + .map(this::buildLoggedInAccount) + .orElseGet(() -> buildSignUpAccount(registrationId, socialIdentifier)); + } + + private LoggedInAccountDto buildLoggedInAccount(Account account) { + return new LoggedInAccountDto(account.getId(), account.getRole().name(), false); + } + + private LoggedInAccountDto buildSignUpAccount(RegistrationId registrationId, String socialIdentifier) { + Account signedUpAccount = signUpService.signUp(registrationId, socialIdentifier); + + return new LoggedInAccountDto(signedUpAccount.getId(), signedUpAccount.getRole().name(), true); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/RefreshTokenService.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/RefreshTokenService.java index e8411e6b..3480227a 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/RefreshTokenService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/RefreshTokenService.java @@ -4,14 +4,12 @@ import com.dnd.spaced.core.auth.application.exception.BlockedTokenException; import com.dnd.spaced.core.auth.application.exception.ExpiredTokenException; import com.dnd.spaced.core.auth.application.exception.RotationRefreshTokenMismatchException; -import com.dnd.spaced.core.auth.application.internal.GenerateTokenService; import com.dnd.spaced.core.auth.domain.PrivateClaims; import com.dnd.spaced.core.auth.domain.TokenDecoder; import com.dnd.spaced.core.auth.domain.enums.TokenType; import com.dnd.spaced.core.auth.domain.repository.RefreshTokenRotationRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -22,18 +20,13 @@ public class RefreshTokenService { private final BlacklistTokenService blacklistTokenService; private final RefreshTokenRotationRepository refreshTokenRotationRepository; - @Transactional public TokenDto refreshToken(String refreshToken) { PrivateClaims privateClaims = convertTokenPrivateClaims(refreshToken); validateBlacklistToken(privateClaims); validateRotationRefreshToken(refreshToken, privateClaims); - TokenDto tokenDto = generateTokenService.generate(privateClaims.accountId(), privateClaims.roleName()); - - refreshTokenRotationRepository.save(privateClaims.accountId(), tokenDto.refreshToken()); - - return tokenDto; + return generateRefreshToken(privateClaims); } private PrivateClaims convertTokenPrivateClaims(String refreshToken) { @@ -68,4 +61,11 @@ private void validateRefreshToken(String refreshToken, PrivateClaims privateClai throw new RotationRefreshTokenMismatchException("기존 Refresh Token과 일치하지 않습니다."); } } + + private TokenDto generateRefreshToken(PrivateClaims privateClaims) { + TokenDto tokenDto = generateTokenService.generate(privateClaims.accountId(), privateClaims.roleName()); + + refreshTokenRotationRepository.save(privateClaims.accountId(), tokenDto.refreshToken()); + return tokenDto; + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/SignUpService.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/SignUpService.java similarity index 63% rename from space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/SignUpService.java rename to space-d/src/main/java/com/dnd/spaced/core/auth/application/SignUpService.java index 9c629be2..89561d25 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/SignUpService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/SignUpService.java @@ -1,4 +1,4 @@ -package com.dnd.spaced.core.auth.application.internal; +package com.dnd.spaced.core.auth.application; import com.dnd.spaced.core.account.domain.Account; import com.dnd.spaced.core.account.domain.NicknameMetadata; @@ -18,7 +18,7 @@ @Service @RequiredArgsConstructor @Transactional -public class SignUpService { +class SignUpService { private static final Role DEFAULT_ROLE = Role.ROLE_USER; @@ -27,18 +27,18 @@ public class SignUpService { private final NicknameMetadataRepository nicknameMetadataRepository; private final ApplicationEventPublisher eventPublisher; - Account signUp(RegistrationId registrationId, String socialIdentifier) { + public Account signUp(RegistrationId registrationId, String socialIdentifier) { String profileImageName = findRandomProfileImage(); String formattedNickname = formatNickname(); - Account persistedAccount = persistAccount( + Account account = setupAccount( registrationId, socialIdentifier, formattedNickname, profileImageName ); - eventPublisher.publishEvent(new InitializedAccountEvent(persistedAccount.getId())); - return persistedAccount; + publishSignedUpAccountEvent(account); + return account; } private String findRandomProfileImage() { @@ -49,7 +49,8 @@ private String findRandomProfileImage() { private String formatNickname() { String nickname = nicknameProperties.generate(); NicknameMetadata metadata = nicknameMetadataRepository.findBy(nickname) - .orElseThrow(() -> new NicknameMetadataNotFoundException("닉네임 메타데이터가 정상적으로 초기화되지 않았습니다.")); + .orElseThrow(() -> new NicknameMetadataNotFoundException( + "닉네임 메타데이터가 정상적으로 초기화되지 않았습니다.")); metadata.addCount(); return nicknameProperties.format( @@ -58,20 +59,34 @@ private String formatNickname() { ); } - private Account persistAccount( + private Account setupAccount( RegistrationId registrationId, String socialIdentifier, String formattedNickname, String profileImageName ) { - Account newAccount = Account.builder() - .registrationId(registrationId) - .socialIdentifier(socialIdentifier) - .nickname(formattedNickname) - .role(DEFAULT_ROLE) - .profileImage(profileImageName) - .build(); + Account newAccount = buildAccount(registrationId, socialIdentifier, formattedNickname, + profileImageName); + return persistAccount(newAccount); + } + + private Account buildAccount(RegistrationId registrationId, String socialIdentifier, String formattedNickname, + String profileImageName) { + return Account.builder() + .registrationId(registrationId) + .socialIdentifier(socialIdentifier) + .nickname(formattedNickname) + .role(DEFAULT_ROLE) + .profileImageName(ProfileImageName.findByImageName(profileImageName)) + .build(); + } + + private Account persistAccount(Account newAccount) { return accountRepository.save(newAccount); } + + private void publishSignedUpAccountEvent(Account account) { + eventPublisher.publishEvent(new InitializedAccountEvent(account.getId())); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/request/InitAccountCareerInfoRequest.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/request/InitAccountCareerRequest.java similarity index 85% rename from space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/request/InitAccountCareerInfoRequest.java rename to space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/request/InitAccountCareerRequest.java index f1dfd565..40f53961 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/request/InitAccountCareerInfoRequest.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/request/InitAccountCareerRequest.java @@ -2,7 +2,7 @@ import jakarta.validation.constraints.NotBlank; -public record InitAccountCareerInfoRequest( +public record InitAccountCareerRequest( @NotBlank String jobGroupName, diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/response/LoggedInAccountDto.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/response/LoggedInAccountDto.java new file mode 100644 index 00000000..e8de14f4 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/response/LoggedInAccountDto.java @@ -0,0 +1,4 @@ +package com.dnd.spaced.core.auth.application.dto.response; + +public record LoggedInAccountDto(Long id, String roleName, boolean isSignUp) { +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/response/LoggedInAccountInfoDto.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/response/LoggedInAccountInfoDto.java deleted file mode 100644 index 1347afec..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/dto/response/LoggedInAccountInfoDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.dnd.spaced.core.auth.application.dto.response; - -public record LoggedInAccountInfoDto(Long id, String roleName, boolean isSignUp) { -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/LoginService.java b/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/LoginService.java deleted file mode 100644 index 2e0152b2..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/application/internal/LoginService.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.dnd.spaced.core.auth.application.internal; - -import com.dnd.spaced.core.account.domain.Account; -import com.dnd.spaced.core.account.domain.enums.RegistrationId; -import com.dnd.spaced.core.account.domain.repository.AccountRepository; -import com.dnd.spaced.core.auth.application.dto.response.LoggedInAccountInfoDto; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class LoginService { - - private final AccountRepository accountRepository; - private final SignUpService signUpService; - - public LoggedInAccountInfoDto login(String registrationIdName, String socialIdentifier) { - RegistrationId registrationId = RegistrationId.findBy(registrationIdName); - - return accountRepository.findBy(registrationId, socialIdentifier) - .map(account -> - new LoggedInAccountInfoDto( - account.getId(), - account.getRole().name(), - false) - ) - .orElseGet(() -> { - Account signedUpAccount = signUpService.signUp(registrationId, socialIdentifier); - - return new LoggedInAccountInfoDto( - signedUpAccount.getId(), - signedUpAccount.getRole().name(), - true - ); - }); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtDecoder.java b/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtDecoder.java index 96814e51..7ba43015 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtDecoder.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtDecoder.java @@ -47,29 +47,47 @@ private void validateToken(String token) { private Optional parse(TokenType tokenType, String token) { try { - JWEObject jweObject = JWEObject.parse(token); + return extractClaimsSet(tokenType, token); + } catch (JOSEException e) { + throw new FailedDecodeTokenException("토큰 디코딩에 실패했습니다", e); + } catch (ParseException e) { + throw new InvalidTokenException("유효한 토큰이 아닙니다.", e); + } + } - jweObject.decrypt(jweDecrypter); + private Optional extractClaimsSet(TokenType tokenType, String token) + throws ParseException, JOSEException { + JWTClaimsSet claimsSet = findJWTClaimsSet(tokenType, token); - SignedJWT signedJwt = jweObject.getPayload() - .toSignedJWT(); - JWSVerifier jwsVerifier = jwsVerifierFinder.findByTokenType(tokenType); + validateIssuer(claimsSet); - validateSign(signedJwt, jwsVerifier); + return findClaimsSet(claimsSet); + } - JWTClaimsSet claims = signedJwt.getJWTClaimsSet(); + private JWTClaimsSet findJWTClaimsSet(TokenType tokenType, String token) throws ParseException, JOSEException { + JWEObject jweObject = findJWEObject(token); + SignedJWT signedJwt = findSignedJWT(jweObject); + JWSVerifier jwsVerifier = findJWSVerifier(tokenType); - validateIssuer(claims.getIssuer()); - if (isExpiredToken(claims.getExpirationTime())) { - return Optional.empty(); - } + validateSign(signedJwt, jwsVerifier); - return Optional.of(claims); - } catch (JOSEException e) { - throw new FailedDecodeTokenException("토큰 디코딩에 실패했습니다", e); - } catch (ParseException e) { - throw new InvalidTokenException("유효한 토큰이 아닙니다.", e); - } + return signedJwt.getJWTClaimsSet(); + } + + private JWEObject findJWEObject(String token) throws ParseException, JOSEException { + JWEObject jweObject = JWEObject.parse(token); + + jweObject.decrypt(jweDecrypter); + return jweObject; + } + + private SignedJWT findSignedJWT(JWEObject jweObject) { + return jweObject.getPayload() + .toSignedJWT(); + } + + private JWSVerifier findJWSVerifier(TokenType tokenType) { + return jwsVerifierFinder.findByTokenType(tokenType); } private void validateSign(SignedJWT signedJwt, JWSVerifier jwsVerifier) throws JOSEException { @@ -85,12 +103,20 @@ private boolean isExpiredToken(Date expirationTime) { return expirationDate.isBefore(now); } - private void validateIssuer(String issuer) { - if (!tokenProperties.issuer().equals(issuer)) { + private void validateIssuer(JWTClaimsSet claimsSet) { + if (!tokenProperties.issuer().equals(claimsSet.getIssuer())) { throw new InvalidTokenException("서비스에서 발급한 토큰이 아닙니다."); } } + private Optional findClaimsSet(JWTClaimsSet claimsSet) { + if (isExpiredToken(claimsSet.getExpirationTime())) { + return Optional.empty(); + } + + return Optional.of(claimsSet); + } + private PrivateClaims convert(JWTClaimsSet claims) { Date issueTime = claims.getIssueTime(); diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtEncoder.java b/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtEncoder.java index 082cf166..f9a15b58 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtEncoder.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/infrastructure/jwt/JwtEncoder.java @@ -37,13 +37,7 @@ public class JwtEncoder implements TokenEncoder { @Override public String encode(LocalDateTime publishTime, TokenType tokenType, Long accountId, String roleName) { try { - JWEHeader header = createJweHeader(); - JWTClaimsSet claims = createJwtPayload(tokenType, accountId, roleName, publishTime); - SignedJWT signedJwt = createSignedJwt(claims, tokenType); - JWEObject jweObject = createJweObject(header, signedJwt); - - jweObject.encrypt(jweEncrypter); - return jweObject.serialize(); + return serializeToken(publishTime, tokenType, accountId, roleName); } catch (KeyLengthException e) { throw new FailedEncodeTokenException("키 길이를 지원하지 않는 환경입니다.", e); } catch (JOSEException e) { @@ -51,6 +45,16 @@ public String encode(LocalDateTime publishTime, TokenType tokenType, Long accoun } } + private String serializeToken(LocalDateTime publishTime, TokenType tokenType, Long accountId, String roleName) + throws JOSEException { + JWEHeader header = createJweHeader(); + JWTClaimsSet claims = createJwtPayload(tokenType, accountId, roleName, publishTime); + SignedJWT signedJwt = setupSignedJwt(claims, tokenType); + JWEObject jweObject = setupJweObject(header, signedJwt); + + return jweObject.serialize(); + } + private JWEHeader createJweHeader() { return new JWEHeader.Builder(JWEAlgorithm.A256KW, EncryptionMethod.A256GCM) .contentType(TOKEN_CONTENT_TYPE) @@ -74,7 +78,7 @@ private JWTClaimsSet createJwtPayload( .build(); } - private SignedJWT createSignedJwt(JWTClaimsSet claims, TokenType tokenType) throws JOSEException { + private SignedJWT setupSignedJwt(JWTClaimsSet claims, TokenType tokenType) throws JOSEException { JWSHeader jwsHeader = new Builder(JWSAlgorithm.HS256).build(); SignedJWT signedJwt = new SignedJWT(jwsHeader, claims); @@ -82,8 +86,12 @@ private SignedJWT createSignedJwt(JWTClaimsSet claims, TokenType tokenType) thro return signedJwt; } - private JWEObject createJweObject(JWEHeader header, SignedJWT signedJwt) { - return new JWEObject(header, new Payload(signedJwt)); + private JWEObject setupJweObject(JWEHeader header, SignedJWT signedJwt) throws JOSEException { + Payload payload = new Payload(signedJwt); + JWEObject jweObject = new JWEObject(header, payload); + + jweObject.encrypt(jweEncrypter); + return jweObject; } private Date convertDate(LocalDateTime target) { diff --git a/space-d/src/main/java/com/dnd/spaced/core/auth/presentation/AuthController.java b/space-d/src/main/java/com/dnd/spaced/core/auth/presentation/AuthController.java index 4be383cd..cceb798e 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/auth/presentation/AuthController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/auth/presentation/AuthController.java @@ -1,13 +1,13 @@ package com.dnd.spaced.core.auth.presentation; -import com.dnd.spaced.core.auth.application.InitAccountCareerInfoService; +import com.dnd.spaced.core.auth.application.InitAccountCareerService; import com.dnd.spaced.core.auth.application.RefreshTokenService; -import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerInfoRequest; +import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerRequest; import com.dnd.spaced.core.auth.application.dto.response.TokenDto; import com.dnd.spaced.core.auth.presentation.dto.response.AccessTokenResponse; import com.dnd.spaced.core.auth.presentation.exception.RefreshTokenNotFoundException; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; import com.dnd.spaced.global.config.properties.TokenProperties; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; import jakarta.servlet.http.Cookie; @@ -35,14 +35,14 @@ public class AuthController { private final TokenProperties tokenProperties; private final RefreshTokenService refreshTokenService; - private final InitAccountCareerInfoService initAccountCareerInfoService; + private final InitAccountCareerService initAccountCareerService; @PostMapping("/profile") - public ResponseEntity initAccountCareerInfo( - @CurrentAccountInfo AuthAccountInfo accountInfo, - @Valid @RequestBody InitAccountCareerInfoRequest request + public ResponseEntity initAccountCareer( + @CurrentAccount AuthAccountId accountId, + @Valid @RequestBody InitAccountCareerRequest request ) { - initAccountCareerInfoService.initCareerInfo(accountInfo.accountId(), request); + initAccountCareerService.initCareer(accountId.id(), request); return ResponseEntityConst.NO_CONTENT; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/BookmarkService.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/BookmarkService.java deleted file mode 100644 index cdcaeba1..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/BookmarkService.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.dnd.spaced.core.bookmark.application; - -import com.dnd.spaced.core.bookmark.application.dto.mapper.BookmarkApplicationMapper; -import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; -import com.dnd.spaced.core.bookmark.application.dto.request.DeleteBookmarkRequest; -import com.dnd.spaced.core.bookmark.application.dto.request.ReadAllBookmarkRequest; -import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse; -import com.dnd.spaced.core.bookmark.application.exception.AlreadyExistsBookmarkException; -import com.dnd.spaced.core.bookmark.application.exception.BookmarkLockException; -import com.dnd.spaced.core.bookmark.application.exception.WordNotFoundException; -import com.dnd.spaced.core.bookmark.domain.Bookmark; -import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; -import com.dnd.spaced.core.word.application.event.dto.WordBookmarkCountDecrementedEvent; -import com.dnd.spaced.core.word.application.event.dto.WordBookmarkCountIncrementedEvent; -import com.dnd.spaced.core.word.domain.repository.WordRepository; -import java.util.List; -import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; -import org.redisson.api.RLock; -import org.redisson.api.RedissonClient; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionTemplate; - -@Service -@RequiredArgsConstructor -public class BookmarkService { - - private final RedissonClient redissonClient; - private final WordRepository wordRepository; - private final BookmarkRepository bookmarkRepository; - private final TransactionTemplate transactionTemplate; - private final ApplicationEventPublisher eventPublisher; - - public void createBookmark(Long accountId, CreateBookmarkRequest request) { - validateWordId(request); - - transactionTemplate.executeWithoutResult( - transactionStatus -> { - RLock lock = redissonClient.getLock(calculateLockName(accountId, request)); - - try { - boolean acquireLock = lock.tryLock(1, 1, TimeUnit.SECONDS); - - if (!acquireLock) { - return; - } - - validateExistsBookmark(accountId, request); - - Bookmark bookmark = new Bookmark(accountId, request.wordId()); - - bookmarkRepository.save(bookmark); - publishAddedBookmarkEvent(bookmark); - } catch (InterruptedException e) { - throw new BookmarkLockException("북마크 생성 중 인터럽트 발생", e); - } finally { - if (lock.isHeldByCurrentThread()) { - lock.unlock(); - } - } - } - ); - } - - @Transactional - public void deleteBookmark(Long accountId, DeleteBookmarkRequest request) { - bookmarkRepository.delete(accountId, request.wordId()); - publishDeletedBookmarkEvent(request.wordId()); - } - - public BookmarkCollectionResponse readBookmarks( - Long accountId, - ReadAllBookmarkRequest request, - Pageable pageable - ) { - List bookmarks = bookmarkRepository.findAllBy(accountId, request.lastBookmarkId(), pageable); - - return BookmarkApplicationMapper.toDto(bookmarks); - } - - private void validateWordId(CreateBookmarkRequest request) { - if (isExistsWord(request.wordId())) { - throw new WordNotFoundException("지정한 식별자의 용어를 찾지 못했습니다."); - } - } - - private String calculateLockName(Long accountId, CreateBookmarkRequest request) { - return accountId + ":" + request.wordId(); - } - - private boolean isExistsWord(Long wordId) { - return !wordRepository.existsBy(wordId); - } - - private void validateExistsBookmark(Long accountId, CreateBookmarkRequest request) { - if (isExistsBookmark(accountId, request.wordId())) { - throw new AlreadyExistsBookmarkException("이미 북마크에 추가된 용어입니다."); - } - } - - private boolean isExistsBookmark(Long accountId, Long wordId) { - return bookmarkRepository.existsBy(accountId, wordId); - } - - private void publishAddedBookmarkEvent(Bookmark bookmark) { - eventPublisher.publishEvent(new WordBookmarkCountIncrementedEvent(bookmark.getWordId())); - } - - private void publishDeletedBookmarkEvent(Long wordId) { - eventPublisher.publishEvent(new WordBookmarkCountDecrementedEvent(wordId)); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceFacade.java new file mode 100644 index 00000000..3739defc --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceFacade.java @@ -0,0 +1,52 @@ +package com.dnd.spaced.core.bookmark.application; + +import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.dto.request.DeleteBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.dto.request.ReadAllBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse; +import com.dnd.spaced.core.bookmark.domain.Bookmark; +import com.dnd.spaced.core.word.application.event.dto.WordBookmarkCountDecrementedEvent; +import com.dnd.spaced.core.word.application.event.dto.WordBookmarkCountIncrementedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class BookmarkServiceFacade { + + private final CreateBookmarkService createBookmarkService; + private final ReadBookmarkService readBookmarkService; + private final DeleteBookmarkService deleteBookmarkService; + private final ApplicationEventPublisher eventPublisher; + + public void createBookmark(Long accountId, CreateBookmarkRequest request) { + Bookmark bookmark = createBookmarkService.createBookmark(accountId, request); + + publishAddedBookmarkEvent(bookmark); + } + + @Transactional + public void deleteBookmark(Long accountId, DeleteBookmarkRequest request) { + deleteBookmarkService.deleteBookmark(accountId, request); + publishDeletedBookmarkEvent(request.wordId()); + } + + public BookmarkCollectionResponse readBookmarks( + Long accountId, + ReadAllBookmarkRequest request, + Pageable pageable + ) { + return readBookmarkService.readBookmarks(accountId, request, pageable); + } + + private void publishAddedBookmarkEvent(Bookmark bookmark) { + eventPublisher.publishEvent(new WordBookmarkCountIncrementedEvent(bookmark.getWordId())); + } + + private void publishDeletedBookmarkEvent(Long wordId) { + eventPublisher.publishEvent(new WordBookmarkCountDecrementedEvent(wordId)); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/CreateBookmarkService.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/CreateBookmarkService.java new file mode 100644 index 00000000..a95fc7bf --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/CreateBookmarkService.java @@ -0,0 +1,100 @@ +package com.dnd.spaced.core.bookmark.application; + +import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.exception.AlreadyExistsBookmarkException; +import com.dnd.spaced.core.bookmark.application.exception.BookmarkInterruptedException; +import com.dnd.spaced.core.bookmark.application.exception.BookmarkLockException; +import com.dnd.spaced.core.bookmark.application.exception.WordNotFoundException; +import com.dnd.spaced.core.bookmark.domain.Bookmark; +import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; +import com.dnd.spaced.core.word.domain.repository.WordRepository; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; + +@Service +@RequiredArgsConstructor +class CreateBookmarkService { + + private static final int LOCK_WAIT_TIME = 1; + private static final int LOCK_LEASE_TIME = 1; + private static final String LOCK_SEPARATOR = ":"; + private static final String LOCK_PREFIX = "bookmark" + LOCK_SEPARATOR + "create" + LOCK_SEPARATOR; + + private final RedissonClient redissonClient; + private final WordRepository wordRepository; + private final BookmarkRepository bookmarkRepository; + private final TransactionTemplate transactionTemplate; + + public Bookmark createBookmark(Long accountId, CreateBookmarkRequest request) { + validateWordId(request); + return executeBookmarkCreationWithTransaction(accountId, request); + } + + private Bookmark executeBookmarkCreationWithTransaction(Long accountId, CreateBookmarkRequest request) { + RLock lock = redissonClient.getLock(calculateLockName(accountId, request)); + + if (!tryLock(lock)) { + throw new BookmarkLockException("북마크 생성 중 락 획득 실패"); + } + + try { + return transactionTemplate.execute(action -> doBookmarkCreation(accountId, request)); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + private boolean tryLock(RLock lock) { + try { + return lock.tryLock(LOCK_WAIT_TIME, LOCK_LEASE_TIME, TimeUnit.SECONDS); + } catch (RedisException e) { + if (e.getCause() instanceof InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new BookmarkInterruptedException("북마크 생성 중 인터럽트 발생", interruptedException); + } + throw new BookmarkLockException("북마크 생성 중 락 획득 실패", e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BookmarkInterruptedException("북마크 생성 중 인터럽트 발생", e); + } + } + + private Bookmark doBookmarkCreation(Long accountId, CreateBookmarkRequest request) { + validateExistsBookmark(accountId, request); + return persistBookmark(accountId, request); + } + + private String calculateLockName(Long accountId, CreateBookmarkRequest request) { + return LOCK_PREFIX + accountId + LOCK_SEPARATOR + request.wordId(); + } + + private void validateExistsBookmark(Long accountId, CreateBookmarkRequest request) { + if (bookmarkRepository.existsBy(accountId, request.wordId())) { + throw new AlreadyExistsBookmarkException("이미 북마크에 추가된 용어입니다."); + } + } + + private Bookmark persistBookmark(Long accountId, CreateBookmarkRequest request) { + Bookmark bookmark = new Bookmark(accountId, request.wordId()); + + bookmarkRepository.save(bookmark); + return bookmark; + } + + private void validateWordId(CreateBookmarkRequest request) { + if (isMissingWord(request.wordId())) { + throw new WordNotFoundException("지정한 식별자의 용어를 찾지 못했습니다."); + } + } + + private boolean isMissingWord(Long wordId) { + return !wordRepository.existsBy(wordId); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/DeleteBookmarkService.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/DeleteBookmarkService.java new file mode 100644 index 00000000..762a9c63 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/DeleteBookmarkService.java @@ -0,0 +1,19 @@ +package com.dnd.spaced.core.bookmark.application; + +import com.dnd.spaced.core.bookmark.application.dto.request.DeleteBookmarkRequest; +import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +class DeleteBookmarkService { + + private final BookmarkRepository bookmarkRepository; + + @Transactional + public void deleteBookmark(Long accountId, DeleteBookmarkRequest request) { + bookmarkRepository.delete(accountId, request.wordId()); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/ReadBookmarkService.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/ReadBookmarkService.java new file mode 100644 index 00000000..59eff9b5 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/ReadBookmarkService.java @@ -0,0 +1,29 @@ +package com.dnd.spaced.core.bookmark.application; + +import com.dnd.spaced.core.bookmark.application.dto.mapper.BookmarkMapper; +import com.dnd.spaced.core.bookmark.application.dto.request.ReadAllBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse; +import com.dnd.spaced.core.bookmark.domain.Bookmark; +import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class ReadBookmarkService { + + private final BookmarkMapper mapper; + private final BookmarkRepository bookmarkRepository; + + public BookmarkCollectionResponse readBookmarks( + Long accountId, + ReadAllBookmarkRequest request, + Pageable pageable + ) { + List bookmarks = bookmarkRepository.findAllBy(accountId, request.lastBookmarkId(), pageable); + + return mapper.toDto(bookmarks); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/dto/mapper/BookmarkApplicationMapper.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/dto/mapper/BookmarkMapper.java similarity index 73% rename from space-d/src/main/java/com/dnd/spaced/core/bookmark/application/dto/mapper/BookmarkApplicationMapper.java rename to space-d/src/main/java/com/dnd/spaced/core/bookmark/application/dto/mapper/BookmarkMapper.java index eed90c2f..dc2dcbcb 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/dto/mapper/BookmarkApplicationMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/dto/mapper/BookmarkMapper.java @@ -3,25 +3,24 @@ import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse; import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse.BookmarkResponse; import com.dnd.spaced.core.bookmark.domain.Bookmark; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class BookmarkApplicationMapper { +@Mapper +public class BookmarkMapper { - public static BookmarkCollectionResponse toDto(List bookmarks) { + public BookmarkCollectionResponse toDto(List bookmarks) { if (bookmarks.isEmpty()) { return new BookmarkCollectionResponse(List.of(), null); } List bookmarkResponses = bookmarks.stream() - .map(BookmarkApplicationMapper::toBookmarkResponse) + .map(this::toBookmarkResponse) .toList(); return new BookmarkCollectionResponse(bookmarkResponses, bookmarks.get(bookmarks.size() - 1).getId()); } - private static BookmarkResponse toBookmarkResponse(Bookmark bookmark) { + private BookmarkResponse toBookmarkResponse(Bookmark bookmark) { return new BookmarkResponse( bookmark.getId(), bookmark.getAccountId(), diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkInterruptedException.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkInterruptedException.java new file mode 100644 index 00000000..5326d895 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkInterruptedException.java @@ -0,0 +1,11 @@ +package com.dnd.spaced.core.bookmark.application.exception; + +import com.dnd.spaced.global.exception.base.BookmarkServerException; +import com.dnd.spaced.global.exception.code.BookmarkErrorCode; + +public class BookmarkInterruptedException extends BookmarkServerException { + + public BookmarkInterruptedException(String message, Throwable e) { + super(BookmarkErrorCode.BOOKMARK_INTERRUPTED_EXCEPTION, message, e); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkLockException.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkLockException.java index d1263a3d..7e58bd50 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkLockException.java +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/exception/BookmarkLockException.java @@ -5,6 +5,10 @@ public class BookmarkLockException extends BookmarkServerException { + public BookmarkLockException(String message) { + super(BookmarkErrorCode.BOOKMARK_LOCK_EXCEPTION, message); + } + public BookmarkLockException(String message, Throwable e) { super(BookmarkErrorCode.BOOKMARK_LOCK_EXCEPTION, message, e); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkScheduler.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkScheduler.java index d008f0dd..2df2003b 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkScheduler.java +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkScheduler.java @@ -1,13 +1,12 @@ package com.dnd.spaced.core.bookmark.application.schedule; import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; -import com.dnd.spaced.core.word.application.DeletedWordIdRepository; +import com.dnd.spaced.core.word.application.repository.DeletedWordIdRepository; import java.time.Clock; import java.time.LocalDateTime; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.retry.support.RetryTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepository.java index 2dc863a2..bc7ed213 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepository.java @@ -29,11 +29,13 @@ public void save(Bookmark bookmark) { @Override public Optional findBy(Long accountId, Long wordId) { - BookmarkWithWord result = queryFactory.select(Projections.constructor( - BookmarkWithWord.class, - bookmark, - word.deleted - )) + BookmarkWithWord result = queryFactory.select( + Projections.constructor( + BookmarkWithWord.class, + bookmark, + word.deleted + ) + ) .from(bookmark) .leftJoin(word).on(bookmark.wordId.eq(word.id)) .where( @@ -42,7 +44,7 @@ public Optional findBy(Long accountId, Long wordId) { ) .fetchOne(); - if (result == null || result.wordDeleted()) { + if (isDeletedWord(result)) { return Optional.empty(); } @@ -51,11 +53,13 @@ public Optional findBy(Long accountId, Long wordId) { @Override public boolean existsBy(Long accountId, Long wordId) { - BookmarkWithWord result = queryFactory.select(Projections.constructor( - BookmarkWithWord.class, - bookmark, - word.deleted - )) + BookmarkWithWord result = queryFactory.select( + Projections.constructor( + BookmarkWithWord.class, + bookmark, + word.deleted + ) + ) .from(bookmark) .leftJoin(word).on(bookmark.wordId.eq(word.id)) .where( @@ -64,7 +68,7 @@ public boolean existsBy(Long accountId, Long wordId) { ) .fetchOne(); - return result != null && !result.wordDeleted; + return isExistBookmark(result); } @Override @@ -83,11 +87,13 @@ public void deleteAllBy(Set wordId) { @Override public List findAllBy(Long accountId, Long lastBookmarkId, Pageable pageable) { - return queryFactory.select(Projections.constructor( - BookmarkWithWord.class, - bookmark, - word.deleted - )) + return queryFactory.select( + Projections.constructor( + BookmarkWithWord.class, + bookmark, + word.deleted + ) + ) .from(bookmark) .leftJoin(word).on(word.id.eq(bookmark.wordId)) .where(bookmark.accountId.eq(accountId), ltLastBookmarkId(lastBookmarkId)) @@ -95,11 +101,19 @@ public List findAllBy(Long accountId, Long lastBookmarkId, Pageable pa .limit(pageable.getPageSize()) .fetch() .stream() - .filter(BookmarkWithWord::isNotWordDeleted) + .filter(BookmarkWithWord::isValidWord) .map(BookmarkWithWord::bookmark) .toList(); } + private boolean isDeletedWord(BookmarkWithWord result) { + return result == null || result.wordDeleted(); + } + + private boolean isExistBookmark(BookmarkWithWord result) { + return result != null && !result.wordDeleted; + } + private BooleanExpression ltLastBookmarkId(Long lastBookmarkId) { if (lastBookmarkId == null) { return null; @@ -110,7 +124,7 @@ private BooleanExpression ltLastBookmarkId(Long lastBookmarkId) { public record BookmarkWithWord(Bookmark bookmark, boolean wordDeleted) { - public boolean isNotWordDeleted() { + boolean isValidWord() { return !wordDeleted(); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/bookmark/presentation/BookmarkController.java b/space-d/src/main/java/com/dnd/spaced/core/bookmark/presentation/BookmarkController.java index 5623527b..b3c80a44 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/bookmark/presentation/BookmarkController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/bookmark/presentation/BookmarkController.java @@ -1,12 +1,12 @@ package com.dnd.spaced.core.bookmark.presentation; -import com.dnd.spaced.core.bookmark.application.BookmarkService; +import com.dnd.spaced.core.bookmark.application.BookmarkServiceFacade; import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; import com.dnd.spaced.core.bookmark.application.dto.request.DeleteBookmarkRequest; import com.dnd.spaced.core.bookmark.application.dto.request.ReadAllBookmarkRequest; import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; import com.dnd.spaced.global.resolver.bookmark.BookmarkPageable; import jakarta.validation.Valid; @@ -25,35 +25,35 @@ @RequiredArgsConstructor public class BookmarkController { - private final BookmarkService bookmarkService; + private final BookmarkServiceFacade bookmarkServiceFacade; @GetMapping public ResponseEntity readBookmarks( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, ReadAllBookmarkRequest request, @BookmarkPageable Pageable pageable ) { - BookmarkCollectionResponse response = bookmarkService.readBookmarks(accountInfo.accountId(), request, pageable); + BookmarkCollectionResponse response = bookmarkServiceFacade.readBookmarks(accountId.id(), request, pageable); return ResponseEntity.ok(response); } @PostMapping public ResponseEntity createBookmark( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @Valid @RequestBody CreateBookmarkRequest request ) { - bookmarkService.createBookmark(accountInfo.accountId(), request); + bookmarkServiceFacade.createBookmark(accountId.id(), request); return ResponseEntityConst.NO_CONTENT; } @DeleteMapping public ResponseEntity deleteBookmark( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @Valid @RequestBody DeleteBookmarkRequest request ) { - bookmarkService.deleteBookmark(accountInfo.accountId(), request); + bookmarkServiceFacade.deleteBookmark(accountId.id(), request); return ResponseEntityConst.NO_CONTENT; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/CommentService.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/CommentService.java deleted file mode 100644 index 31517051..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/application/CommentService.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.dnd.spaced.core.comment.application; - -import com.dnd.spaced.core.comment.application.dto.mapper.CommentResponseCollectionMapper; -import com.dnd.spaced.core.comment.application.dto.request.CreateCommentRequest; -import com.dnd.spaced.core.comment.application.dto.request.UpdateCommentRequest; -import com.dnd.spaced.core.comment.application.dto.response.CommentCollectionResponse; -import com.dnd.spaced.core.comment.application.exception.CommentNotFoundException; -import com.dnd.spaced.core.comment.application.exception.ForbiddenCommentException; -import com.dnd.spaced.core.comment.application.exception.WordNotFoundException; -import com.dnd.spaced.core.comment.domain.Comment; -import com.dnd.spaced.core.comment.domain.dto.LikedCommentInfo; -import com.dnd.spaced.core.comment.domain.repository.CommentRepository; -import com.dnd.spaced.core.word.domain.repository.WordRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class CommentService { - - private final WordRepository wordRepository; - private final CommentRepository commentRepository; - - @Transactional - public void createComment(Long accountId, Long wordId, CreateCommentRequest request) { - validateWordId(wordId); - - Comment comment = new Comment(accountId, wordId, request.content()); - - commentRepository.save(comment); - } - - @Transactional - public void deleteComment(Long accountId, Long commentId) { - Comment comment = findComment(commentId); - - validateDeleteAuthority(comment, accountId); - comment.delete(); - } - - @Transactional - public void updateComment(Long accountId, Long commentId, UpdateCommentRequest request) { - Comment comment = findComment(commentId); - - validateUpdateAuthority(comment, accountId); - comment.changeContent(request.content()); - } - - public CommentCollectionResponse readComments(Long accountId, Long wordId, Long lastCommentId, Pageable pageable) { - List comments = commentRepository.findAllBy(accountId, wordId, lastCommentId, pageable); - - return CommentResponseCollectionMapper.toCollectionDto(comments); - } - - private void validateWordId(Long wordId) { - if (!wordRepository.existsBy(wordId)) { - throw new WordNotFoundException("댓글과 관련된 용어를 찾을 수 없습니다."); - } - } - - private Comment findComment(Long commentId) { - return commentRepository.findBy(commentId) - .orElseThrow(() -> new CommentNotFoundException("지정한 ID에 해당하는 댓글이 없습니다.")); - } - - private void validateDeleteAuthority(Comment comment, Long accountId) { - if (comment.isNotWriter(accountId)) { - throw new ForbiddenCommentException("댓글을 삭제할 권한이 없습니다."); - } - } - - private void validateUpdateAuthority(Comment comment, Long accountId) { - if (comment.isNotWriter(accountId)) { - throw new ForbiddenCommentException("댓글을 수정할 권한이 없습니다."); - } - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/CommentServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/CommentServiceFacade.java new file mode 100644 index 00000000..0660cdce --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/CommentServiceFacade.java @@ -0,0 +1,44 @@ +package com.dnd.spaced.core.comment.application; + +import com.dnd.spaced.core.comment.application.dto.mapper.CommentResponseCollectionMapper; +import com.dnd.spaced.core.comment.application.dto.request.CreateCommentRequest; +import com.dnd.spaced.core.comment.application.dto.request.UpdateCommentRequest; +import com.dnd.spaced.core.comment.application.dto.response.CommentCollectionResponse; +import com.dnd.spaced.core.comment.domain.dto.LikedComment; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CommentServiceFacade { + + private final CreateCommentService createCommentService; + private final ReadCommentService readCommentService; + private final UpdateCommentService updateCommentService; + private final DeleteCommentService deleteCommentService; + private final CommentResponseCollectionMapper mapper; + + @Transactional + public void createComment(Long accountId, Long wordId, CreateCommentRequest request) { + createCommentService.createComment(accountId, wordId, request); + } + + @Transactional + public void deleteComment(Long accountId, Long commentId) { + deleteCommentService.deleteComment(accountId, commentId); + } + + @Transactional + public void updateComment(Long accountId, Long commentId, UpdateCommentRequest request) { + updateCommentService.updateComment(accountId, commentId, request); + } + + public CommentCollectionResponse readComments(Long accountId, Long wordId, Long lastCommentId, Pageable pageable) { + List comments = readCommentService.readComments(accountId, wordId, lastCommentId, pageable); + + return mapper.toResponse(comments); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/CreateCommentService.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/CreateCommentService.java new file mode 100644 index 00000000..d6000d6a --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/CreateCommentService.java @@ -0,0 +1,37 @@ +package com.dnd.spaced.core.comment.application; + +import com.dnd.spaced.core.comment.application.dto.request.CreateCommentRequest; +import com.dnd.spaced.core.comment.application.exception.WordNotFoundException; +import com.dnd.spaced.core.comment.domain.Comment; +import com.dnd.spaced.core.comment.domain.repository.CommentRepository; +import com.dnd.spaced.core.word.domain.repository.WordRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +class CreateCommentService { + + private final WordRepository wordRepository; + private final CommentRepository commentRepository; + + @Transactional + public void createComment(Long accountId, Long wordId, CreateCommentRequest request) { + validateWordId(wordId); + + persistComment(accountId, wordId, request); + } + + private void validateWordId(Long wordId) { + if (!wordRepository.existsBy(wordId)) { + throw new WordNotFoundException("댓글과 관련된 용어를 찾을 수 없습니다."); + } + } + + private void persistComment(Long accountId, Long wordId, CreateCommentRequest request) { + Comment comment = new Comment(accountId, wordId, request.content()); + + commentRepository.save(comment); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/DeleteCommentService.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/DeleteCommentService.java new file mode 100644 index 00000000..dd30ad3e --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/DeleteCommentService.java @@ -0,0 +1,39 @@ +package com.dnd.spaced.core.comment.application; + +import com.dnd.spaced.core.comment.application.exception.CommentNotFoundException; +import com.dnd.spaced.core.comment.application.exception.ForbiddenCommentException; +import com.dnd.spaced.core.comment.domain.Comment; +import com.dnd.spaced.core.comment.domain.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +class DeleteCommentService { + + private final CommentRepository commentRepository; + + @Transactional + public void deleteComment(Long accountId, Long commentId) { + Comment comment = findComment(commentId); + + validateCommentDeletePermission(comment, accountId); + executeCommentDeletion(comment); + } + + private Comment findComment(Long commentId) { + return commentRepository.findBy(commentId) + .orElseThrow(() -> new CommentNotFoundException("지정한 ID에 해당하는 댓글이 없습니다.")); + } + + private void validateCommentDeletePermission(Comment comment, Long accountId) { + if (comment.isReader(accountId)) { + throw new ForbiddenCommentException("댓글을 삭제할 권한이 없습니다."); + } + } + + private void executeCommentDeletion(Comment comment) { + comment.delete(); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/ReadCommentService.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/ReadCommentService.java new file mode 100644 index 00000000..939a4a65 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/ReadCommentService.java @@ -0,0 +1,19 @@ +package com.dnd.spaced.core.comment.application; + +import com.dnd.spaced.core.comment.domain.dto.LikedComment; +import com.dnd.spaced.core.comment.domain.repository.CommentRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class ReadCommentService { + + private final CommentRepository commentRepository; + + public List readComments(Long accountId, Long wordId, Long lastCommentId, Pageable pageable) { + return commentRepository.findAllBy(accountId, wordId, lastCommentId, pageable); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/UpdateCommentService.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/UpdateCommentService.java new file mode 100644 index 00000000..4e47a330 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/UpdateCommentService.java @@ -0,0 +1,40 @@ +package com.dnd.spaced.core.comment.application; + +import com.dnd.spaced.core.comment.application.dto.request.UpdateCommentRequest; +import com.dnd.spaced.core.comment.application.exception.CommentNotFoundException; +import com.dnd.spaced.core.comment.application.exception.ForbiddenCommentException; +import com.dnd.spaced.core.comment.domain.Comment; +import com.dnd.spaced.core.comment.domain.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +class UpdateCommentService { + + private final CommentRepository commentRepository; + + @Transactional + public void updateComment(Long accountId, Long commentId, UpdateCommentRequest request) { + Comment comment = findComment(commentId); + + validateCommentUpdatePermission(comment, accountId); + executeCommentUpdate(request, comment); + } + + private Comment findComment(Long commentId) { + return commentRepository.findBy(commentId) + .orElseThrow(() -> new CommentNotFoundException("지정한 ID에 해당하는 댓글이 없습니다.")); + } + + private void validateCommentUpdatePermission(Comment comment, Long accountId) { + if (comment.isReader(accountId)) { + throw new ForbiddenCommentException("댓글을 수정할 권한이 없습니다."); + } + } + + private void executeCommentUpdate(UpdateCommentRequest request, Comment comment) { + comment.changeContent(request.content()); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/dto/mapper/CommentResponseCollectionMapper.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/dto/mapper/CommentResponseCollectionMapper.java index c805c0c9..107cdf02 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/application/dto/mapper/CommentResponseCollectionMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/dto/mapper/CommentResponseCollectionMapper.java @@ -5,39 +5,38 @@ import com.dnd.spaced.core.comment.application.dto.response.CommentCollectionResponse.CommentResponse; import com.dnd.spaced.core.comment.application.dto.response.CommentCollectionResponse.CommentWriterResponse; import com.dnd.spaced.core.comment.domain.Comment; -import com.dnd.spaced.core.comment.domain.dto.LikedCommentInfo; +import com.dnd.spaced.core.comment.domain.dto.LikedComment; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class CommentResponseCollectionMapper { +@Mapper +public class CommentResponseCollectionMapper { - public static CommentCollectionResponse toCollectionDto(List comments) { + public CommentCollectionResponse toResponse(List comments) { if (comments.isEmpty()) { return new CommentCollectionResponse(List.of(), null); } List responses = comments.stream() - .map(CommentResponseCollectionMapper::toCommentResponse) + .map(this::toCommentResponse) .toList(); return new CommentCollectionResponse(responses, comments.get(comments.size() - 1).comment().getId()); } - private static CommentResponse toCommentResponse(LikedCommentInfo likedCommentInfo) { + private CommentResponse toCommentResponse(LikedComment likedComment) { return new CommentResponse( - toCommentContentResponse(likedCommentInfo.comment()), + toCommentContentResponse(likedComment.comment()), toCommentWriterResponse( - likedCommentInfo.writerNickname(), - likedCommentInfo.writerProfileImage(), - likedCommentInfo.comment().getWriterId() + likedComment.writerNickname(), + likedComment.writerProfileImage(), + likedComment.comment().getWriterId() ), - likedCommentInfo.isLiked() + likedComment.isLiked() ); } - private static CommentContentResponse toCommentContentResponse(Comment comment) { + private CommentContentResponse toCommentContentResponse(Comment comment) { return new CommentContentResponse( comment.getId(), comment.getWordId(), @@ -46,7 +45,7 @@ private static CommentContentResponse toCommentContentResponse(Comment comment) ); } - private static CommentWriterResponse toCommentWriterResponse( + private CommentWriterResponse toCommentWriterResponse( String writerNickname, String writerProfileImage, Long writerId diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/application/event/listener/CommentLikeCountListener.java b/space-d/src/main/java/com/dnd/spaced/core/comment/application/event/listener/CommentLikeCountListener.java index aa09f41b..2dc54c64 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/application/event/listener/CommentLikeCountListener.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/application/event/listener/CommentLikeCountListener.java @@ -17,12 +17,12 @@ public class CommentLikeCountListener { @EventListener @Transactional public void listen(LikedEvent event) { - commentRepository.increaseLikeCount(event.commentId()); + commentRepository.addLikeCount(event.commentId()); } @EventListener @Transactional public void listen(UnlikedEvent event) { - commentRepository.decreaseLikeCount(event.commentId()); + commentRepository.subtractLikeCount(event.commentId()); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/domain/Comment.java b/space-d/src/main/java/com/dnd/spaced/core/comment/domain/Comment.java index 08749e08..3b9891f9 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/domain/Comment.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/domain/Comment.java @@ -68,11 +68,11 @@ public boolean isWriter(Account account) { return account.isEqualTo(this.writerId); } - public boolean isNotWriter(Account account) { + public boolean isReader(Account account) { return !isWriter(account); } - public boolean isNotWriter(Long accountId) { + public boolean isReader(Long accountId) { return !isWriter(accountId); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/domain/dto/LikedCommentInfo.java b/space-d/src/main/java/com/dnd/spaced/core/comment/domain/dto/LikedComment.java similarity index 69% rename from space-d/src/main/java/com/dnd/spaced/core/comment/domain/dto/LikedCommentInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/comment/domain/dto/LikedComment.java index a3de4bac..01e60d02 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/domain/dto/LikedCommentInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/domain/dto/LikedComment.java @@ -2,14 +2,14 @@ import com.dnd.spaced.core.comment.domain.Comment; -public record LikedCommentInfo( +public record LikedComment( Comment comment, boolean isLiked, String writerNickname, String writerProfileImage ) { - public LikedCommentInfo(Comment comment, String writerNickname, String writerProfileImage) { + public LikedComment(Comment comment, String writerNickname, String writerProfileImage) { this(comment, false, writerNickname, writerProfileImage); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/domain/repository/CommentRepository.java b/space-d/src/main/java/com/dnd/spaced/core/comment/domain/repository/CommentRepository.java index 47fbd29a..5d41f74d 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/domain/repository/CommentRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/domain/repository/CommentRepository.java @@ -1,7 +1,7 @@ package com.dnd.spaced.core.comment.domain.repository; import com.dnd.spaced.core.comment.domain.Comment; -import com.dnd.spaced.core.comment.domain.dto.LikedCommentInfo; +import com.dnd.spaced.core.comment.domain.dto.LikedComment; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Pageable; @@ -12,11 +12,9 @@ public interface CommentRepository { Optional findBy(Long commentId); - List findAllBy(Long accountId, Long wordId, Long lastCommentId, Pageable pageable); + List findAllBy(Long accountId, Long wordId, Long lastCommentId, Pageable pageable); - void delete(Comment comment); + void addLikeCount(Long commentId); - void increaseLikeCount(Long commentId); - - void decreaseLikeCount(Long commentId); + void subtractLikeCount(Long commentId); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepository.java index ef0623f5..9238b715 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepository.java @@ -6,7 +6,7 @@ import com.dnd.spaced.core.comment.domain.Comment; import com.dnd.spaced.core.comment.domain.repository.CommentRepository; -import com.dnd.spaced.core.comment.domain.dto.LikedCommentInfo; +import com.dnd.spaced.core.comment.domain.dto.LikedComment; import com.dnd.spaced.global.consts.AuthConst; import com.querydsl.core.types.ConstructorExpression; import com.querydsl.core.types.Projections; @@ -32,11 +32,15 @@ public Comment save(Comment comment) { @Override public Optional findBy(Long commentId) { - return commentCrudRepository.findById(commentId); + Comment result = queryFactory.selectFrom(comment) + .where(comment.id.eq(commentId), comment.deleted.isFalse()) + .fetchOne(); + + return Optional.ofNullable(result); } @Override - public List findAllBy(Long accountId, Long wordId, Long lastCommentId, Pageable pageable) { + public List findAllBy(Long accountId, Long wordId, Long lastCommentId, Pageable pageable) { if (AuthConst.GUEST_ACCOUNT_ID.equals(accountId)) { return findAllWithoutIsLikedBy(wordId, lastCommentId, pageable); } @@ -45,68 +49,55 @@ public List findAllBy(Long accountId, Long wordId, Long lastCo } @Override - public void delete(Comment comment) { - commentCrudRepository.delete(comment); - } - - @Override - public void increaseLikeCount(Long commentId) { + public void addLikeCount(Long commentId) { queryFactory.update(comment) .set(comment.likeCount, comment.likeCount.add(1)) - .where(comment.id.eq(commentId)) + .where(comment.id.eq(commentId), comment.deleted.isFalse()) .execute(); } @Override - public void decreaseLikeCount(Long commentId) { + public void subtractLikeCount(Long commentId) { queryFactory.update(comment) .set(comment.likeCount, comment.likeCount.subtract(1)) - .where(comment.id.eq(commentId)) + .where(comment.id.eq(commentId), comment.deleted.isFalse()) .execute(); } - private List findAllWithIsLikedBy( + private List findAllWithIsLikedBy( Long accountId, Long wordId, Long lastCommentId, Pageable pageable ) { - return queryFactory.select(getLikedCommentInfoWithLiked()) + return queryFactory.select(getLikedCommentWithLiked()) .from(comment) .leftJoin(account).on(comment.writerId.eq(account.id)) .leftJoin(like).on(comment.id.eq(like.commentId), like.accountId.eq(accountId)) .where( comment.wordId.eq(wordId), comment.deleted.isFalse(), - calculateLastIdExpression(lastCommentId) + gtLastCommentId(lastCommentId) ) .orderBy(comment.id.asc()) .limit(pageable.getPageSize()) .fetch(); } - private List findAllWithoutIsLikedBy(Long wordId, Long lastCommentId, Pageable pageable) { - return queryFactory.select(getLikedCommentInfoWithoutLiked()) + private List findAllWithoutIsLikedBy(Long wordId, Long lastCommentId, Pageable pageable) { + return queryFactory.select(getLikedCommentWithoutLiked()) .from(comment) .leftJoin(account).on(comment.writerId.eq(account.id)) .where( comment.wordId.eq(wordId), comment.deleted.isFalse(), - calculateLastIdExpression(lastCommentId) + gtLastCommentId(lastCommentId) ) .orderBy(comment.id.asc()) .limit(pageable.getPageSize()) .fetch(); } - private BooleanExpression calculateLastIdExpression(Long lastCommentId) { - if (lastCommentId == null) { - return null; - } - - return gtLastCommentId(lastCommentId); - } - private BooleanExpression gtLastCommentId(Long commentId) { if (commentId == null) { return null; @@ -115,22 +106,22 @@ private BooleanExpression gtLastCommentId(Long commentId) { return comment.id.gt(commentId); } - private ConstructorExpression getLikedCommentInfoWithLiked() { + private ConstructorExpression getLikedCommentWithLiked() { return Projections.constructor( - LikedCommentInfo.class, + LikedComment.class, comment, like.id.isNotNull(), - account.profileInfo.nickname, - account.profileInfo.profileImage + account.profile.nickname, + account.profile.profileImageName ); } - private ConstructorExpression getLikedCommentInfoWithoutLiked() { + private ConstructorExpression getLikedCommentWithoutLiked() { return Projections.constructor( - LikedCommentInfo.class, + LikedComment.class, comment, - account.profileInfo.nickname, - account.profileInfo.profileImage + account.profile.nickname, + account.profile.profileImageName ); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/comment/presentation/CommentController.java b/space-d/src/main/java/com/dnd/spaced/core/comment/presentation/CommentController.java index f46cf8fd..b824c4ac 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/comment/presentation/CommentController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/comment/presentation/CommentController.java @@ -1,13 +1,13 @@ package com.dnd.spaced.core.comment.presentation; -import com.dnd.spaced.core.comment.application.CommentService; +import com.dnd.spaced.core.comment.application.CommentServiceFacade; import com.dnd.spaced.core.comment.application.dto.request.CreateCommentRequest; import com.dnd.spaced.core.comment.application.dto.request.ReadAllCommentRequest; import com.dnd.spaced.core.comment.application.dto.request.UpdateCommentRequest; import com.dnd.spaced.core.comment.application.dto.response.CommentCollectionResponse; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.GuestAccountInfo; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.GuestAccountId; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; import com.dnd.spaced.global.resolver.comment.CommentPageable; import jakarta.validation.Valid; @@ -28,15 +28,15 @@ @RequiredArgsConstructor public class CommentController { - private final CommentService commentService; + private final CommentServiceFacade commentServiceFacade; @PostMapping("/words/{wordId}/comments") public ResponseEntity creteComment( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @Valid @RequestBody CreateCommentRequest request, @PathVariable Long wordId ) { - commentService.createComment(accountInfo.accountId(), wordId, request); + commentServiceFacade.createComment(accountId.id(), wordId, request); URI location = UriComponentsBuilder.fromPath("/words/{wordId}") .buildAndExpand(wordId) @@ -47,32 +47,32 @@ public ResponseEntity creteComment( } @DeleteMapping("/comments/{commentId}") - public ResponseEntity deleteComment(@CurrentAccountInfo AuthAccountInfo accountInfo, @PathVariable Long commentId) { - commentService.deleteComment(accountInfo.accountId(), commentId); + public ResponseEntity deleteComment(@CurrentAccount AuthAccountId accountId, @PathVariable Long commentId) { + commentServiceFacade.deleteComment(accountId.id(), commentId); return ResponseEntityConst.NO_CONTENT; } @PutMapping("/comments/{commentId}") public ResponseEntity update( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @Valid @RequestBody UpdateCommentRequest request, @PathVariable Long commentId ) { - commentService.updateComment(accountInfo.accountId(), commentId, request); + commentServiceFacade.updateComment(accountId.id(), commentId, request); return ResponseEntityConst.NO_CONTENT; } @GetMapping("/words/{wordId}/comments") public ResponseEntity readComments( - @CurrentAccountInfo GuestAccountInfo accountInfo, + @CurrentAccount GuestAccountId accountId, @PathVariable Long wordId, ReadAllCommentRequest request, @CommentPageable Pageable pageable ) { - CommentCollectionResponse response = commentService.readComments( - accountInfo.accountId(), + CommentCollectionResponse response = commentServiceFacade.readComments( + accountId.id(), wordId, request.lastCommentId(), pageable diff --git a/space-d/src/main/java/com/dnd/spaced/core/like/application/LikeService.java b/space-d/src/main/java/com/dnd/spaced/core/like/application/LikeService.java index dcdea36e..baedf922 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/like/application/LikeService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/like/application/LikeService.java @@ -22,35 +22,29 @@ public class LikeService { @Transactional public void processLike(Long accountId, Long commentId) { - Comment targetComment = findTargetComment(commentId); + Comment comment = findComment(commentId); - likeRepository.findBy(accountId, targetComment.getId()) + likeRepository.findBy(accountId, comment.getId()) .ifPresentOrElse( - like -> processDeleteLike(like, targetComment), - () -> processAddLike(accountId, targetComment) + like -> deleteLike(like, comment), + () -> addLike(accountId, comment) ); } - private Comment findTargetComment(Long commentId) { + private Comment findComment(Long commentId) { return commentRepository.findBy(commentId) .orElseThrow(() -> new AssociationCommentNotFoundException("좋아요 대상인 댓글을 찾을 수 없습니다.")); } - private void processDeleteLike(Like like, Comment comment) { + private void deleteLike(Like like, Comment comment) { likeRepository.delete(like); - publishDeletedLikeEvent(comment); - } - - private void processAddLike(Long accountId, Comment comment) { - likeRepository.save(new Like(accountId, comment.getId())); - publishAddedLikeEvent(comment); - } - - private void publishDeletedLikeEvent(Comment comment) { eventPublisher.publishEvent(new UnlikedEvent(comment.getId())); } - private void publishAddedLikeEvent(Comment comment) { + private void addLike(Long accountId, Comment comment) { + Like like = new Like(accountId, comment.getId()); + + likeRepository.save(like); eventPublisher.publishEvent(new LikedEvent(comment.getId())); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/like/presentation/LikeController.java b/space-d/src/main/java/com/dnd/spaced/core/like/presentation/LikeController.java index 2a70718a..cb511d51 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/like/presentation/LikeController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/like/presentation/LikeController.java @@ -1,8 +1,8 @@ package com.dnd.spaced.core.like.presentation; import com.dnd.spaced.core.like.application.LikeService; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -20,10 +20,10 @@ public class LikeController { @PostMapping public ResponseEntity processLike( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @PathVariable Long commentId ) { - likeService.processLike(accountInfo.accountId(), commentId); + likeService.processLike(accountId.id(), commentId); return ResponseEntityConst.NO_CONTENT; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/CreateQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/CreateQuizService.java new file mode 100644 index 00000000..9335f813 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/CreateQuizService.java @@ -0,0 +1,168 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.request.CreateQuizRequest; +import com.dnd.spaced.core.quiz.application.exception.InvalidQuizWordCountException; +import com.dnd.spaced.core.quiz.application.exception.WordMetadataNotFoundException; +import com.dnd.spaced.core.quiz.domain.Quiz; +import com.dnd.spaced.core.quiz.domain.QuizOption; +import com.dnd.spaced.core.quiz.domain.QuizQuestion; +import com.dnd.spaced.core.quiz.domain.embed.QuizAnswerOption; +import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; +import com.dnd.spaced.core.quiz.domain.repository.QuizOptionRepository; +import com.dnd.spaced.core.quiz.domain.repository.QuizQuestionRepository; +import com.dnd.spaced.core.quiz.domain.repository.QuizRepository; +import com.dnd.spaced.core.quiz.domain.service.QuizWordCountValidator; +import com.dnd.spaced.core.word.domain.WordMetadata; +import com.dnd.spaced.core.word.domain.dto.SimpleWord; +import com.dnd.spaced.core.word.domain.repository.WordMetadataRepository; +import com.dnd.spaced.core.word.domain.repository.WordRandomRepository; +import com.dnd.spaced.global.config.properties.QuizQuestionProperties; +import java.util.Collections; +import java.util.List; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class CreateQuizService { + + private static final Long DEFAULT_WORD_METADATA_ID = 1L; + private static final int QUIZ_QUESTION_WORD_COUNT = 5; + private static final int REQUIRED_QUIZ_WORD_COUNT = 20; + private static final int REQUIRED_QUESTION_WORD_COUNT = 4; + private static final int ANSWER_OPTION_INDEX = 0; + + private final QuizRepository quizRepository; + private final QuizQuestionRepository quizQuestionRepository; + private final QuizOptionRepository quizOptionRepository; + private final WordRandomRepository wordRandomRepository; + private final WordMetadataRepository wordMetadataRepository; + private final QuizQuestionProperties quizQuestionProperties; + + public Long createQuiz(Long accountId, CreateQuizRequest request) { + QuizCategory quizCategory = findQuizCategory(request); + + validateQuizCreation(quizCategory); + + Quiz quiz = createQuiz(accountId, quizCategory); + + return quiz.getId(); + } + + private QuizCategory findQuizCategory(CreateQuizRequest request) { + return QuizCategory.findBy(request.quizCategoryName()); + } + + private void validateQuizCreation(QuizCategory quizCategory) { + WordMetadata wordMetadata = findWordMetadata(); + + validateQuizMetadata(quizCategory, wordMetadata); + } + + private WordMetadata findWordMetadata() { + return wordMetadataRepository.findBy(DEFAULT_WORD_METADATA_ID) + .orElseThrow(() -> new WordMetadataNotFoundException( + "용어 메타데이터가 정상적으로 설정되지 않았습니다.") + ); + } + + private void validateQuizMetadata(QuizCategory quizCategory, WordMetadata wordMetadata) { + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + if (quizWordCountValidator.isInvalidate(quizCategory, wordMetadata, REQUIRED_QUIZ_WORD_COUNT)) { + throw new InvalidQuizWordCountException("퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); + } + } + + private Quiz createQuiz(Long accountId, QuizCategory quizCategory) { + Quiz quiz = persistQuiz(accountId); + List> randomSplitWords = findRandomSplitWords(quizCategory); + List quizQuestionIds = persistQuizQuestion(quizCategory, randomSplitWords, quiz); + + persistQuizOptions(randomSplitWords, quizQuestionIds); + return quiz; + } + + private Quiz persistQuiz(Long accountId) { + Quiz quiz = new Quiz(accountId); + + return quizRepository.save(quiz); + } + + private List> findRandomSplitWords(QuizCategory quizCategory) { + List randomWords = findRandomWords(quizCategory); + + return splitByQuestionWordCount(randomWords); + } + + private List findRandomWords(QuizCategory quizCategory) { + return wordRandomRepository.findRandomAllBy(quizCategory, REQUIRED_QUIZ_WORD_COUNT); + } + + private List> splitByQuestionWordCount(List words) { + return IntStream.range(0, QUIZ_QUESTION_WORD_COUNT) + .mapToObj(index -> index * REQUIRED_QUESTION_WORD_COUNT) + .map(index -> splitByIndex(words, index)) + .toList(); + } + + private List splitByIndex(List words, Integer startIndex) { + int endIndex = Math.min(startIndex + REQUIRED_QUESTION_WORD_COUNT, REQUIRED_QUIZ_WORD_COUNT); + + return words.subList(startIndex, endIndex); + } + + private List persistQuizQuestion( + QuizCategory quizCategory, + List> splitWords, + Quiz savedQuiz + ) { + List quizQuestions = splitWords.stream() + .map(words -> convertQuizQuestion(quizCategory, savedQuiz, words)) + .toList(); + + return quizQuestionRepository.saveAll(quizQuestions); + } + + private QuizQuestion convertQuizQuestion(QuizCategory quizCategory, Quiz savedQuiz, List words) { + SimpleWord answerWord = words.get(ANSWER_OPTION_INDEX); + + return QuizQuestion.of( + quizCategory, + quizQuestionProperties.getQuestion(), + answerWord.meaning(), + new QuizAnswerOption( + answerWord.id(), + answerWord.name() + ), + savedQuiz + ); + } + + private void persistQuizOptions(List> splitWords, List quizQuestionIds) { + List quizOptions = IntStream.range(0, splitWords.size()) + .mapToObj(index -> + convertQuizOptions( + splitWords.get(index), + quizQuestionIds.get(index) + ) + ) + .flatMap(List::stream) + .toList(); + + quizOptionRepository.saveAll(quizOptions); + } + + private List convertQuizOptions(List targetWords, Long targetQuizQuestionId) { + Collections.shuffle(targetWords); + + return IntStream.range(0, targetWords.size()) + .mapToObj(index -> convertQuizOption(targetQuizQuestionId, index, targetWords.get(index))) + .toList(); + } + + private QuizOption convertQuizOption(Long targetQuizQuestionId, int index, SimpleWord word) { + return QuizOption.of(word.id(), word.name(), index, targetQuizQuestionId); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/GradeQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/GradeQuizService.java new file mode 100644 index 00000000..62d028fc --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/GradeQuizService.java @@ -0,0 +1,50 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; +import com.dnd.spaced.core.quiz.application.exception.AlreadyGradeQuizException; +import com.dnd.spaced.core.quiz.application.exception.QuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.Quiz; +import com.dnd.spaced.core.quiz.domain.Quiz.SubmitAnswer; +import com.dnd.spaced.core.quiz.domain.QuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.repository.QuizGradedAnswerRepository; +import com.dnd.spaced.core.quiz.domain.repository.QuizRepository; +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class GradeQuizService { + + private final QuizRepository quizRepository; + private final QuizGradedAnswerRepository quizGradedAnswerRepository; + + public void gradeQuiz(Long accountId, Long quizId, GradeQuizRequest request) { + Quiz quiz = findQuiz(quizId); + + validateQuiz(quiz); + + convertQuizGradedAnswer(accountId, request, quiz); + } + + private Quiz findQuiz(Long quizId) { + return quizRepository.findBy(quizId) + .orElseThrow(() -> new QuizNotFoundException("지정한 id의 퀴즈를 찾지 못했습니다.")); + } + + private void validateQuiz(Quiz quiz) { + if (quiz.isSolved()) { + throw new AlreadyGradeQuizException("이미 풀었던 퀴즈입니다."); + } + } + + private void convertQuizGradedAnswer(Long accountId, GradeQuizRequest request, Quiz quiz) { + List submitAnswers = Arrays.stream(request.submitAnswers()) + .map(submitAnswer -> new SubmitAnswer(submitAnswer.wordId(), submitAnswer.content())) + .toList(); + List quizGradedAnswers = quiz.grade(accountId, submitAnswers); + + quizGradedAnswerRepository.saveAll(quizGradedAnswers); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/GradeTodayQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/GradeTodayQuizService.java new file mode 100644 index 00000000..d3e7a1d2 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/GradeTodayQuizService.java @@ -0,0 +1,49 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; +import com.dnd.spaced.core.quiz.application.exception.AlreadyGradeTodayQuizException; +import com.dnd.spaced.core.quiz.application.exception.TodayQuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.TodayQuiz; +import com.dnd.spaced.core.quiz.domain.TodayQuiz.SubmitAnswer; +import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.repository.TodayQuizGradedAnswerRepository; +import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class GradeTodayQuizService { + + private final TodayQuizRepository todayQuizRepository; + private final TodayQuizGradedAnswerRepository todayQuizGradedAnswerRepository; + + public void gradeTodayQuiz(Long accountId, Long todayQuizId, GradeTodayQuizRequest request) { + TodayQuiz todayQuiz = findTodayQuiz(todayQuizId); + + validateTodayQuizGradedAnswer(accountId, todayQuizId); + gradeTodayQuiz(accountId, request, todayQuiz); + } + + private TodayQuiz findTodayQuiz(Long todayQuizId) { + return todayQuizRepository.findTodayQuizBy(todayQuizId) + .orElseThrow( + () -> new TodayQuizNotFoundException( + "지정한 id의 오늘의 퀴즈를 찾지 못했습니다." + ) + ); + } + + private void validateTodayQuizGradedAnswer(Long accountId, Long todayQuizId) { + if (todayQuizGradedAnswerRepository.existsBy(accountId, todayQuizId)) { + throw new AlreadyGradeTodayQuizException("이미 오늘의 퀴즈를 풀었습니다."); + } + } + + private void gradeTodayQuiz(Long accountId, GradeTodayQuizRequest request, TodayQuiz todayQuiz) { + SubmitAnswer submitAnswer = new SubmitAnswer(request.selectedWordId(), request.selectedContent()); + TodayQuizGradedAnswer gradedAnswer = todayQuiz.grade(accountId, submitAnswer); + + todayQuizGradedAnswerRepository.save(gradedAnswer); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/QuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/QuizService.java deleted file mode 100644 index 4766bcb6..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/QuizService.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.dnd.spaced.core.quiz.application; - -import com.dnd.spaced.core.quiz.application.dto.mapper.QuizGradedAnswerCollectionResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.mapper.QuizResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.mapper.QuizCollectionResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.request.CreateQuizRequest; -import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; -import com.dnd.spaced.core.quiz.application.dto.request.ReadAllQuizRequest; -import com.dnd.spaced.core.quiz.application.dto.request.ReadQuizGradedAnswerSearchRequest; -import com.dnd.spaced.core.quiz.application.dto.response.QuizGradedAnswerCollectionResponse; -import com.dnd.spaced.core.quiz.application.dto.response.QuizCollectionResponse; -import com.dnd.spaced.core.quiz.application.dto.response.QuizResponse; -import com.dnd.spaced.core.quiz.application.enums.QuizWordCountValidator; -import com.dnd.spaced.core.quiz.application.event.dto.AddedQuizQuestionEvent; -import com.dnd.spaced.core.quiz.application.exception.AlreadyGradeQuizException; -import com.dnd.spaced.core.quiz.application.exception.InvalidQuizWordCountException; -import com.dnd.spaced.core.quiz.application.exception.QuizNotFoundException; -import com.dnd.spaced.core.quiz.application.exception.WordMetadataNotFoundException; -import com.dnd.spaced.core.quiz.domain.Quiz; -import com.dnd.spaced.core.quiz.domain.Quiz.SubmitAnswer; -import com.dnd.spaced.core.quiz.domain.QuizGradedAnswer; -import com.dnd.spaced.core.quiz.domain.QuizOption; -import com.dnd.spaced.core.quiz.domain.QuizQuestion; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizInfo; -import com.dnd.spaced.core.quiz.domain.embed.QuizAnswerOption; -import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; -import com.dnd.spaced.core.quiz.domain.repository.QuizGradedAnswerRepository; -import com.dnd.spaced.core.quiz.domain.repository.QuizOptionRepository; -import com.dnd.spaced.core.quiz.domain.repository.QuizQuestionRepository; -import com.dnd.spaced.core.quiz.domain.repository.QuizRepository; -import com.dnd.spaced.core.skill.application.event.dto.GradedQuizEvent; -import com.dnd.spaced.core.word.domain.WordMetadata; -import com.dnd.spaced.core.word.domain.dto.SimpleWordInfo; -import com.dnd.spaced.core.word.domain.repository.WordMetadataRepository; -import com.dnd.spaced.core.word.domain.repository.WordRandomRepository; -import com.dnd.spaced.global.config.properties.QuizQuestionProperties; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.stream.IntStream; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class QuizService { - - private static final Long DEFAULT_WORD_METADATA_ID = 1L; - private static final int REQUIRED_QUIZ_WORD_COUNT = 20; - private static final int REQUIRED_QUESTION_WORD_COUNT = 4; - private static final int ANSWER_OPTION_INDEX = 0; - - private final QuizRepository quizRepository; - private final QuizQuestionRepository quizQuestionRepository; - private final QuizOptionRepository quizOptionRepository; - private final WordRandomRepository wordRandomRepository; - private final WordMetadataRepository wordMetadataRepository; - private final QuizGradedAnswerRepository quizGradedAnswerRepository; - private final QuizQuestionProperties quizQuestionProperties; - private final ApplicationEventPublisher eventPublisher; - - @Transactional - public Long createQuiz(Long accountId, CreateQuizRequest request) { - QuizCategory quizCategory = QuizCategory.findBy(request.quizCategoryName()); - - validateQuizCreation(quizCategory); - - Quiz quiz = createQuiz(accountId, quizCategory); - Quiz savedQuiz = quizRepository.save(quiz); - - publishAddedQuizQuestionEvent(); - return savedQuiz.getId(); - } - - @Transactional - public void grade(Long accountId, Long quizId, GradeQuizRequest request) { - Quiz quiz = findQuiz(quizId); - - validateQuiz(quiz); - - List submitAnswers = convertSubmitAnswers(request); - List quizGradedAnswers = quiz.grade(accountId, submitAnswers); - - quizGradedAnswerRepository.saveAll(quizGradedAnswers); - quiz.solve(); - publishGradedQuizEvent(accountId, quizGradedAnswers); - } - - public QuizGradedAnswerCollectionResponse readGradedAnswers( - Long accountId, - ReadQuizGradedAnswerSearchRequest request, - Pageable pageable - ) { - List quizGradedAnswers = quizGradedAnswerRepository.findAllBy( - accountId, - request.lastQuizGradedAnswerId(), - pageable - ); - - return QuizGradedAnswerCollectionResponseMapper.toCollectionDto(quizGradedAnswers); - } - - public QuizGradedAnswerCollectionResponse readGradedAnswers(Long accountId, Long quizId) { - List quizGradedAnswers = quizGradedAnswerRepository.findAllBy(accountId, quizId); - - return QuizGradedAnswerCollectionResponseMapper.toCollectionDto(quizGradedAnswers); - } - - public QuizResponse readQuiz(Long accountId, Long quizId) { - QuizInfo quizInfo = findQuizInfo(quizId, accountId); - - return QuizResponseMapper.toDto(quizInfo); - } - - public QuizCollectionResponse readQuizzes(Long accountId, ReadAllQuizRequest request, Pageable pageable) { - List quizzes = quizRepository.findAllBy(accountId, request.lastQuizId(), pageable); - - return QuizCollectionResponseMapper.toCollectionResponse(quizzes); - } - - private void validateQuiz(Quiz quiz) { - if (quiz.isSolved()) { - throw new AlreadyGradeQuizException("이미 풀었던 퀴즈입니다."); - } - } - - private Quiz findQuiz(Long quizId) { - return quizRepository.findBy(quizId) - .orElseThrow(() -> new QuizNotFoundException("지정한 id의 퀴즈를 찾지 못했습니다.")); - } - - private QuizInfo findQuizInfo(Long quizId, Long accountId) { - return quizRepository.findBy(quizId, accountId) - .orElseThrow(() -> new QuizNotFoundException("지정한 id의 퀴즈를 찾지 못했습니다.")); - } - - private void publishAddedQuizQuestionEvent() { - eventPublisher.publishEvent(new AddedQuizQuestionEvent()); - } - - private void validateQuizCreation(QuizCategory quizCategory) { - WordMetadata wordMetadata = wordMetadataRepository.findBy(DEFAULT_WORD_METADATA_ID) - .orElseThrow(() -> new WordMetadataNotFoundException( - "용어 메타데이터가 정상적으로 설정되지 않았습니다.") - ); - - if (QuizWordCountValidator.isInvalidate(quizCategory, wordMetadata, REQUIRED_QUIZ_WORD_COUNT)) { - throw new InvalidQuizWordCountException("퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); - } - } - - private Quiz createQuiz(Long accountId, QuizCategory quizCategory) { - Quiz quiz = new Quiz(accountId); - Quiz savedQuiz = quizRepository.save(quiz); - List randomWords = findRandomWords(quizCategory); - List> splitWords = splitByQuestionWordCount(randomWords); - List quizQuestionIds = persistQuizQuestion(quizCategory, splitWords, savedQuiz); - - persistQuizOptions(splitWords, quizQuestionIds); - return quiz; - } - - private List findRandomWords(QuizCategory quizCategory) { - return wordRandomRepository.findRandomAllBy(quizCategory, REQUIRED_QUIZ_WORD_COUNT); - } - - private List> splitByQuestionWordCount(List words) { - List> result = new ArrayList<>(); - - for (int startIndex = 0; startIndex < REQUIRED_QUIZ_WORD_COUNT; startIndex += REQUIRED_QUESTION_WORD_COUNT) { - int endIndex = Math.min(startIndex + REQUIRED_QUESTION_WORD_COUNT, REQUIRED_QUIZ_WORD_COUNT); - - result.add(words.subList(startIndex, endIndex)); - } - - return result; - } - - private List persistQuizQuestion( - QuizCategory quizCategory, - List> splitWords, - Quiz savedQuiz - ) { - List quizQuestions = splitWords.stream() - .map(words -> { - SimpleWordInfo answerWord = words.get(ANSWER_OPTION_INDEX); - - return QuizQuestion.of( - quizCategory, - quizQuestionProperties.getQuestion(), - answerWord.meaning(), - new QuizAnswerOption( - answerWord.id(), - answerWord.name() - ), - savedQuiz - ); - }) - .toList(); - - return quizQuestionRepository.saveAll(quizQuestions); - } - - private void persistQuizOptions(List> splitWords, List quizQuestionIds) { - List quizOptions = IntStream.range(0, splitWords.size()) - .mapToObj(index -> - convertQuizOptions( - splitWords.get(index), - quizQuestionIds.get(index) - ) - ) - .flatMap(List::stream) - .toList(); - - quizOptionRepository.saveAll(quizOptions); - } - - private List convertQuizOptions(List targetWords, Long targetQuizQuestionId) { - Collections.shuffle(targetWords); - - return IntStream.range(0, targetWords.size()) - .mapToObj(index -> convertQuizOption(targetQuizQuestionId, index, targetWords.get(index)) - ) - .toList(); - } - - private QuizOption convertQuizOption(Long targetQuizQuestionId, int index, SimpleWordInfo word) { - return QuizOption.of(word.id(), word.name(), index, targetQuizQuestionId); - } - - private void publishGradedQuizEvent(Long accountId, List quizGradedAnswers) { - eventPublisher.publishEvent(GradedQuizEvent.of(accountId, quizGradedAnswers)); - } - - private List convertSubmitAnswers(GradeQuizRequest request) { - return Arrays.stream(request.submitAnswers()) - .map(submitAnswer -> new SubmitAnswer(submitAnswer.wordId(), submitAnswer.content())) - .toList(); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/QuizServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/QuizServiceFacade.java new file mode 100644 index 00000000..4c27cb44 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/QuizServiceFacade.java @@ -0,0 +1,81 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.mapper.QuizGradedAnswerCollectionResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.mapper.QuizResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.mapper.QuizCollectionResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.request.CreateQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.ReadAllQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.ReadQuizGradedAnswerSearchRequest; +import com.dnd.spaced.core.quiz.application.dto.response.QuizGradedAnswerCollectionResponse; +import com.dnd.spaced.core.quiz.application.dto.response.QuizCollectionResponse; +import com.dnd.spaced.core.quiz.application.dto.response.QuizResponse; +import com.dnd.spaced.core.quiz.application.event.dto.AddedQuizQuestionEvent; +import com.dnd.spaced.core.quiz.domain.QuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto; +import com.dnd.spaced.core.skill.application.event.dto.GradedQuizEvent; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class QuizServiceFacade { + + private final CreateQuizService createQuizService; + private final GradeQuizService gradeQuizService; + private final ReadQuizService readQuizService; + private final QuizResponseMapper quizMapper; + private final QuizCollectionResponseMapper quizCollectionMapper; + private final QuizGradedAnswerCollectionResponseMapper gradedAnswerMapper; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public Long createQuiz(Long accountId, CreateQuizRequest request) { + Long quizId = createQuizService.createQuiz(accountId, request); + + eventPublisher.publishEvent(new AddedQuizQuestionEvent()); + return quizId; + } + + @Transactional + public void grade(Long accountId, Long quizId, GradeQuizRequest request) { + gradeQuizService.gradeQuiz(accountId, quizId, request); + + List quizGradedAnswers = readQuizService.readGradedAnswers(accountId, quizId); + + eventPublisher.publishEvent(GradedQuizEvent.of(accountId, quizGradedAnswers)); + } + + public QuizGradedAnswerCollectionResponse readGradedAnswers( + Long accountId, + ReadQuizGradedAnswerSearchRequest request, + Pageable pageable + ) { + List quizGradedAnswers = readQuizService.readGradedAnswers(accountId, request, pageable); + + return gradedAnswerMapper.toCollectionResponse(quizGradedAnswers); + } + + public QuizGradedAnswerCollectionResponse readGradedAnswers(Long accountId, Long quizId) { + List quizGradedAnswers = readQuizService.readGradedAnswers(accountId, quizId); + + return gradedAnswerMapper.toCollectionResponse(quizGradedAnswers); + } + + public QuizResponse readQuiz(Long accountId, Long quizId) { + QuizDto quizDto = readQuizService.readQuiz(quizId, accountId); + + return quizMapper.toResponse(quizDto); + } + + public QuizCollectionResponse readQuizzes(Long accountId, ReadAllQuizRequest request, Pageable pageable) { + List quizzes = readQuizService.readQuizzes(accountId, request, pageable); + + return quizCollectionMapper.toCollectionResponse(quizzes); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/ReadQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/ReadQuizService.java new file mode 100644 index 00000000..ec0b8fbf --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/ReadQuizService.java @@ -0,0 +1,49 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.request.ReadAllQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.ReadQuizGradedAnswerSearchRequest; +import com.dnd.spaced.core.quiz.application.exception.QuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.QuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto; +import com.dnd.spaced.core.quiz.domain.repository.QuizGradedAnswerRepository; +import com.dnd.spaced.core.quiz.domain.repository.QuizRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class ReadQuizService { + + private final QuizRepository quizRepository; + private final QuizGradedAnswerRepository quizGradedAnswerRepository; + + public List readGradedAnswers( + Long accountId, + ReadQuizGradedAnswerSearchRequest request, + Pageable pageable + ) { + return quizGradedAnswerRepository.findAllBy( + accountId, + request.lastQuizGradedAnswerId(), + pageable + ); + } + + public List readGradedAnswers(Long accountId, Long quizId) { + return quizGradedAnswerRepository.findAllBy(accountId, quizId); + } + + public QuizDto readQuiz(Long accountId, Long quizId) { + return quizRepository.findBy(quizId, accountId) + .orElseThrow( + () -> new QuizNotFoundException("지정한 id의 퀴즈를 찾지 못했습니다.") + ); + } + + public List readQuizzes(Long accountId, ReadAllQuizRequest request, Pageable pageable) { + return quizRepository.findAllBy(accountId, request.lastQuizId(), pageable); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/ReadTodayQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/ReadTodayQuizService.java new file mode 100644 index 00000000..f97c4d4d --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/ReadTodayQuizService.java @@ -0,0 +1,63 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.request.ReadTodayQuizGradedAnswerSearchRequest; +import com.dnd.spaced.core.quiz.application.dto.response.ReadTodayQuizDto; +import com.dnd.spaced.core.quiz.application.exception.TodayQuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.TodayQuiz; +import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import com.dnd.spaced.core.quiz.domain.repository.TodayQuizGradedAnswerRepository; +import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class ReadTodayQuizService { + + private final TodayQuizRepository todayQuizRepository; + private final TodayQuizGradedAnswerRepository todayQuizGradedAnswerRepository; + + public SimpleTodayQuizDto readLatestTodayQuiz() { + return todayQuizRepository.findLatest() + .orElseThrow( + () -> new TodayQuizNotFoundException( + "오늘의 퀴즈가 생성되지 않았습니다.") + ); + } + + public ReadTodayQuizDto readTodayQuiz(Long accountId, Long todayQuizId) { + TodayQuiz todayQuiz = todayQuizRepository.findWithTodayQuizOptionBy(todayQuizId) + .orElseThrow( + () -> new TodayQuizNotFoundException( + "지정한 id의 오늘의 퀴즈를 찾지 못했습니다." + ) + ); + boolean solved = todayQuizGradedAnswerRepository.existsBy(accountId, todayQuizId); + + return new ReadTodayQuizDto(todayQuiz, solved); + } + + public List readTodayQuizGradedAnswers( + Long accountId, + ReadTodayQuizGradedAnswerSearchRequest request, + Pageable pageable + ) { + return todayQuizGradedAnswerRepository.findAllBy( + accountId, + request.lastTodayQuizGradedAnswerId(), + pageable + ); + } + + public TodayQuizGradedAnswer readTargetTodayQuizGradedAnswers(Long accountId, Long todayQuizId) { + return todayQuizGradedAnswerRepository.findBy(accountId, todayQuizId) + .orElseThrow( + () -> new TodayQuizNotFoundException( + "지정한 오늘의 퀴즈 답안지를 찾지 못했습니다." + ) + ); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/TodayQuizService.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/TodayQuizService.java deleted file mode 100644 index c4643e3d..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/TodayQuizService.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.dnd.spaced.core.quiz.application; - -import com.dnd.spaced.core.quiz.application.dto.mapper.SimpleTodayQuizResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.mapper.TodayQuizGradedAnswerResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.mapper.TodayQuizGradedAnswerCollectionResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.mapper.TodayQuizResponseMapper; -import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; -import com.dnd.spaced.core.quiz.application.dto.request.ReadTodayQuizGradedAnswerSearchRequest; -import com.dnd.spaced.core.quiz.application.dto.response.SimpleTodayQuizResponse; -import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizGradedAnswerCollectionResponse; -import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizGradedAnswerResponse; -import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse; -import com.dnd.spaced.core.quiz.application.exception.AlreadyGradeTodayQuizException; -import com.dnd.spaced.core.quiz.application.exception.TodayQuizNotFoundException; -import com.dnd.spaced.core.quiz.domain.TodayQuiz; -import com.dnd.spaced.core.quiz.domain.TodayQuiz.SubmitAnswer; -import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; -import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizInfo; -import com.dnd.spaced.core.quiz.domain.repository.TodayQuizGradedAnswerRepository; -import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; -import com.dnd.spaced.core.skill.application.event.dto.GradedTodayQuizEvent; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class TodayQuizService { - - private final ApplicationEventPublisher eventPublisher; - private final TodayQuizRepository todayQuizRepository; - private final TodayQuizGradedAnswerRepository todayQuizGradedAnswerRepository; - - public SimpleTodayQuizResponse readLatestTodayQuiz() { - SimpleTodayQuizInfo simpleTodayQuizInfo = findLatestQuiz(); - - return SimpleTodayQuizResponseMapper.toDto(simpleTodayQuizInfo); - } - - public TodayQuizResponse readTodayQuiz(Long accountId, Long todayQuizId) { - TodayQuiz todayQuiz = findTodayQuizInfo(todayQuizId); - boolean solved = todayQuizGradedAnswerRepository.existsBy(accountId, todayQuizId); - - return TodayQuizResponseMapper.toDto(todayQuiz, accountId, solved); - } - - @Transactional - public void grade(Long accountId, Long todayQuizId, GradeTodayQuizRequest request) { - TodayQuiz todayQuiz = findTodayQuiz(todayQuizId); - - validateTodayQuizGradedAnswer(accountId, todayQuizId); - - SubmitAnswer submitAnswer = new SubmitAnswer(request.selectedWordId(), request.selectedContent()); - TodayQuizGradedAnswer gradedAnswer = todayQuiz.grade(accountId, submitAnswer); - - todayQuizGradedAnswerRepository.save(gradedAnswer); - publishGradedTodayQuizEvent(accountId, gradedAnswer); - } - - public TodayQuizGradedAnswerCollectionResponse readTodayQuizGradedAnswers( - Long accountId, - ReadTodayQuizGradedAnswerSearchRequest request, - Pageable pageable - ) { - List todayQuizGradedAnswers = todayQuizGradedAnswerRepository.findAllBy( - accountId, - request.lastTodayQuizGradedAnswerId(), - pageable - ); - - return TodayQuizGradedAnswerCollectionResponseMapper.toDto(todayQuizGradedAnswers); - } - - public TodayQuizGradedAnswerResponse readTargetTodayQuizGradedAnswers(Long accountId, Long todayQuizId) { - TodayQuizGradedAnswer todayQuizGradedAnswer = findTodayQuizGradedAnswer(accountId, todayQuizId); - - return TodayQuizGradedAnswerResponseMapper.toDto(todayQuizGradedAnswer); - } - - private SimpleTodayQuizInfo findLatestQuiz() { - return todayQuizRepository.findLatest() - .orElseThrow( - () -> new TodayQuizNotFoundException("오늘의 퀴즈가 생성되지 않았습니다.") - ); - } - - private TodayQuiz findTodayQuizInfo(Long todayQuizId) { - return todayQuizRepository.findWithTodayQuizOptionBy(todayQuizId) - .orElseThrow( - () -> new TodayQuizNotFoundException( - "지정한 id의 오늘의 퀴즈를 찾지 못했습니다." - ) - ); - } - - private TodayQuiz findTodayQuiz(Long todayQuizId) { - return todayQuizRepository.findTodayQuizBy(todayQuizId) - .orElseThrow( - () -> new TodayQuizNotFoundException( - "지정한 id의 오늘의 퀴즈를 찾지 못했습니다." - ) - ); - } - - private void publishGradedTodayQuizEvent(Long accountId, TodayQuizGradedAnswer gradedAnswer) { - eventPublisher.publishEvent(GradedTodayQuizEvent.of(accountId, gradedAnswer.isCorrect())); - } - - private TodayQuizGradedAnswer findTodayQuizGradedAnswer(Long accountId, Long todayQuizId) { - return todayQuizGradedAnswerRepository.findBy(accountId, todayQuizId) - .orElseThrow( - () -> new TodayQuizNotFoundException( - "지정한 오늘의 퀴즈 답안지를 찾지 못했습니다." - ) - ); - } - - private void validateTodayQuizGradedAnswer(Long accountId, Long todayQuizId) { - if (todayQuizGradedAnswerRepository.existsBy(accountId, todayQuizId)) { - throw new AlreadyGradeTodayQuizException("이미 오늘의 퀴즈를 풀었습니다."); - } - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceFacade.java new file mode 100644 index 00000000..66298f04 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceFacade.java @@ -0,0 +1,80 @@ +package com.dnd.spaced.core.quiz.application; + +import com.dnd.spaced.core.quiz.application.dto.mapper.SimpleTodayQuizResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.mapper.TodayQuizGradedAnswerResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.mapper.TodayQuizGradedAnswerCollectionResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.mapper.TodayQuizResponseMapper; +import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.ReadTodayQuizGradedAnswerSearchRequest; +import com.dnd.spaced.core.quiz.application.dto.response.ReadTodayQuizDto; +import com.dnd.spaced.core.quiz.application.dto.response.SimpleTodayQuizResponse; +import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizGradedAnswerCollectionResponse; +import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizGradedAnswerResponse; +import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse; +import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import com.dnd.spaced.core.skill.application.event.dto.GradedTodayQuizEvent; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TodayQuizServiceFacade { + + private final ReadTodayQuizService readTodayQuizService; + private final GradeTodayQuizService gradeTodayQuizService; + private final TodayQuizResponseMapper todayQuizMapper; + private final SimpleTodayQuizResponseMapper simpleTodayQuizMapper; + private final TodayQuizGradedAnswerCollectionResponseMapper gradedAnswerMapper; + private final ApplicationEventPublisher eventPublisher; + + @Transactional + public void gradeTodayQuiz(Long accountId, Long todayQuizId, GradeTodayQuizRequest request) { + gradeTodayQuizService.gradeTodayQuiz(accountId, todayQuizId, request); + TodayQuizGradedAnswer todayQuizGradedAnswer = readTodayQuizService.readTargetTodayQuizGradedAnswers( + accountId, + todayQuizId + ); + + eventPublisher.publishEvent(GradedTodayQuizEvent.of(accountId, todayQuizGradedAnswer.isCorrect())); + } + + public SimpleTodayQuizResponse readLatestTodayQuiz() { + SimpleTodayQuizDto simpleTodayQuizDto = readTodayQuizService.readLatestTodayQuiz(); + + return simpleTodayQuizMapper.toResponse(simpleTodayQuizDto); + } + + public TodayQuizResponse readTodayQuiz(Long accountId, Long todayQuizId) { + ReadTodayQuizDto readTodayQuizDto = readTodayQuizService.readTodayQuiz(accountId, todayQuizId); + + return todayQuizMapper.toResponse(readTodayQuizDto, accountId); + } + + public TodayQuizGradedAnswerCollectionResponse readTodayQuizGradedAnswers( + Long accountId, + ReadTodayQuizGradedAnswerSearchRequest request, + Pageable pageable + ) { + List todayQuizGradedAnswers = readTodayQuizService.readTodayQuizGradedAnswers( + accountId, + request, + pageable + ); + + return gradedAnswerMapper.toResponse(todayQuizGradedAnswers); + } + + public TodayQuizGradedAnswerResponse readTargetTodayQuizGradedAnswers(Long accountId, Long todayQuizId) { + TodayQuizGradedAnswer todayQuizGradedAnswer = readTodayQuizService.readTargetTodayQuizGradedAnswers( + accountId, + todayQuizId + ); + + return TodayQuizGradedAnswerResponseMapper.toDto(todayQuizGradedAnswer); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizCollectionResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizCollectionResponseMapper.java index b80edea9..b89b2c4a 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizCollectionResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizCollectionResponseMapper.java @@ -1,20 +1,19 @@ package com.dnd.spaced.core.quiz.application.dto.mapper; import static com.dnd.spaced.core.quiz.application.dto.response.QuizCollectionResponse.*; -import static com.dnd.spaced.core.quiz.domain.dto.SimpleQuizInfo.*; +import static com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto.*; import com.dnd.spaced.core.quiz.application.dto.response.QuizCollectionResponse; import com.dnd.spaced.core.quiz.application.dto.response.QuizCollectionResponse.QuizResponse.QuizQuestionResponse; -import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizInfo; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto; +import com.dnd.spaced.global.mapper.Mapper; import java.util.Collections; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class QuizCollectionResponseMapper { +@Mapper +public class QuizCollectionResponseMapper { - public static QuizCollectionResponse toCollectionResponse(List quizzes) { + public QuizCollectionResponse toCollectionResponse(List quizzes) { if (quizzes == null || quizzes.isEmpty()) { return new QuizCollectionResponse( Collections.emptyList(), @@ -23,13 +22,13 @@ public static QuizCollectionResponse toCollectionResponse(List q } List quizResponses = quizzes.stream() - .map(QuizCollectionResponseMapper::toQuizResponse) + .map(this::toQuizResponse) .toList(); return new QuizCollectionResponse(quizResponses, quizResponses.get(quizResponses.size() - 1).id()); } - private static QuizResponse toQuizResponse(SimpleQuizInfo quiz) { + private QuizResponse toQuizResponse(SimpleQuizDto quiz) { return new QuizResponse( quiz.id(), quiz.accountId(), @@ -39,7 +38,7 @@ private static QuizResponse toQuizResponse(SimpleQuizInfo quiz) { ); } - private static List toQuizQuestionResponse(List quizQuestions) { + private List toQuizQuestionResponse(List quizQuestions) { return quizQuestions.stream() .map( quizQuestion -> diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizGradedAnswerCollectionResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizGradedAnswerCollectionResponseMapper.java index 603956ea..b0b6acdc 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizGradedAnswerCollectionResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizGradedAnswerCollectionResponseMapper.java @@ -4,40 +4,39 @@ import com.dnd.spaced.core.quiz.application.dto.response.QuizGradedAnswerCollectionResponse.QuizGradedAnswerResponse; import com.dnd.spaced.core.quiz.domain.QuizGradedAnswer; import com.dnd.spaced.core.quiz.domain.QuizQuestion; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class QuizGradedAnswerCollectionResponseMapper { +@Mapper +public class QuizGradedAnswerCollectionResponseMapper { - public static QuizGradedAnswerCollectionResponse toCollectionDto(List quizGradedAnswers) { + public QuizGradedAnswerCollectionResponse toCollectionResponse(List quizGradedAnswers) { if (quizGradedAnswers.isEmpty()) { return new QuizGradedAnswerCollectionResponse(List.of(), null); } List responses = quizGradedAnswers.stream() - .map(QuizGradedAnswerCollectionResponseMapper::toDto) + .map(this::toResponse) .toList(); return new QuizGradedAnswerCollectionResponse(responses, responses.get(responses.size() - 1).id()); } - private static QuizGradedAnswerResponse toDto(QuizGradedAnswer quizGradedAnswer) { + private QuizGradedAnswerResponse toResponse(QuizGradedAnswer quizGradedAnswer) { QuizQuestion question = quizGradedAnswer.getQuizQuestion(); return new QuizGradedAnswerResponse( quizGradedAnswer.getId(), quizGradedAnswer.getAccountId(), quizGradedAnswer.getQuizId(), - toDto(question), + toResponse(question), question.getQuizAnswerOption().getAnswerContent(), quizGradedAnswer.getSelectedContent(), quizGradedAnswer.isCorrect() ); } - private static QuizGradedAnswerResponse.QuizQuestionResponse toDto(QuizQuestion quizQuestion) { + private QuizGradedAnswerResponse.QuizQuestionResponse toResponse(QuizQuestion quizQuestion) { return new QuizGradedAnswerResponse.QuizQuestionResponse( quizQuestion.getId(), quizQuestion.getQuizCategory().getName(), diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizResponseMapper.java index 63bffb63..ebb2f9a7 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/QuizResponseMapper.java @@ -3,20 +3,19 @@ import com.dnd.spaced.core.quiz.application.dto.response.QuizResponse; import com.dnd.spaced.core.quiz.application.dto.response.QuizResponse.QuizQuestionResponse; import com.dnd.spaced.core.quiz.application.dto.response.QuizResponse.QuizQuestionResponse.QuizOptionResponse; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo.QuizQuestionInfo; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo.QuizQuestionInfo.QuizOptionInfo; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto.QuizQuestionDto; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto.QuizQuestionDto.QuizOptionDto; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class QuizResponseMapper { +@Mapper +public class QuizResponseMapper { - public static QuizResponse toDto(QuizInfo quiz) { + public QuizResponse toResponse(QuizDto quiz) { List quizQuestionResponses = quiz.quizQuestions() .stream() - .map(QuizResponseMapper::toQuizDto) + .map(this::toQuizQuestionResponse) .toList(); return new QuizResponse( @@ -26,10 +25,10 @@ public static QuizResponse toDto(QuizInfo quiz) { ); } - private static QuizQuestionResponse toQuizDto(QuizQuestionInfo quizQuestion) { + private QuizQuestionResponse toQuizQuestionResponse(QuizQuestionDto quizQuestion) { List quizOptionResponses = quizQuestion.quizOptions() .stream() - .map(QuizResponseMapper::toQuizOptionDto) + .map(this::toQuizOptionResponse) .toList(); return new QuizQuestionResponse( @@ -42,7 +41,7 @@ private static QuizQuestionResponse toQuizDto(QuizQuestionInfo quizQuestion) { ); } - private static QuizOptionResponse toQuizOptionDto(QuizOptionInfo quizOption) { + private QuizOptionResponse toQuizOptionResponse(QuizOptionDto quizOption) { return new QuizOptionResponse(quizOption.id(), quizOption.wordId(), quizOption.content()); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/SimpleTodayQuizResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/SimpleTodayQuizResponseMapper.java index 7b6bad45..dcd273ad 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/SimpleTodayQuizResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/SimpleTodayQuizResponseMapper.java @@ -2,24 +2,23 @@ import com.dnd.spaced.core.quiz.application.dto.response.SimpleTodayQuizResponse; import com.dnd.spaced.core.quiz.application.dto.response.SimpleTodayQuizResponse.TodayQuizQuestionResponse; -import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizInfo; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import com.dnd.spaced.global.mapper.Mapper; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class SimpleTodayQuizResponseMapper { +@Mapper +public class SimpleTodayQuizResponseMapper { - public static SimpleTodayQuizResponse toDto(SimpleTodayQuizInfo simpleTodayQuizInfo) { + public SimpleTodayQuizResponse toResponse(SimpleTodayQuizDto simpleTodayQuizDto) { TodayQuizQuestionResponse todayQuizQuestionResponse = new TodayQuizQuestionResponse( - simpleTodayQuizInfo.quizCategory().getName(), - simpleTodayQuizInfo.question(), - simpleTodayQuizInfo.questionContent() + simpleTodayQuizDto.quizCategory().getName(), + simpleTodayQuizDto.question(), + simpleTodayQuizDto.questionContent() ); return new SimpleTodayQuizResponse( - simpleTodayQuizInfo.id(), + simpleTodayQuizDto.id(), todayQuizQuestionResponse, - simpleTodayQuizInfo.createdAt() + simpleTodayQuizDto.createdAt() ); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizGradedAnswerCollectionResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizGradedAnswerCollectionResponseMapper.java index 5f776915..0e8a6062 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizGradedAnswerCollectionResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizGradedAnswerCollectionResponseMapper.java @@ -6,22 +6,21 @@ import com.dnd.spaced.core.quiz.domain.TodayQuiz; import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizQuestion; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class TodayQuizGradedAnswerCollectionResponseMapper { +@Mapper +public class TodayQuizGradedAnswerCollectionResponseMapper { - public static TodayQuizGradedAnswerCollectionResponse toDto(List todayQuizGradedAnswers) { + public TodayQuizGradedAnswerCollectionResponse toResponse(List todayQuizGradedAnswers) { List responses = todayQuizGradedAnswers.stream() - .map(TodayQuizGradedAnswerCollectionResponseMapper::toDto) + .map(this::toResponse) .toList(); return new TodayQuizGradedAnswerCollectionResponse(responses); } - public static TodayQuizGradedAnswerResponse toDto(TodayQuizGradedAnswer todayQuizGradedAnswer) { + private TodayQuizGradedAnswerResponse toResponse(TodayQuizGradedAnswer todayQuizGradedAnswer) { TodayQuiz quiz = todayQuizGradedAnswer.getTodayQuiz(); TodayQuizQuestion quizQuestion = quiz.getTodayQuizQuestion(); @@ -29,14 +28,14 @@ public static TodayQuizGradedAnswerResponse toDto(TodayQuizGradedAnswer todayQui todayQuizGradedAnswer.getId(), quiz.getId(), todayQuizGradedAnswer.getAccountId(), - toGradedAnswerDto(quizQuestion), + toTodayQuizQuestionResponse(quizQuestion), todayQuizGradedAnswer.getSelectedContent(), quizQuestion.getTodayQuizAnswerOption().getAnswerContent(), todayQuizGradedAnswer.isCorrect() ); } - private static TodayQuizQuestionResponse toGradedAnswerDto(TodayQuizQuestion quizQuestion) { + private TodayQuizQuestionResponse toTodayQuizQuestionResponse(TodayQuizQuestion quizQuestion) { return new TodayQuizQuestionResponse( quizQuestion.getQuizCategory().getName(), quizQuestion.getQuestion(), diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizResponseMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizResponseMapper.java index d6511caf..0a23cc71 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizResponseMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/mapper/TodayQuizResponseMapper.java @@ -1,32 +1,38 @@ package com.dnd.spaced.core.quiz.application.dto.mapper; +import com.dnd.spaced.core.quiz.application.dto.response.ReadTodayQuizDto; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse.TodayQuizQuestionResponse; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse.TodayQuizQuestionResponse.TodayQuizOptionResponse; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse.TodayQuizStatus; -import com.dnd.spaced.core.quiz.domain.TodayQuiz; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizQuestion; +import com.dnd.spaced.global.consts.AuthConst; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class TodayQuizResponseMapper { +@Mapper +public class TodayQuizResponseMapper { - public static TodayQuizResponse toDto(TodayQuiz todayQuiz, Long accountId, boolean solved) { - TodayQuizQuestionResponse todayQuizQuestion = toTodayQuizQuestionDto(todayQuiz.getTodayQuizQuestion()); + public TodayQuizResponse toResponse(ReadTodayQuizDto todayQuizDto, Long accountId) { + TodayQuizQuestionResponse todayQuizQuestion = toTodayQuizQuestionResponse( + todayQuizDto.todayQuiz().getTodayQuizQuestion() + ); - if (accountId == -1L) { - return new TodayQuizResponse(todayQuiz.getId(), todayQuizQuestion, TodayQuizStatus.NOT_LOGGED_IN); + if (AuthConst.GUEST_ACCOUNT_ID.equals(accountId)) { + return new TodayQuizResponse( + todayQuizDto.todayQuiz().getId(), + todayQuizQuestion, + TodayQuizStatus.NOT_LOGGED_IN + ); } - if (solved) { - return new TodayQuizResponse(todayQuiz.getId(), todayQuizQuestion, TodayQuizStatus.SOLVED); + if (todayQuizDto.solved()) { + return new TodayQuizResponse(todayQuizDto.todayQuiz().getId(), todayQuizQuestion, TodayQuizStatus.SOLVED); } - return new TodayQuizResponse(todayQuiz.getId(), todayQuizQuestion, TodayQuizStatus.NOT_SOLVED); + return new TodayQuizResponse(todayQuizDto.todayQuiz().getId(), todayQuizQuestion, TodayQuizStatus.NOT_SOLVED); } - private static TodayQuizQuestionResponse toTodayQuizQuestionDto(TodayQuizQuestion todayQuizQuestion) { - List todayQuizOptionResponses = toTodayQuizOptionDto(todayQuizQuestion); + private TodayQuizQuestionResponse toTodayQuizQuestionResponse(TodayQuizQuestion todayQuizQuestion) { + List todayQuizOptionResponses = toTodayQuizOptionResponse(todayQuizQuestion); return new TodayQuizQuestionResponse( todayQuizQuestion.getQuizCategory().getName(), @@ -38,7 +44,7 @@ private static TodayQuizQuestionResponse toTodayQuizQuestionDto(TodayQuizQuestio ); } - private static List toTodayQuizOptionDto(TodayQuizQuestion todayQuizQuestion) { + private List toTodayQuizOptionResponse(TodayQuizQuestion todayQuizQuestion) { return todayQuizQuestion.getTodayQuizOptions() .stream() .map(todayQuizOption -> diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/response/ReadTodayQuizDto.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/response/ReadTodayQuizDto.java new file mode 100644 index 00000000..4705f944 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/dto/response/ReadTodayQuizDto.java @@ -0,0 +1,6 @@ +package com.dnd.spaced.core.quiz.application.dto.response; + +import com.dnd.spaced.core.quiz.domain.TodayQuiz; + +public record ReadTodayQuizDto(TodayQuiz todayQuiz, boolean solved) { +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/enums/QuizWordCountValidator.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/enums/QuizWordCountValidator.java deleted file mode 100644 index 9b8942f6..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/enums/QuizWordCountValidator.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.dnd.spaced.core.quiz.application.enums; - -import com.dnd.spaced.core.quiz.application.enums.exception.QuizCategoryNotFoundException; -import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; -import com.dnd.spaced.core.word.domain.WordMetadata; -import java.util.Arrays; -import java.util.function.BiPredicate; - -public enum QuizWordCountValidator { - - DEVELOP(QuizCategory.DEVELOP, WordMetadata::canGenerateBusinessQuiz), - DESIGN(QuizCategory.DESIGN, WordMetadata::canGenerateDesignQuiz), - BUSINESS(QuizCategory.BUSINESS, WordMetadata::canGenerateDesignQuiz), - TOTAL(QuizCategory.TOTAL, WordMetadata::canGenerateTotalQuiz); - - private final QuizCategory category; - private final BiPredicate validator; - - QuizWordCountValidator(QuizCategory quizCategory, BiPredicate validator) { - this.category = quizCategory; - this.validator = validator; - } - - public static boolean isValidate(QuizCategory category, WordMetadata wordMetadata, int requiredWordCount) { - return Arrays.stream(QuizWordCountValidator.values()) - .filter(validator -> validator.category.equals(category)) - .findAny() - .orElseThrow(() -> new QuizCategoryNotFoundException("지정한 퀴즈 카테고리를 찾을 수 없습니다.")) - .validator - .test(wordMetadata, requiredWordCount); - } - - public static boolean isInvalidate(QuizCategory category, WordMetadata wordMetadata, int requiredWordCount) { - return !isValidate(category, wordMetadata, requiredWordCount); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/enums/exception/QuizCategoryNotFoundException.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/exception/QuizCategoryNotFoundException.java similarity index 84% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/application/enums/exception/QuizCategoryNotFoundException.java rename to space-d/src/main/java/com/dnd/spaced/core/quiz/application/exception/QuizCategoryNotFoundException.java index 496b4f76..e6e1d877 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/application/enums/exception/QuizCategoryNotFoundException.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/application/exception/QuizCategoryNotFoundException.java @@ -1,4 +1,4 @@ -package com.dnd.spaced.core.quiz.application.enums.exception; +package com.dnd.spaced.core.quiz.application.exception; import com.dnd.spaced.global.exception.base.QuizClientException; import com.dnd.spaced.global.exception.code.QuizErrorCode; diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/Quiz.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/Quiz.java index 6964a580..6b9d0fd3 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/Quiz.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/Quiz.java @@ -41,12 +41,9 @@ public Quiz(Long accountId) { this.accountId = accountId; } - public void solve() { - this.solved = true; - } - public List grade(Long accountId, List submitAnswers) { validateAnswers(submitAnswers); + solve(); return gradeQuestions(accountId, submitAnswers); } @@ -57,6 +54,10 @@ private void validateAnswers(List submitAnswers) { } } + private void solve() { + this.solved = true; + } + private List gradeQuestions(Long accountId, List submitAnswers) { List quizGradedAnswers = new ArrayList<>(); diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/QuizInfo.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/QuizDto.java similarity index 77% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/QuizInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/QuizDto.java index 56779a59..b7061de3 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/QuizInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/QuizDto.java @@ -5,24 +5,24 @@ import java.time.LocalDateTime; import java.util.List; -public record QuizInfo( +public record QuizDto( Long id, Long accountId, boolean solved, LocalDateTime createdAt, - List quizQuestions + List quizQuestions ) { - public record QuizQuestionInfo( + public record QuizQuestionDto( Long id, QuizCategory quizCategory, QuizAnswerOption quizAnswerOption, String questionContent, String questionExample, - List quizOptions + List quizOptions ) { - public record QuizOptionInfo( + public record QuizOptionDto( Long id, Long wordId, String content, diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleQuizInfo.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleQuizDto.java similarity index 62% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleQuizInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleQuizDto.java index 6afbf1c7..0d88063c 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleQuizInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleQuizDto.java @@ -4,14 +4,14 @@ import java.time.LocalDateTime; import java.util.List; -public record SimpleQuizInfo( +public record SimpleQuizDto( Long id, Long accountId, boolean solved, LocalDateTime createdAt, - List quizQuestions + List quizQuestions ) { - public record QuizQuestionInfo(QuizCategory quizCategory, String questionExample) { + public record QuizQuestionDto(QuizCategory quizCategory, String questionExample) { } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleTodayQuizInfo.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleTodayQuizDto.java similarity index 92% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleTodayQuizInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleTodayQuizDto.java index b130b8ca..24b62fe1 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleTodayQuizInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/SimpleTodayQuizDto.java @@ -4,7 +4,7 @@ import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import java.time.LocalDateTime; -public record SimpleTodayQuizInfo( +public record SimpleTodayQuizDto( Long id, QuizCategory quizCategory, String question, diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/TodayQuizInfo.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/TodayQuizDto.java similarity index 68% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/TodayQuizInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/TodayQuizDto.java index c0fa2e27..bae66bb2 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/TodayQuizInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/TodayQuizDto.java @@ -4,16 +4,16 @@ import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import java.util.List; -public record TodayQuizInfo( +public record TodayQuizDto( Long id, QuizCategory quizCategory, String question, String questionContent, TodayQuizAnswerOption todayQuizAnswerOption, - List todayQuizOptions + List todayQuizOptions ) { - public record TodayQuizOptionInfo(Long id, Long wordId, String content, int optionOrder) { + public record TodayQuizOptionDto(Long id, Long wordId, String content, int optionOrder) { } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/QuizDtoMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/QuizDtoMapper.java new file mode 100644 index 00000000..00e05959 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/QuizDtoMapper.java @@ -0,0 +1,66 @@ +package com.dnd.spaced.core.quiz.domain.dto.mapper; + +import com.dnd.spaced.core.quiz.domain.Quiz; +import com.dnd.spaced.core.quiz.domain.QuizOption; +import com.dnd.spaced.core.quiz.domain.QuizQuestion; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto.QuizQuestionDto; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto.QuizQuestionDto.QuizOptionDto; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class QuizDtoMapper { + + public static QuizDto toDto(Quiz quiz, Map> quizOptionMap) { + List quizQuestions = quiz.getQuizQuestions() + .stream() + .map(quizQuestion -> toQuizQuestionDto(quizQuestion, quizOptionMap.get(quizQuestion.getId()))) + .toList(); + + return new QuizDto( + quiz.getId(), + quiz.getAccountId(), + quiz.isSolved(), + quiz.getCreatedAt(), + quizQuestions + ); + } + + private static QuizQuestionDto toQuizQuestionDto(QuizQuestion quizQuestion, List quizOptions) { + if (quizOptions == null) { + return new QuizQuestionDto( + quizQuestion.getId(), + quizQuestion.getQuizCategory(), + quizQuestion.getQuizAnswerOption(), + quizQuestion.getQuestion(), + quizQuestion.getPassage(), + Collections.emptyList() + ); + } + List quizOptionDtos = quizOptions.stream() + .map(QuizDtoMapper::toQuizOptionDto) + .toList(); + + return new QuizQuestionDto( + quizQuestion.getId(), + quizQuestion.getQuizCategory(), + quizQuestion.getQuizAnswerOption(), + quizQuestion.getQuestion(), + quizQuestion.getPassage(), + quizOptionDtos + ); + } + + private static QuizOptionDto toQuizOptionDto(QuizOption quizOption) { + return new QuizOptionDto( + quizOption.getId(), + quizOption.getWordId(), + quizOption.getContent(), + quizOption.getOptionOrder() + ); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/QuizInfoMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/QuizInfoMapper.java deleted file mode 100644 index 787806c2..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/QuizInfoMapper.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.dnd.spaced.core.quiz.domain.dto.mapper; - -import com.dnd.spaced.core.quiz.domain.Quiz; -import com.dnd.spaced.core.quiz.domain.QuizOption; -import com.dnd.spaced.core.quiz.domain.QuizQuestion; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo.QuizQuestionInfo; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo.QuizQuestionInfo.QuizOptionInfo; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class QuizInfoMapper { - - public static QuizInfo toDto(Quiz quiz) { - return new QuizInfo( - quiz.getId(), - quiz.getAccountId(), - quiz.isSolved(), - quiz.getCreatedAt(), - Collections.emptyList() - ); - } - - public static QuizInfo toDto(Quiz quiz, Map> quizOptionMap) { - List quizQuestions = quiz.getQuizQuestions() - .stream() - .map(quizQuestion -> toQuizQuestionDto(quizQuestion, quizOptionMap.get(quizQuestion.getId()))) - .toList(); - - return new QuizInfo( - quiz.getId(), - quiz.getAccountId(), - quiz.isSolved(), - quiz.getCreatedAt(), - quizQuestions - ); - } - - private static QuizQuestionInfo toQuizQuestionDto(QuizQuestion quizQuestion, List quizOptions) { - if (quizOptions == null) { - return new QuizQuestionInfo( - quizQuestion.getId(), - quizQuestion.getQuizCategory(), - quizQuestion.getQuizAnswerOption(), - quizQuestion.getQuestion(), - quizQuestion.getPassage(), - Collections.emptyList() - ); - } - List quizOptionInfos = quizOptions.stream() - .map(QuizInfoMapper::toQuizOptionDto) - .toList(); - - return new QuizQuestionInfo( - quizQuestion.getId(), - quizQuestion.getQuizCategory(), - quizQuestion.getQuizAnswerOption(), - quizQuestion.getQuestion(), - quizQuestion.getPassage(), - quizOptionInfos - ); - } - - private static QuizOptionInfo toQuizOptionDto(QuizOption quizOption) { - return new QuizOptionInfo( - quizOption.getId(), - quizOption.getWordId(), - quizOption.getContent(), - quizOption.getOptionOrder() - ); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/TodayQuizInfoMapper.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/TodayQuizDtoMapper.java similarity index 55% rename from space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/TodayQuizInfoMapper.java rename to space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/TodayQuizDtoMapper.java index 85bcaf42..38527b36 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/TodayQuizInfoMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/dto/mapper/TodayQuizDtoMapper.java @@ -2,24 +2,23 @@ import com.dnd.spaced.core.quiz.domain.TodayQuiz; import com.dnd.spaced.core.quiz.domain.TodayQuizOption; -import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.TodayQuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.TodayQuizInfo.TodayQuizOptionInfo; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import com.dnd.spaced.core.quiz.domain.dto.TodayQuizDto; +import com.dnd.spaced.core.quiz.domain.dto.TodayQuizDto.TodayQuizOptionDto; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizAnswerOption; import com.dnd.spaced.core.quiz.domain.embed.TodayQuizQuestion; import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; +import com.dnd.spaced.global.mapper.Mapper; import java.time.LocalDateTime; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class TodayQuizInfoMapper { +@Mapper +public class TodayQuizDtoMapper { - public static SimpleTodayQuizInfo toDto(TodayQuiz todayQuiz) { + public SimpleTodayQuizDto toDto(TodayQuiz todayQuiz) { TodayQuizQuestion todayQuizQuestion = todayQuiz.getTodayQuizQuestion(); - return new SimpleTodayQuizInfo( + return new SimpleTodayQuizDto( todayQuiz.getId(), todayQuizQuestion.getQuizCategory(), todayQuizQuestion.getQuestion(), @@ -29,7 +28,7 @@ public static SimpleTodayQuizInfo toDto(TodayQuiz todayQuiz) { ); } - public static SimpleTodayQuizInfo toDto( + public SimpleTodayQuizDto toDto( Long id, LocalDateTime createdAt, String question, @@ -40,27 +39,29 @@ public static SimpleTodayQuizInfo toDto( ) { TodayQuizAnswerOption todayQuizAnswerOption = new TodayQuizAnswerOption(answerWordId, answerContent); - return new SimpleTodayQuizInfo(id, quizCategory, question, questionContent, todayQuizAnswerOption, createdAt); + return new SimpleTodayQuizDto(id, quizCategory, question, questionContent, todayQuizAnswerOption, createdAt); } - public static TodayQuizInfo toDto(TodayQuiz todayQuiz, List todayQuizOptions) { + public TodayQuizDto toDto(TodayQuiz todayQuiz, List todayQuizOptions) { TodayQuizQuestion todayQuizQuestion = todayQuiz.getTodayQuizQuestion(); - List todayQuizOptionInfos = todayQuizOptions.stream() - .map( - option -> new TodayQuizOptionInfo( - option.getId(), - option.getWordId(), option.getContent(), - option.getOptionOrder() - )) - .toList(); + List todayQuizOptionDtos = todayQuizOptions.stream() + .map( + option -> new TodayQuizOptionDto( + option.getId(), + option.getWordId(), + option.getContent(), + option.getOptionOrder() + ) + ) + .toList(); - return new TodayQuizInfo( + return new TodayQuizDto( todayQuiz.getId(), todayQuizQuestion.getQuizCategory(), todayQuizQuestion.getQuestion(), todayQuizQuestion.getPassage(), todayQuizQuestion.getTodayQuizAnswerOption(), - todayQuizOptionInfos + todayQuizOptionDtos ); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/QuizRepository.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/QuizRepository.java index f42cb57c..66f8b8b7 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/QuizRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/QuizRepository.java @@ -1,8 +1,8 @@ package com.dnd.spaced.core.quiz.domain.repository; import com.dnd.spaced.core.quiz.domain.Quiz; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizInfo; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Pageable; @@ -15,7 +15,7 @@ public interface QuizRepository { Optional findBy(Long quizId); - Optional findBy(Long quizId, Long accountId); + Optional findBy(Long quizId, Long accountId); - List findAllBy(Long accountId, Long lastQuizId, Pageable pageable); + List findAllBy(Long accountId, Long lastQuizId, Pageable pageable); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/TodayQuizRepository.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/TodayQuizRepository.java index e2ceb6ac..573b0410 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/TodayQuizRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/repository/TodayQuizRepository.java @@ -1,14 +1,14 @@ package com.dnd.spaced.core.quiz.domain.repository; import com.dnd.spaced.core.quiz.domain.TodayQuiz; -import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizInfo; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; import java.util.Optional; public interface TodayQuizRepository { TodayQuiz save(TodayQuiz todayQuiz); - Optional findLatest(); + Optional findLatest(); Optional findTodayQuizBy(Long todayQuizId); diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/service/QuizWordCountValidator.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/service/QuizWordCountValidator.java new file mode 100644 index 00000000..aa5808e0 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/domain/service/QuizWordCountValidator.java @@ -0,0 +1,47 @@ +package com.dnd.spaced.core.quiz.domain.service; + +import com.dnd.spaced.core.quiz.application.exception.QuizCategoryNotFoundException; +import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; +import com.dnd.spaced.core.word.domain.WordMetadata; +import java.util.EnumMap; +import java.util.Map; +import java.util.function.BiPredicate; + +public class QuizWordCountValidator { + + private final Map> validators; + + public static QuizWordCountValidator create() { + Map> validators = createValidators(); + + return new QuizWordCountValidator(validators); + } + + private static Map> createValidators() { + Map> validators = new EnumMap<>(QuizCategory.class); + + validators.put(QuizCategory.BUSINESS, WordMetadata::canGenerateBusinessQuiz); + validators.put(QuizCategory.DESIGN, WordMetadata::canGenerateDesignQuiz); + validators.put(QuizCategory.DEVELOP, WordMetadata::canGenerateDevelopQuiz); + validators.put(QuizCategory.TOTAL, WordMetadata::canGenerateTotalQuiz); + return validators; + } + + private QuizWordCountValidator(Map> validators) { + this.validators = validators; + } + + public boolean isValidate(QuizCategory category, WordMetadata wordMetadata, int requiredWordCount) { + BiPredicate validator = validators.get(category); + + if (validator == null) { + throw new QuizCategoryNotFoundException("지정한 퀴즈 카테고리를 찾을 수 없습니다."); + } + + return validator.test(wordMetadata, requiredWordCount); + } + + public boolean isInvalidate(QuizCategory category, WordMetadata wordMetadata, int requiredWordCount) { + return !isValidate(category, wordMetadata, requiredWordCount); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/QuizGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/QuizGatewayRepository.java index 6b135657..1ee57c63 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/QuizGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/QuizGatewayRepository.java @@ -6,10 +6,10 @@ import com.dnd.spaced.core.quiz.domain.Quiz; import com.dnd.spaced.core.quiz.domain.QuizOption; import com.dnd.spaced.core.quiz.domain.QuizQuestion; -import com.dnd.spaced.core.quiz.domain.dto.QuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizInfo.QuizQuestionInfo; -import com.dnd.spaced.core.quiz.domain.dto.mapper.QuizInfoMapper; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto.QuizQuestionDto; +import com.dnd.spaced.core.quiz.domain.dto.mapper.QuizDtoMapper; import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.quiz.domain.repository.QuizRepository; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -77,7 +77,7 @@ public Optional findBy(Long quizId) { } @Override - public Optional findBy(Long quizId, Long accountId) { + public Optional findBy(Long quizId, Long accountId) { Quiz result = findQuizFetchJoinWithQuizQuestions(quizId, accountId); if (result == null) { @@ -86,13 +86,13 @@ public Optional findBy(Long quizId, Long accountId) { List quizQuestionId = findQuizQuestionIds(result); Map> quizOptionMap = findQuizOptions(quizQuestionId); - QuizInfo quizInfo = QuizInfoMapper.toDto(result, quizOptionMap); + QuizDto quizDto = QuizDtoMapper.toDto(result, quizOptionMap); - return Optional.of(quizInfo); + return Optional.of(quizDto); } @Override - public List findAllBy(Long accountId, Long lastQuizId, Pageable pageable) { + public List findAllBy(Long accountId, Long lastQuizId, Pageable pageable) { String sql = calculateFindAllSql(lastQuizId); MapSqlParameterSource sqlParameters = calculateSqlParameters(accountId, lastQuizId, pageable); @@ -106,7 +106,7 @@ public List findAllBy(Long accountId, Long lastQuizId, Pageable simpleQuizValue.createdAt ), Collectors.mapping( - simpleQuizValue -> new QuizQuestionInfo( + simpleQuizValue -> new QuizQuestionDto( simpleQuizValue.quizCategory, simpleQuizValue.questionContent ), @@ -115,7 +115,7 @@ public List findAllBy(Long accountId, Long lastQuizId, Pageable )) .entrySet() .stream() - .map(entry -> new SimpleQuizInfo( + .map(entry -> new SimpleQuizDto( entry.getKey().id, entry.getKey().accountId, entry.getKey().solved, diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/TodayQuizGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/TodayQuizGatewayRepository.java index 3c3363b6..5bd996d0 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/TodayQuizGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/infrastructure/persistence/TodayQuizGatewayRepository.java @@ -3,12 +3,15 @@ import static com.dnd.spaced.core.quiz.domain.QTodayQuiz.todayQuiz; import com.dnd.spaced.core.quiz.domain.TodayQuiz; -import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizInfo; -import com.dnd.spaced.core.quiz.domain.dto.mapper.TodayQuizInfoMapper; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import com.dnd.spaced.core.quiz.domain.embed.TodayQuizAnswerOption; import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; import com.dnd.spaced.global.consts.CacheConst; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -22,16 +25,7 @@ @RequiredArgsConstructor public class TodayQuizGatewayRepository implements TodayQuizRepository { - private static final RowMapper simpleTodayQuizInfoRowMapper = - (rs, ignoreRowNum) -> TodayQuizInfoMapper.toDto( - rs.getLong(1), - rs.getTimestamp(2).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(), - rs.getString(3), - rs.getString(4), - QuizCategory.valueOf(rs.getString(5)), - rs.getString(6), - rs.getLong(7) - ); + private static final RowMapper simpleTodayQuizDtoRowMapper = new SimpleTodayQuizDtoMapper(); private final JdbcTemplate jdbcTemplate; private final JPAQueryFactory queryFactory; @@ -48,7 +42,7 @@ public TodayQuiz save(TodayQuiz todayQuiz) { key = "'" + CacheConst.TODAY_QUIZ_CACHE_NAME + "'", cacheManager = "memoryCacheManager" ) - public Optional findLatest() { + public Optional findLatest() { String sql = """ SELECT tq.id, @@ -66,9 +60,9 @@ public Optional findLatest() { ) t left join today_quizzes tq ON t.id = tq.id; """; try { - SimpleTodayQuizInfo simpleTodayQuizInfo = jdbcTemplate.queryForObject(sql, simpleTodayQuizInfoRowMapper); + SimpleTodayQuizDto simpleTodayQuizDto = jdbcTemplate.queryForObject(sql, simpleTodayQuizDtoRowMapper); - return Optional.of(simpleTodayQuizInfo); + return Optional.of(simpleTodayQuizDto); } catch (IncorrectResultSizeDataAccessException ignored) { return Optional.empty(); } @@ -92,4 +86,31 @@ public Optional findWithTodayQuizOptionBy(Long todayQuizId) { return Optional.ofNullable(result); } + + private static class SimpleTodayQuizDtoMapper implements RowMapper { + + @Override + public SimpleTodayQuizDto mapRow(ResultSet rs, int ignoreRowNum) throws SQLException { + Long id = rs.getLong(1); + LocalDateTime createdAt = rs.getTimestamp(2) + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + String question = rs.getString(3); + String questionContent = rs.getString(4); + QuizCategory quizCategory = QuizCategory.valueOf(rs.getString(5)); + String answerContent = rs.getString(6); + Long answerWordId = rs.getLong(7); + TodayQuizAnswerOption todayQuizAnswerOption = new TodayQuizAnswerOption(answerWordId, answerContent); + + return new SimpleTodayQuizDto( + id, + quizCategory, + question, + questionContent, + todayQuizAnswerOption, + createdAt + ); + } + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/QuizController.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/QuizController.java index 801d2fb7..ae35cd08 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/QuizController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/QuizController.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.quiz.presentation; -import com.dnd.spaced.core.quiz.application.QuizService; +import com.dnd.spaced.core.quiz.application.QuizServiceFacade; import com.dnd.spaced.core.quiz.application.dto.request.CreateQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.ReadAllQuizRequest; @@ -8,8 +8,8 @@ import com.dnd.spaced.core.quiz.application.dto.response.QuizGradedAnswerCollectionResponse; import com.dnd.spaced.core.quiz.application.dto.response.QuizCollectionResponse; import com.dnd.spaced.core.quiz.application.dto.response.QuizResponse; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; import com.dnd.spaced.global.resolver.quiz.GradedAnswerPageable; import com.dnd.spaced.global.resolver.quiz.QuizPageable; import jakarta.validation.Valid; @@ -30,14 +30,14 @@ @RequiredArgsConstructor public class QuizController { - private final QuizService quizService; + private final QuizServiceFacade quizServiceFacade; @PostMapping public ResponseEntity createQuiz( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @Valid @RequestBody CreateQuizRequest request ) { - Long savedQuizId = quizService.createQuiz(accountInfo.accountId(), request); + Long savedQuizId = quizServiceFacade.createQuiz(accountId.id(), request); URI location = UriComponentsBuilder.fromPath("/quizzes/{quizId}") .buildAndExpand(savedQuizId) .toUri(); @@ -48,11 +48,11 @@ public ResponseEntity createQuiz( @PostMapping("/{quizId}/graded-answers") public ResponseEntity gradeQuiz( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @PathVariable Long quizId, @Valid @RequestBody GradeQuizRequest request ) { - quizService.grade(accountInfo.accountId(), quizId, request); + quizServiceFacade.grade(accountId.id(), quizId, request); URI location = UriComponentsBuilder.fromPath("/quizzes/{id}/graded-answer") .buildAndExpand(quizId) .toUri(); @@ -63,12 +63,12 @@ public ResponseEntity gradeQuiz( @GetMapping("/graded-answers") public ResponseEntity readQuizGradedAnswers( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, ReadQuizGradedAnswerSearchRequest request, @GradedAnswerPageable Pageable pageable ) { - QuizGradedAnswerCollectionResponse response = quizService.readGradedAnswers( - accountInfo.accountId(), + QuizGradedAnswerCollectionResponse response = quizServiceFacade.readGradedAnswers( + accountId.id(), request, pageable ); @@ -78,31 +78,31 @@ public ResponseEntity readQuizGradedAnswers( @GetMapping("/{quizId}/graded-answers") public ResponseEntity readTargetQuizGradedAnswers( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @PathVariable Long quizId ) { - QuizGradedAnswerCollectionResponse response = quizService.readGradedAnswers(accountInfo.accountId(), quizId); + QuizGradedAnswerCollectionResponse response = quizServiceFacade.readGradedAnswers(accountId.id(), quizId); return ResponseEntity.ok(response); } @GetMapping("/{quizId}") public ResponseEntity readQuiz( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @PathVariable Long quizId ) { - QuizResponse response = quizService.readQuiz(accountInfo.accountId(), quizId); + QuizResponse response = quizServiceFacade.readQuiz(accountId.id(), quizId); return ResponseEntity.ok(response); } @GetMapping public ResponseEntity readQuizzes( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, ReadAllQuizRequest request, @QuizPageable Pageable pageable ) { - QuizCollectionResponse response = quizService.readQuizzes(accountInfo.accountId(), request, pageable); + QuizCollectionResponse response = quizServiceFacade.readQuizzes(accountId.id(), request, pageable); return ResponseEntity.ok(response); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/TodayQuizController.java b/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/TodayQuizController.java index 24707731..6eabea30 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/TodayQuizController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/quiz/presentation/TodayQuizController.java @@ -1,15 +1,15 @@ package com.dnd.spaced.core.quiz.presentation; -import com.dnd.spaced.core.quiz.application.TodayQuizService; +import com.dnd.spaced.core.quiz.application.TodayQuizServiceFacade; import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.ReadTodayQuizGradedAnswerSearchRequest; import com.dnd.spaced.core.quiz.application.dto.response.SimpleTodayQuizResponse; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizGradedAnswerCollectionResponse; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizGradedAnswerResponse; import com.dnd.spaced.core.quiz.application.dto.response.TodayQuizResponse; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.GuestAccountInfo; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.GuestAccountId; import com.dnd.spaced.global.resolver.quiz.GradedAnswerPageable; import jakarta.validation.Valid; import java.net.URI; @@ -29,30 +29,30 @@ @RequiredArgsConstructor public class TodayQuizController { - private final TodayQuizService todayQuizService; + private final TodayQuizServiceFacade todayQuizServiceFacade; @GetMapping("/latest") public ResponseEntity readLatestTodayQuiz() { - return ResponseEntity.ok(todayQuizService.readLatestTodayQuiz()); + return ResponseEntity.ok(todayQuizServiceFacade.readLatestTodayQuiz()); } @GetMapping("/{todayQuizId}") public ResponseEntity readTodayQuiz( - @CurrentAccountInfo GuestAccountInfo accountInfo, + @CurrentAccount GuestAccountId accountId, @PathVariable Long todayQuizId ) { return ResponseEntity.ok( - todayQuizService.readTodayQuiz(accountInfo.accountId(), todayQuizId) + todayQuizServiceFacade.readTodayQuiz(accountId.id(), todayQuizId) ); } @PostMapping("/{todayQuizId}/graded-answers") public ResponseEntity gradeTodayQuiz( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @PathVariable Long todayQuizId, @Valid @RequestBody GradeTodayQuizRequest request ) { - todayQuizService.grade(accountInfo.accountId(), todayQuizId, request); + todayQuizServiceFacade.gradeTodayQuiz(accountId.id(), todayQuizId, request); URI location = UriComponentsBuilder.fromPath("/today-quizzes/{id}/graded-answers") .buildAndExpand(todayQuizId) .toUri(); @@ -63,20 +63,20 @@ public ResponseEntity gradeTodayQuiz( @GetMapping("/{todayQuizId}/graded-answers") public ResponseEntity readTargetTodayQuizGradedAnswers( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @PathVariable Long todayQuizId ) { - return ResponseEntity.ok(todayQuizService.readTargetTodayQuizGradedAnswers(accountInfo.accountId(), todayQuizId)); + return ResponseEntity.ok(todayQuizServiceFacade.readTargetTodayQuizGradedAnswers(accountId.id(), todayQuizId)); } @GetMapping("/graded-answers") public ResponseEntity readTodayQuizGradedAnswers( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, ReadTodayQuizGradedAnswerSearchRequest request, @GradedAnswerPageable Pageable pageable ) { return ResponseEntity.ok( - todayQuizService.readTodayQuizGradedAnswers(accountInfo.accountId(), request, pageable) + todayQuizServiceFacade.readTodayQuizGradedAnswers(accountId.id(), request, pageable) ); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/report/application/ReportService.java b/space-d/src/main/java/com/dnd/spaced/core/report/application/ReportService.java index 9d51c8d5..365e1f8b 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/report/application/ReportService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/report/application/ReportService.java @@ -33,12 +33,16 @@ private Comment findComment(ReportRequest request) { } private void processReport(Comment comment, Long reporterId, ReportRequest request) { - validateReportedComment(comment, reporterId); + validateSelfReporting(comment, reporterId); ReportReason reportReason = findReportReason(request); - Report report = new Report(reportReason, request.commentId(), reporterId); + persistReport(reporterId, request, reportReason); + } - reportRepository.save(report); + private void validateSelfReporting(Comment comment, Long reporterId) { + if (comment.isWriter(reporterId)) { + throw new CannotReportOwnCommentException("자신이 작성한 댓글은 신고할 수 없습니다."); + } } private ReportReason findReportReason(ReportRequest request) { @@ -50,9 +54,9 @@ private ReportReason findReportReason(ReportRequest request) { ); } - private void validateReportedComment(Comment comment, Long reporterId) { - if (comment.isWriter(reporterId)) { - throw new CannotReportOwnCommentException("자신이 작성한 댓글은 신고할 수 없습니다."); - } + private void persistReport(Long reporterId, ReportRequest request, ReportReason reportReason) { + Report report = new Report(reportReason, request.commentId(), reporterId); + + reportRepository.save(report); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/report/domain/Report.java b/space-d/src/main/java/com/dnd/spaced/core/report/domain/Report.java index 1d35a4e4..c8959d3c 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/report/domain/Report.java +++ b/space-d/src/main/java/com/dnd/spaced/core/report/domain/Report.java @@ -34,13 +34,12 @@ public class Report extends CreateTimeEntity { private Long reporterId; @Enumerated(EnumType.STRING) - private ReportStatus reportStatus; + private ReportStatus reportStatus = ReportStatus.PENDING; public Report(ReportReason reportReason, Long commentId, Long reporterId) { this.reportReason = reportReason; this.commentId = commentId; this.reporterId = reporterId; - this.reportStatus = ReportStatus.PENDING; } public void process(ReportStatus reportStatus) { @@ -48,6 +47,6 @@ public void process(ReportStatus reportStatus) { } public boolean isProcessed() { - return this.reportStatus == ReportStatus.PROCESSED; + return this.reportStatus.isProcessed(); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/report/domain/enums/ReportStatus.java b/space-d/src/main/java/com/dnd/spaced/core/report/domain/enums/ReportStatus.java index 3c0e77c5..9eb5fdd9 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/report/domain/enums/ReportStatus.java +++ b/space-d/src/main/java/com/dnd/spaced/core/report/domain/enums/ReportStatus.java @@ -22,7 +22,7 @@ public static Optional findBy(String name) { .findAny(); } - public boolean isProcess() { + public boolean isProcessed() { return this == PROCESSED; } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/report/presentation/ReportController.java b/space-d/src/main/java/com/dnd/spaced/core/report/presentation/ReportController.java index 1aeb6275..f28f7c11 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/report/presentation/ReportController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/report/presentation/ReportController.java @@ -2,8 +2,8 @@ import com.dnd.spaced.core.report.application.ReportService; import com.dnd.spaced.core.report.application.dto.request.ReportRequest; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; import com.dnd.spaced.global.consts.controller.ResponseEntityConst; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -22,10 +22,10 @@ public class ReportController { @PostMapping public ResponseEntity report( - @CurrentAccountInfo AuthAccountInfo accountInfo, + @CurrentAccount AuthAccountId accountId, @Valid @RequestBody ReportRequest request ) { - reportService.report(accountInfo.accountId(), request); + reportService.report(accountId.id(), request); return ResponseEntityConst.NO_CONTENT; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/skill/application/SkillService.java b/space-d/src/main/java/com/dnd/spaced/core/skill/application/SkillService.java index f0198902..100d2af8 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/skill/application/SkillService.java +++ b/space-d/src/main/java/com/dnd/spaced/core/skill/application/SkillService.java @@ -18,16 +18,17 @@ public class SkillService { private final SkillRepository skillRepository; private final QuizMetadataRepository quizMetadataRepository; + private final SkillApplicationMapper mapper; public SkillResponse readSkill(Long accountId) { QuizMetadata quizMetadata = findQuizMetadata(); return skillRepository.findBy(accountId) - .map(skill -> handleFoundSkill(skill, quizMetadata)) - .orElseGet(() -> SkillApplicationMapper.toDto(accountId)); + .map(skill -> buildSkillResponse(skill, quizMetadata)) + .orElseGet(() -> mapper.toDefaultDto(accountId)); } - private SkillResponse handleFoundSkill(Skill skill, QuizMetadata quizMetadata) { + private SkillResponse buildSkillResponse(Skill skill, QuizMetadata quizMetadata) { double totalQuizQuestionCorrectPercent = skill.calculateQuizQuestionCorrectPercent( quizMetadata.getTotalQuizQuestionCount() ); @@ -35,7 +36,7 @@ private SkillResponse handleFoundSkill(Skill skill, QuizMetadata quizMetadata) { quizMetadata.getTotalTodayQuizQuestionCount() ); - return SkillApplicationMapper.toDto( + return mapper.toDefaultDto( skill, totalQuizQuestionCorrectPercent, totalTodayQuizQuestionCorrectPercent diff --git a/space-d/src/main/java/com/dnd/spaced/core/skill/application/dto/SkillApplicationMapper.java b/space-d/src/main/java/com/dnd/spaced/core/skill/application/dto/SkillApplicationMapper.java index ae0b9e3e..7198389b 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/skill/application/dto/SkillApplicationMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/skill/application/dto/SkillApplicationMapper.java @@ -2,13 +2,12 @@ import com.dnd.spaced.core.skill.application.dto.response.SkillResponse; import com.dnd.spaced.core.skill.domain.Skill; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import com.dnd.spaced.global.mapper.Mapper; -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class SkillApplicationMapper { +@Mapper +public class SkillApplicationMapper { - public static SkillResponse toDto( + public SkillResponse toDefaultDto( Skill skill, double totalQuizQuestionCorrectPercent, double totalTodayQuizQuestionCorrectPercent @@ -24,7 +23,7 @@ public static SkillResponse toDto( ); } - public static SkillResponse toDto(Long accountId) { + public SkillResponse toDefaultDto(Long accountId) { return new SkillResponse( accountId, 0L, diff --git a/space-d/src/main/java/com/dnd/spaced/core/skill/domain/Skill.java b/space-d/src/main/java/com/dnd/spaced/core/skill/domain/Skill.java index 1235f056..cadea3fa 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/skill/domain/Skill.java +++ b/space-d/src/main/java/com/dnd/spaced/core/skill/domain/Skill.java @@ -27,32 +27,32 @@ public class Skill { private Long accountId; - private long submitQuizQuestionCount = 0; + private long submitQuizQuestionCount = 0L; - private long quizQuestionCorrectCount = 0; + private long quizQuestionCorrectCount = 0L; - private long submitTodayQuizQuestionCount = 0; + private long submitTodayQuizQuestionCount = 0L; - private long todayQuizQuestionCorrectCount = 0; + private long todayQuizQuestionCorrectCount = 0L; public Skill(Long accountId) { this.accountId = accountId; } public double calculateQuizQuestionCorrectPercent(long totalQuizQuestionCount) { - if (totalQuizQuestionCount == 0L || quizQuestionCorrectCount == 0L) { + if (hasNeverAttemptedQuiz(totalQuizQuestionCount)) { return 0.0d; } - return ((double) quizQuestionCorrectCount / totalQuizQuestionCount) * PERCENT; + return calculateQuizCorrectPercent(totalQuizQuestionCount); } public double calculateTodayQuizQuestionCorrectPercent(long totalTodayQuizQuestionCount) { - if (totalTodayQuizQuestionCount == 0L || todayQuizQuestionCorrectCount == 0L) { + if (hasNeverAttemptedTodayQuiz(totalTodayQuizQuestionCount)) { return 0.0d; } - return ((double) todayQuizQuestionCorrectCount / totalTodayQuizQuestionCount) * PERCENT; + return calculateTodayQuizCorrectPercent(totalTodayQuizQuestionCount); } public void addCorrectQuizQuestion(long correctCount) { @@ -64,4 +64,20 @@ public void addCorrectTodayQuizQuestion(long correctCount) { this.submitTodayQuizQuestionCount += TODAY_QUIZ_QUESTION_COUNT; this.todayQuizQuestionCorrectCount += correctCount; } + + private boolean hasNeverAttemptedQuiz(long totalQuizQuestionCount) { + return totalQuizQuestionCount == 0L || quizQuestionCorrectCount == 0L; + } + + private double calculateQuizCorrectPercent(long totalQuizQuestionCount) { + return ((double) quizQuestionCorrectCount / totalQuizQuestionCount) * PERCENT; + } + + private boolean hasNeverAttemptedTodayQuiz(long totalTodayQuizQuestionCount) { + return totalTodayQuizQuestionCount == 0L || todayQuizQuestionCorrectCount == 0L; + } + + private double calculateTodayQuizCorrectPercent(long totalTodayQuizQuestionCount) { + return ((double) todayQuizQuestionCorrectCount / totalTodayQuizQuestionCount) * PERCENT; + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/skill/presentation/SkillController.java b/space-d/src/main/java/com/dnd/spaced/core/skill/presentation/SkillController.java index 8c2440dc..4523a5c6 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/skill/presentation/SkillController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/skill/presentation/SkillController.java @@ -2,8 +2,8 @@ import com.dnd.spaced.core.skill.application.SkillService; import com.dnd.spaced.core.skill.application.dto.response.SkillResponse; -import com.dnd.spaced.global.auth.resolver.CurrentAccountInfo; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfo; +import com.dnd.spaced.global.auth.resolver.CurrentAccount; +import com.dnd.spaced.global.auth.resolver.AuthAccountId; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -18,8 +18,8 @@ public class SkillController { private final SkillService skillService; @GetMapping - public ResponseEntity readSkill(@CurrentAccountInfo AuthAccountInfo accountInfo) { - SkillResponse response = skillService.readSkill(accountInfo.accountId()); + public ResponseEntity readSkill(@CurrentAccount AuthAccountId accountId) { + SkillResponse response = skillService.readSkill(accountId.id()); return ResponseEntity.ok(response); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/ReadPopularWordService.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/ReadPopularWordService.java new file mode 100644 index 00000000..4535a016 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/ReadPopularWordService.java @@ -0,0 +1,21 @@ +package com.dnd.spaced.core.word.application; + +import com.dnd.spaced.core.word.domain.dto.PopularWord; +import com.dnd.spaced.core.word.domain.repository.PopularWordRepository; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class ReadPopularWordService { + + private final Clock clock; + private final PopularWordRepository popularWordRepository; + + public List readPopularWords() { + return popularWordRepository.findAllBy(LocalDateTime.now(clock)); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/ReadWordViewService.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/ReadWordViewService.java new file mode 100644 index 00000000..f3fa11ef --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/ReadWordViewService.java @@ -0,0 +1,38 @@ +package com.dnd.spaced.core.word.application; + +import com.dnd.spaced.core.word.application.dto.request.ReadAllWordRequest; +import com.dnd.spaced.core.word.application.exception.WordNotFoundException; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.enums.Category; +import com.dnd.spaced.core.word.domain.repository.WordViewRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class ReadWordViewService { + + private static final Category NO_CATEGORY_FILTER = null; + private static final Category NO_LAST_CATEGORY_FILTER = null; + + private final WordViewRepository wordViewRepository; + + public WordView readWord(Long wordId) { + return wordViewRepository.findBy(wordId) + .orElseThrow( + () -> new WordNotFoundException( + "지정한 ID에 해당하는 용어를 찾을 수 없습니다." + ) + ); + } + + public List readWords(ReadAllWordRequest request, Pageable pageable) { + Category category = Category.findBy(request.categoryName()) + .orElse(NO_CATEGORY_FILTER); + Category lastCategory = Category.findBy(request.lastCategoryName()) + .orElse(NO_LAST_CATEGORY_FILTER); + return wordViewRepository.findAllBy(category, request.lastWordName(), lastCategory, pageable); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/SearchWordViewService.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/SearchWordViewService.java new file mode 100644 index 00000000..c4a5b5b1 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/SearchWordViewService.java @@ -0,0 +1,52 @@ +package com.dnd.spaced.core.word.application; + +import com.dnd.spaced.core.word.application.dto.request.SearchWordRequest; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.enums.Category; +import com.dnd.spaced.core.word.domain.repository.WordViewRepository; +import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchCondition; +import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchPageRequest; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +class SearchWordViewService { + + private static final Category NO_CATEGORY_FILTER = null; + private static final Category NO_LAST_CATEGORY_FILTER = null; + + private final WordViewRepository wordViewRepository; + + public List searchWord(SearchWordRequest request, Pageable pageable) { + WordSearchCondition wordSearchCondition = buildWordSearchCondition( + request); + WordSearchPageRequest wordSearchPageRequest = buildWordSearchPageRequest( + request, pageable); + return wordViewRepository.search(wordSearchCondition, wordSearchPageRequest); + } + + private WordSearchCondition buildWordSearchCondition(SearchWordRequest request) { + Category category = Category.findBy(request.categoryName()) + .orElse(NO_CATEGORY_FILTER); + + return new WordSearchCondition( + request.name(), + category, + request.pronunciation() + ); + } + + private WordSearchPageRequest buildWordSearchPageRequest(SearchWordRequest request, Pageable pageable) { + Category lastCategory = Category.findBy(request.lastCategoryName()) + .orElse(NO_LAST_CATEGORY_FILTER); + + return new WordSearchPageRequest( + pageable, + request.lastWordName(), + lastCategory + ); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/WordService.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/WordService.java deleted file mode 100644 index 1795ecc0..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/word/application/WordService.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.dnd.spaced.core.word.application; - -import com.dnd.spaced.core.word.application.dto.WordApplicationMapper; -import com.dnd.spaced.core.word.application.dto.request.ReadAllWordRequest; -import com.dnd.spaced.core.word.application.dto.request.SearchWordRequest; -import com.dnd.spaced.core.word.application.dto.response.PopularWordCollectionResponse; -import com.dnd.spaced.core.word.application.dto.response.WordCollectionResponse; -import com.dnd.spaced.core.word.application.dto.response.WordResponse; -import com.dnd.spaced.core.word.application.event.dto.WordViewCountIncrementEvent; -import com.dnd.spaced.core.word.application.event.dto.WordViewCountStatisticsEvent; -import com.dnd.spaced.core.word.application.exception.WordNotFoundException; -import com.dnd.spaced.core.word.domain.dto.WordInfo; -import com.dnd.spaced.core.word.domain.enums.Category; -import com.dnd.spaced.core.word.domain.repository.PopularWordRepository; -import com.dnd.spaced.core.word.domain.repository.WordInfoRepository; -import com.dnd.spaced.core.word.domain.dto.PopularWord; -import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchCondition; -import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchPageRequest; -import java.time.Clock; -import java.time.LocalDateTime; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -public class WordService { - - private final Clock clock; - private final WordInfoRepository wordInfoRepository; - private final PopularWordRepository popularWordRepository; - private final ApplicationEventPublisher eventPublisher; - - public WordResponse readWord(Long wordId) { - WordInfo word = findWord(wordId); - - publishWordViewCountIncrementedEvent(word); - return WordApplicationMapper.toPronunciationInfoDto(word); - } - - public WordCollectionResponse readWords(ReadAllWordRequest request, Pageable pageable) { - Category category = Category.findBy(request.categoryName()) - .orElse(null); - Category lastCategory = Category.findBy(request.lastCategoryName()) - .orElse(null); - List words = wordInfoRepository.findAllBy(category, request.lastWordName(), lastCategory, pageable); - - return WordApplicationMapper.toWordCollectionDto(words); - } - - public WordCollectionResponse searchWord(SearchWordRequest request, Pageable pageable) { - Category category = Category.findBy(request.categoryName()) - .orElse(null); - Category lastCategory = Category.findBy(request.lastCategoryName()) - .orElse(null); - WordSearchCondition wordSearchCondition = new WordSearchCondition( - request.name(), - category, - request.pronunciation() - ); - WordSearchPageRequest wordSearchPageRequest = new WordSearchPageRequest( - pageable, - request.lastWordName(), - lastCategory); - List words = wordInfoRepository.search(wordSearchCondition, wordSearchPageRequest); - - return WordApplicationMapper.toWordCollectionDto(words); - } - - public PopularWordCollectionResponse readPopularWords() { - List popularWords = popularWordRepository.findAllBy(LocalDateTime.now(clock)); - - return WordApplicationMapper.toPopularWordCollectionDto(popularWords); - } - - private WordInfo findWord(Long wordId) { - return wordInfoRepository.findBy(wordId) - .orElseThrow(() -> new WordNotFoundException("지정한 ID에 해당하는 용어를 찾을 수 없습니다.")); - } - - private void publishWordViewCountIncrementedEvent(WordInfo word) { - eventPublisher.publishEvent(new WordViewCountIncrementEvent(word.id(), LocalDateTime.now(clock))); - eventPublisher.publishEvent(new WordViewCountStatisticsEvent(word.id(), LocalDateTime.now(clock))); - } -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/WordServiceFacade.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/WordServiceFacade.java new file mode 100644 index 00000000..122c3694 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/WordServiceFacade.java @@ -0,0 +1,65 @@ +package com.dnd.spaced.core.word.application; + +import com.dnd.spaced.core.word.application.dto.WordApplicationMapper; +import com.dnd.spaced.core.word.application.dto.request.ReadAllWordRequest; +import com.dnd.spaced.core.word.application.dto.request.SearchWordRequest; +import com.dnd.spaced.core.word.application.dto.response.PopularWordCollectionResponse; +import com.dnd.spaced.core.word.application.dto.response.WordCollectionResponse; +import com.dnd.spaced.core.word.application.dto.response.WordResponse; +import com.dnd.spaced.core.word.application.event.dto.WordViewCountIncrementEvent; +import com.dnd.spaced.core.word.application.event.dto.WordViewCountStatisticsEvent; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.dto.PopularWord; +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class WordServiceFacade { + + private final Clock clock; + private final ReadWordViewService readWordViewService; + private final SearchWordViewService searchWordViewService; + private final ReadPopularWordService readPopularWordService; + private final WordApplicationMapper mapper; + private final ApplicationEventPublisher eventPublisher; + + public WordResponse readWord(Long wordId) { + WordView wordView = readWordView(wordId); + + publishWordViewCountIncrementedEvent(wordView); + return mapper.toPronunciationResponse(wordView); + } + + public WordCollectionResponse readWords(ReadAllWordRequest request, Pageable pageable) { + List words = readWordViewService.readWords(request, pageable); + + return mapper.toWordCollectionResponse(words); + } + + public WordCollectionResponse searchWord(SearchWordRequest request, Pageable pageable) { + List words = searchWordViewService.searchWord(request, pageable); + + return mapper.toWordCollectionResponse(words); + } + + public PopularWordCollectionResponse readPopularWords() { + List popularWords = readPopularWordService.readPopularWords(); + + return mapper.toPopularWordCollectionResponse(popularWords); + } + + private WordView readWordView(Long wordId) { + return readWordViewService.readWord(wordId); + } + + private void publishWordViewCountIncrementedEvent(WordView word) { + eventPublisher.publishEvent(new WordViewCountIncrementEvent(word.id(), LocalDateTime.now(clock))); + eventPublisher.publishEvent(new WordViewCountStatisticsEvent(word.id(), LocalDateTime.now(clock))); + } +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/dto/WordApplicationMapper.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/dto/WordApplicationMapper.java index fade4d51..691eba16 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/application/dto/WordApplicationMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/dto/WordApplicationMapper.java @@ -5,34 +5,33 @@ import com.dnd.spaced.core.word.application.dto.response.WordCollectionResponse; import com.dnd.spaced.core.word.application.dto.response.WordResponse; import com.dnd.spaced.core.word.application.dto.response.WordResponse.PronunciationResponse; -import com.dnd.spaced.core.word.domain.dto.WordInfo; -import com.dnd.spaced.core.word.domain.dto.WordInfo.PronunciationInfo; -import com.dnd.spaced.core.word.domain.dto.WordInfo.WordExampleInfo; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.dto.WordView.PronunciationView; +import com.dnd.spaced.core.word.domain.dto.WordView.WordExampleView; import com.dnd.spaced.core.word.domain.dto.PopularWord; +import com.dnd.spaced.global.mapper.Mapper; import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Mapper public final class WordApplicationMapper { - public static WordCollectionResponse toWordCollectionDto(List words) { + public WordCollectionResponse toWordCollectionResponse(List words) { if (words.isEmpty()) { return new WordCollectionResponse(List.of(), null); } List wordResponses = words.stream() - .map(WordApplicationMapper::toPronunciationInfoDto) + .map(this::toPronunciationResponse) .toList(); return new WordCollectionResponse(wordResponses, wordResponses.get(wordResponses.size() - 1).name()); } - public static WordResponse toPronunciationInfoDto(WordInfo word) { - List pronunciations = toPronunciationInfoDto(word.pronunciations()); + public WordResponse toPronunciationResponse(WordView word) { + List pronunciations = toPronunciationResponse(word.pronunciations()); List examples = word.wordExamples() .stream() - .map(WordExampleInfo::example) + .map(WordExampleView::example) .toList(); return new WordResponse( @@ -47,7 +46,7 @@ public static WordResponse toPronunciationInfoDto(WordInfo word) { ); } - public static PopularWordCollectionResponse toPopularWordCollectionDto(List popularWords) { + public PopularWordCollectionResponse toPopularWordCollectionResponse(List popularWords) { List responses = popularWords.stream() .map( popularWord -> new PopularWordResponse( @@ -61,7 +60,7 @@ public static PopularWordCollectionResponse toPopularWordCollectionDto(List toPronunciationInfoDto(List pronunciations) { + private List toPronunciationResponse(List pronunciations) { return pronunciations.stream() .map( pronunciation -> new PronunciationResponse( diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordBookmarkCountListener.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordBookmarkCountListener.java index a52c03e5..49914c58 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordBookmarkCountListener.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordBookmarkCountListener.java @@ -26,6 +26,6 @@ public void listen(WordBookmarkCountIncrementedEvent event) { @EventListener @Transactional public void listen(WordBookmarkCountDecrementedEvent event) { - wordRepository.updateSubtractBookmarkCount(event.wordId()); + wordRepository.subtractBookmarkCount(event.wordId()); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordViewCounterEventListener.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordViewCounterEventListener.java index ade5e779..74bd4274 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordViewCounterEventListener.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/event/listener/WordViewCounterEventListener.java @@ -26,7 +26,7 @@ public class WordViewCounterEventListener { @EventListener public void listen(WordViewCountIncrementEvent event) { if (!popularWordRepository.existsBy(event.wordId(), event.localDateTime())) { - transactionTemplate.executeWithoutResult(status -> wordRepository.updateViewCount(event.wordId())); + transactionTemplate.executeWithoutResult(status -> wordRepository.addViewCount(event.wordId())); String requestId = MDC.get(REQUEST_ID); log.info("[{}] wordId : {}, localDateTime : {}", requestId, event.wordId(), event.localDateTime()); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/DeletedWordIdRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/repository/DeletedWordIdRepository.java similarity index 81% rename from space-d/src/main/java/com/dnd/spaced/core/word/application/DeletedWordIdRepository.java rename to space-d/src/main/java/com/dnd/spaced/core/word/application/repository/DeletedWordIdRepository.java index 6501e7ad..e3cc62e3 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/application/DeletedWordIdRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/repository/DeletedWordIdRepository.java @@ -1,4 +1,4 @@ -package com.dnd.spaced.core.word.application; +package com.dnd.spaced.core.word.application.repository; import java.time.LocalDateTime; import java.util.Set; diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/application/schedule/PopularWordScheduler.java b/space-d/src/main/java/com/dnd/spaced/core/word/application/schedule/PopularWordScheduler.java index 991f5d0e..33f04bb5 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/application/schedule/PopularWordScheduler.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/application/schedule/PopularWordScheduler.java @@ -54,7 +54,7 @@ private void updatePopularWordViewCount(LocalDateTime yesterday) { .toList(); List dtos = wordViewCountStatisticsRepository.findAllBy(ids, yesterday); - wordRepository.updateViewCount(dtos); + wordRepository.addViewCount(dtos); } private List calculatePopularWordInfo(List ranking, List names) { diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/Word.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/Word.java index 8c6de4a2..7a83606c 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/Word.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/Word.java @@ -57,12 +57,12 @@ public class Word extends BaseTimeEntity { @Builder private Word(String name, String meaning, String categoryName) { - validateContent(name); + validateName(name); this.name = name; this.wordMeaning = new WordMeaning(meaning); this.category = Category.findBy(categoryName) - .orElseThrow(() -> new InvalidCategoryNameException(String.format(EXCEPTION_FORMAT, name)));; + .orElseThrow(() -> new InvalidCategoryNameException(String.format(EXCEPTION_FORMAT, name))); } public void addPronunciation(Pronunciation pronunciation) { @@ -87,7 +87,7 @@ public void delete() { this.deleted = true; } - private void validateContent(String name) { + private void validateName(String name) { if (isInvalidName(name)) { throw new InvalidWordNameException("용어 이름은 null이거나 비어 있을 수 없습니다."); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/WordExample.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/WordExample.java index 0aa663bf..65cf1c20 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/WordExample.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/WordExample.java @@ -71,8 +71,4 @@ public void changeExample(String changedExample) { public void deleted() { this.deleted = true; } - - public boolean isEqualTo(Long id) { - return this.id.equals(id); - } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/SimpleWord.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/SimpleWord.java new file mode 100644 index 00000000..da84ea73 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/SimpleWord.java @@ -0,0 +1,4 @@ +package com.dnd.spaced.core.word.domain.dto; + +public record SimpleWord(Long id, String categoryName, String name, String meaning) { +} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/SimpleWordInfo.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/SimpleWordInfo.java deleted file mode 100644 index 4848fe6d..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/SimpleWordInfo.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.dnd.spaced.core.word.domain.dto; - -public record SimpleWordInfo(Long id, String categoryName, String name, String meaning) { -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/WordInfo.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/WordView.java similarity index 65% rename from space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/WordInfo.java rename to space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/WordView.java index 330661b3..e0712684 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/WordInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/WordView.java @@ -5,20 +5,20 @@ import com.dnd.spaced.core.word.domain.enums.PronunciationType; import java.util.List; -public record WordInfo( +public record WordView( Long id, String name, Category category, WordMeaning wordMeaning, - List pronunciations, - List wordExamples, + List pronunciations, + List wordExamples, long viewCount, long bookmarkCount ) { - public record PronunciationInfo(Long id, Long wordId, String content, PronunciationType type) { + public record PronunciationView(Long id, Long wordId, String content, PronunciationType type) { } - public record WordExampleInfo(Long id, Long wordId, String example) { + public record WordExampleView(Long id, Long wordId, String example) { } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/mapper/WordInfoMapper.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/mapper/WordViewMapper.java similarity index 59% rename from space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/mapper/WordInfoMapper.java rename to space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/mapper/WordViewMapper.java index 240775fa..8f4e9ede 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/mapper/WordInfoMapper.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/dto/mapper/WordViewMapper.java @@ -1,50 +1,50 @@ package com.dnd.spaced.core.word.domain.dto.mapper; -import static com.dnd.spaced.core.word.domain.dto.WordInfo.PronunciationInfo; +import static com.dnd.spaced.core.word.domain.dto.WordView.PronunciationView; import com.dnd.spaced.core.word.domain.Pronunciation; import com.dnd.spaced.core.word.domain.Word; import com.dnd.spaced.core.word.domain.WordExample; -import com.dnd.spaced.core.word.domain.dto.WordInfo; -import com.dnd.spaced.core.word.domain.dto.WordInfo.WordExampleInfo; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.dto.WordView.WordExampleView; import java.util.List; import lombok.AccessLevel; import lombok.NoArgsConstructor; @NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class WordInfoMapper { +public final class WordViewMapper { - public static WordInfo toDto(Word word, List pronunciations) { - List pronunciationInfos = pronunciations.stream() - .map(WordInfoMapper::toPronunciationInfoDto) + public static WordView toDto(Word word, List pronunciations) { + List pronunciationViews = pronunciations.stream() + .map(WordViewMapper::toPronunciationInfoDto) .toList(); - List wordExampleInfos = word.getWordExamples() + List wordExampleViews = word.getWordExamples() .stream() - .map(WordInfoMapper::toWordExampleInfoDto) + .map(WordViewMapper::toWordExampleInfoDto) .toList(); - return new WordInfo( + return new WordView( word.getId(), word.getName(), word.getCategory(), word.getWordMeaning(), - pronunciationInfos, - wordExampleInfos, + pronunciationViews, + wordExampleViews, word.getViewCount(), word.getBookmarkCount() ); } - private static WordExampleInfo toWordExampleInfoDto(WordExample wordExample) { - return new WordExampleInfo( + private static WordExampleView toWordExampleInfoDto(WordExample wordExample) { + return new WordExampleView( wordExample.getId(), wordExample.getWord().getId(), wordExample.getContent() ); } - private static PronunciationInfo toPronunciationInfoDto(Pronunciation pronunciation) { - return new PronunciationInfo( + private static PronunciationView toPronunciationInfoDto(Pronunciation pronunciation) { + return new PronunciationView( pronunciation.getId(), pronunciation.getWord().getId(), pronunciation.getContent(), diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRandomRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRandomRepository.java index dd208b0b..6297e6fc 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRandomRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRandomRepository.java @@ -2,7 +2,7 @@ import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.word.domain.Word; -import com.dnd.spaced.core.word.domain.dto.SimpleWordInfo; +import com.dnd.spaced.core.word.domain.dto.SimpleWord; import com.dnd.spaced.core.word.domain.enums.Category; import java.util.List; @@ -10,5 +10,5 @@ public interface WordRandomRepository { void saveWith(Word word, Category category); - List findRandomAllBy(QuizCategory quizCategory, long limit); + List findRandomAllBy(QuizCategory quizCategory, long limit); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRepository.java index 44a67bde..3a401811 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordRepository.java @@ -11,13 +11,13 @@ public interface WordRepository { boolean existsBy(Long wordId); - void updateViewCount(Long wordId); + void addViewCount(Long wordId); - void updateViewCount(List wordViewCountStatisticsDtos); + void addViewCount(List wordViewCountStatisticsDtos); void addBookmarkCount(Long wordId); - void updateSubtractBookmarkCount(Long wordId); + void subtractBookmarkCount(Long wordId); List findNameAllBy(Long[] wordIds); diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordInfoRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordViewRepository.java similarity index 63% rename from space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordInfoRepository.java rename to space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordViewRepository.java index 972d1172..3c855df9 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordInfoRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/domain/repository/WordViewRepository.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.word.domain.repository; -import com.dnd.spaced.core.word.domain.dto.WordInfo; +import com.dnd.spaced.core.word.domain.dto.WordView; import com.dnd.spaced.core.word.domain.enums.Category; import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchCondition; import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchPageRequest; @@ -8,11 +8,11 @@ import java.util.Optional; import org.springframework.data.domain.Pageable; -public interface WordInfoRepository { +public interface WordViewRepository { - Optional findBy(Long wordId); + Optional findBy(Long wordId); - List findAllBy(Category category, String lastWordName, Category lastCategory, Pageable pageable); + List findAllBy(Category category, String lastWordName, Category lastCategory, Pageable pageable); - List search(WordSearchCondition condition, WordSearchPageRequest pageRequest); + List search(WordSearchCondition condition, WordSearchPageRequest pageRequest); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/DeletedWordGatewayIdRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/DeletedWordGatewayIdRepository.java index 8b15de16..64a56a18 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/DeletedWordGatewayIdRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/DeletedWordGatewayIdRepository.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.word.infrastructure.persistence; -import com.dnd.spaced.core.word.application.DeletedWordIdRepository; +import com.dnd.spaced.core.word.application.repository.DeletedWordIdRepository; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Set; diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationCrudRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationCrudRepository.java deleted file mode 100644 index de289093..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationCrudRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.dnd.spaced.core.word.infrastructure.persistence; - -import com.dnd.spaced.core.word.domain.Pronunciation; -import org.springframework.data.repository.CrudRepository; - -interface PronunciationCrudRepository extends CrudRepository { -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepository.java index aea814df..daa5b0a0 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepository.java @@ -1,9 +1,8 @@ package com.dnd.spaced.core.word.infrastructure.persistence; -import static com.dnd.spaced.core.word.domain.QPronunciation.*; +import static com.dnd.spaced.core.word.domain.QPronunciation.pronunciation; import com.dnd.spaced.core.word.domain.Pronunciation; -import com.dnd.spaced.core.word.domain.QPronunciation; import com.dnd.spaced.core.word.domain.repository.PronunciationRepository; import com.querydsl.jpa.impl.JPAQueryFactory; import java.sql.Timestamp; @@ -24,23 +23,24 @@ public class PronunciationGatewayRepository implements PronunciationRepository { private final Clock clock; private final JPAQueryFactory queryFactory; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - private final PronunciationCrudRepository pronunciationCrudRepository; public PronunciationGatewayRepository( Clock clock, JdbcTemplate jdbcTemplate, - JPAQueryFactory queryFactory, - PronunciationCrudRepository pronunciationCrudRepository + JPAQueryFactory queryFactory ) { this.clock = clock; this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); this.queryFactory = queryFactory; - this.pronunciationCrudRepository = pronunciationCrudRepository; } @Override public Optional findBy(Long pronunciationId) { - return pronunciationCrudRepository.findById(pronunciationId); + Pronunciation result = queryFactory.selectFrom(pronunciation) + .where(pronunciation.id.eq(pronunciationId), pronunciation.deleted.isFalse()) + .fetchOne(); + + return Optional.ofNullable(result); } @Override @@ -68,12 +68,14 @@ INSERT INTO pronunciations(created_at, updated_at, content, pronunciation_type, @Override public long countBy(Long wordId) { String sql = """ - SELECT COUNT(id) FROM pronunciations WHERE word_id = :wordId + SELECT COUNT(id) FROM pronunciations WHERE word_id = :wordId AND deleted = false """; MapSqlParameterSource parameters = new MapSqlParameterSource(); + parameters.addValue("wordId", wordId); Long result = namedParameterJdbcTemplate.queryForObject(sql, parameters, Long.class); + return result != null ? result : 0L; } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleCrudRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleCrudRepository.java deleted file mode 100644 index f5c8f5cf..00000000 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleCrudRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.dnd.spaced.core.word.infrastructure.persistence; - -import com.dnd.spaced.core.word.domain.WordExample; -import org.springframework.data.repository.CrudRepository; - -interface WordExampleCrudRepository extends CrudRepository { -} diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepository.java index 7bf2fe58..260127f2 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepository.java @@ -24,24 +24,25 @@ public class WordExampleGatewayRepository implements WordExampleRepository { private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; private final JPAQueryFactory queryFactory; + public WordExampleGatewayRepository(Clock clock, JdbcTemplate jdbcTemplate, JPAQueryFactory queryFactory) { + this.clock = clock; + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + this.queryFactory = queryFactory; + } + @Override public Optional findBy(Long wordExampleId) { WordExample result = queryFactory.selectFrom(wordExample) - .where(wordExample.id.eq(wordExampleId)) + .where(wordExample.id.eq(wordExampleId), wordExample.deleted.isFalse()) .fetchOne(); return Optional.ofNullable(result); } - public WordExampleGatewayRepository(Clock clock, JdbcTemplate jdbcTemplate, JPAQueryFactory queryFactory) { - this.clock = clock; - this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); - this.queryFactory = queryFactory; - } - + @Override public long countBy(Long wordId) { String sql = """ - SELECT COUNT(id) FROM word_examples WHERE word_id = :wordId + SELECT COUNT(id) FROM word_examples WHERE word_id = :wordId AND deleted = false """; MapSqlParameterSource parameters = new MapSqlParameterSource(); parameters.addValue("wordId", wordId); @@ -54,7 +55,7 @@ SELECT COUNT(id) FROM word_examples WHERE word_id = :wordId public long update(Long wordExampleId, String example) { return queryFactory.update(wordExample) .set(wordExample.content, example) - .where(wordExample.id.eq(wordExampleId)) + .where(wordExample.id.eq(wordExampleId), wordExample.deleted.isFalse()) .execute(); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepository.java index 8cc9724d..b9ebdbec 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepository.java @@ -34,19 +34,19 @@ public boolean existsBy(Long wordId) { } @Override - public void updateViewCount(Long wordId) { + public void addViewCount(Long wordId) { queryFactory.update(word) .set(word.viewCount, word.viewCount.add(1)) - .where(word.id.eq(wordId)) + .where(word.id.eq(wordId), word.deleted.isFalse()) .execute(); } @Override - public void updateViewCount(List wordViewCountStatisticsDtos) { + public void addViewCount(List wordViewCountStatisticsDtos) { for (WordViewCountStatisticsDto dto : wordViewCountStatisticsDtos) { queryFactory.update(word) .set(word.viewCount, word.viewCount.add(dto.viewCount())) - .where(word.id.eq(dto.id())) + .where(word.id.eq(dto.id()), word.deleted.isFalse()) .execute(); } } @@ -55,15 +55,15 @@ public void updateViewCount(List wordViewCountStatis public void addBookmarkCount(Long wordId) { queryFactory.update(word) .set(word.bookmarkCount, word.bookmarkCount.add(1)) - .where(word.id.eq(wordId)) + .where(word.id.eq(wordId), word.deleted.isFalse()) .execute(); } @Override - public void updateSubtractBookmarkCount(Long wordId) { + public void subtractBookmarkCount(Long wordId) { queryFactory.update(word) .set(word.bookmarkCount, word.bookmarkCount.subtract(1)) - .where(word.id.eq(wordId)) + .where(word.id.eq(wordId), word.deleted.isFalse()) .execute(); } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepository.java index 1c5ee46a..5a1e4dd5 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepository.java @@ -3,7 +3,7 @@ import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.word.domain.Word; import com.dnd.spaced.core.word.domain.WordRandom; -import com.dnd.spaced.core.word.domain.dto.SimpleWordInfo; +import com.dnd.spaced.core.word.domain.dto.SimpleWord; import com.dnd.spaced.core.word.domain.enums.Category; import com.dnd.spaced.core.word.domain.repository.WordRandomRepository; import java.util.List; @@ -18,7 +18,7 @@ public class WordRandomGatewayRepository implements WordRandomRepository { private static final int RANDOM_BOUND = 1_000_000; - private static final RowMapper simpleWordInfoRowMapper = (rs, ignoreRowNum) -> new SimpleWordInfo( + private static final RowMapper simpleWordRowMapper = (rs, ignoreRowNum) -> new SimpleWord( rs.getLong(1), rs.getString(2), rs.getString(3), @@ -28,10 +28,7 @@ public class WordRandomGatewayRepository implements WordRandomRepository { private final WordRandomCrudRepository wordRandomCrudRepository; private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; - public WordRandomGatewayRepository( - WordRandomCrudRepository wordRandomCrudRepository, - JdbcTemplate jdbcTemplate - ) { + public WordRandomGatewayRepository(WordRandomCrudRepository wordRandomCrudRepository, JdbcTemplate jdbcTemplate) { this.wordRandomCrudRepository = wordRandomCrudRepository; this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); } @@ -45,10 +42,10 @@ public void saveWith(Word word, Category category) { } @Override - public List findRandomAllBy(QuizCategory quizCategory, long limit) { + public List findRandomAllBy(QuizCategory quizCategory, long limit) { int random = ThreadLocalRandom.current().nextInt(RANDOM_BOUND); - List result = findGoe(random, quizCategory, limit); + List result = findGoe(random, quizCategory, limit); if (result.size() < limit) { result.addAll(findLoe(random, quizCategory, limit)); @@ -57,7 +54,7 @@ public List findRandomAllBy(QuizCategory quizCategory, long limi return result.subList(0, (int) (limit)); } - private List findGoe(int random, QuizCategory quizCategory, long limit) { + private List findGoe(int random, QuizCategory quizCategory, long limit) { String sql = """ SELECT w.id, @@ -87,10 +84,10 @@ private List findGoe(int random, QuizCategory quizCategory, long sqlParameters.addValue("category", quizCategory.name()); } - return namedParameterJdbcTemplate.query(sql, sqlParameters, simpleWordInfoRowMapper); + return namedParameterJdbcTemplate.query(sql, sqlParameters, simpleWordRowMapper); } - private List findLoe(int random, QuizCategory quizCategory, long limit) { + private List findLoe(int random, QuizCategory quizCategory, long limit) { String sql = """ SELECT w.id, @@ -120,6 +117,6 @@ private List findLoe(int random, QuizCategory quizCategory, long sqlParameters.addValue("category", quizCategory.name()); } - return namedParameterJdbcTemplate.query(sql, sqlParameters, simpleWordInfoRowMapper); + return namedParameterJdbcTemplate.query(sql, sqlParameters, simpleWordRowMapper); } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordInfoGatewayRepository.java b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordViewGatewayRepository.java similarity index 68% rename from space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordInfoGatewayRepository.java rename to space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordViewGatewayRepository.java index 066e024a..aa192552 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordInfoGatewayRepository.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/infrastructure/persistence/WordViewGatewayRepository.java @@ -6,10 +6,10 @@ import com.dnd.spaced.core.word.domain.Pronunciation; import com.dnd.spaced.core.word.domain.Word; -import com.dnd.spaced.core.word.domain.dto.WordInfo; -import com.dnd.spaced.core.word.domain.dto.mapper.WordInfoMapper; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.dto.mapper.WordViewMapper; import com.dnd.spaced.core.word.domain.enums.Category; -import com.dnd.spaced.core.word.domain.repository.WordInfoRepository; +import com.dnd.spaced.core.word.domain.repository.WordViewRepository; import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchCondition; import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchPageRequest; import com.querydsl.core.types.dsl.BooleanExpression; @@ -27,12 +27,12 @@ @Repository @RequiredArgsConstructor -public class WordInfoGatewayRepository implements WordInfoRepository { +public class WordViewGatewayRepository implements WordViewRepository { private final JPAQueryFactory queryFactory; @Override - public Optional findBy(Long wordId) { + public Optional findBy(Long wordId) { Word result = findWord(wordId); if (result == null) { @@ -41,14 +41,17 @@ public Optional findBy(Long wordId) { List pronunciations = findPronunciations(result); - return Optional.of(WordInfoMapper.toDto(result, pronunciations)); + return Optional.of(WordViewMapper.toDto(result, pronunciations)); } @Override - public List findAllBy(Category category, String lastWordName, Category lastCategory, Pageable pageable) { + public List findAllBy(Category category, String lastWordName, Category lastCategory, Pageable pageable) { List wordIds = queryFactory.select(word.id) .from(word) - .where(buildWordPaginationCondition(category, lastWordName, lastCategory)) + .where( + buildWordPaginationCondition(category, lastWordName, lastCategory), + word.deleted.isFalse() + ) .orderBy(word.name.asc(), word.category.asc(), word.id.desc()) .limit(pageable.getPageSize()) .fetch(); @@ -57,18 +60,18 @@ public List findAllBy(Category category, String lastWordName, Category return Collections.emptyList(); } - return mapToWordInfos(wordIds); + return mapToWordViews(wordIds); } @Override - public List search(WordSearchCondition condition, WordSearchPageRequest pageRequest) { + public List search(WordSearchCondition condition, WordSearchPageRequest pageRequest) { List wordIds = fetchFilteredWordIds(condition, pageRequest); if (wordIds.isEmpty()) { return Collections.emptyList(); } - return mapToWordInfos(wordIds); + return mapToWordViews(wordIds); } private Word findWord(Long wordId) { @@ -102,43 +105,22 @@ private BooleanExpression buildWordPaginationCondition( return null; } - private Map fetchWordsWithExamples(List wordIds) { - return queryFactory.selectFrom(word) - .innerJoin(word.wordExamples, wordExample).fetchJoin() - .where(word.id.in(wordIds), wordExample.deleted.isFalse()) - .fetch() - .stream() - .collect(Collectors.toMap(Word::getId, Function.identity())); - } - - private Map> fetchPronunciations(List wordIds) { - return queryFactory - .selectFrom(pronunciation) - .where(pronunciation.word.id.in(wordIds), pronunciation.deleted.isFalse()) - .fetch() - .stream() - .collect(Collectors.groupingBy( - pronunciation -> pronunciation.getWord().getId(), - Collectors.mapping(Function.identity(), Collectors.toList()) - )); - } - private List fetchFilteredWordIds(WordSearchCondition condition, WordSearchPageRequest pageRequest) { - return queryFactory - .select(word.id) - .from(word) - .where( - buildWordPaginationCondition( - condition.category(), - pageRequest.lastWordName(), - pageRequest.lastCategory() - ), - startsWithWordName(condition.name()), - buildPronunciationContentCondition(condition) - ) - .orderBy(word.name.asc(), word.category.asc(), word.id.desc()) - .limit(pageRequest.pageable().getPageSize()) - .fetch(); + return queryFactory.select(word.id) + .from(word) + .where( + startsWithWordName(condition.name()), + buildWordPaginationCondition( + condition.category(), + pageRequest.lastWordName(), + pageRequest.lastCategory() + ), + word.deleted.isFalse(), + buildPronunciationContentCondition(condition) + ) + .orderBy(word.name.asc(), word.category.asc(), word.id.desc()) + .limit(pageRequest.pageable().getPageSize()) + .fetch(); } private BooleanExpression startsWithWordName(String name) { @@ -168,12 +150,32 @@ private BooleanExpression existsPronunciationContentCondition(String content) { .exists(); } - private List mapToWordInfos(List wordIds) { + private List mapToWordViews(List wordIds) { Map wordMap = fetchWordsWithExamples(wordIds); Map> pronunciationMap = fetchPronunciations(wordIds); return wordIds.stream() - .map(id -> WordInfoMapper.toDto(wordMap.get(id), pronunciationMap.get(id))) + .map(id -> WordViewMapper.toDto(wordMap.get(id), pronunciationMap.get(id))) .toList(); } + + private Map fetchWordsWithExamples(List wordIds) { + return queryFactory.selectFrom(word) + .innerJoin(word.wordExamples, wordExample).fetchJoin() + .where(word.id.in(wordIds), wordExample.deleted.isFalse()) + .fetch() + .stream() + .collect(Collectors.toMap(Word::getId, Function.identity())); + } + + private Map> fetchPronunciations(List wordIds) { + return queryFactory.selectFrom(pronunciation) + .where(pronunciation.word.id.in(wordIds), pronunciation.deleted.isFalse()) + .fetch() + .stream() + .collect(Collectors.groupingBy( + pronunciation -> pronunciation.getWord().getId(), + Collectors.mapping(Function.identity(), Collectors.toList()) + )); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/core/word/presentation/WordController.java b/space-d/src/main/java/com/dnd/spaced/core/word/presentation/WordController.java index adfb5c84..5ad0071e 100644 --- a/space-d/src/main/java/com/dnd/spaced/core/word/presentation/WordController.java +++ b/space-d/src/main/java/com/dnd/spaced/core/word/presentation/WordController.java @@ -1,6 +1,6 @@ package com.dnd.spaced.core.word.presentation; -import com.dnd.spaced.core.word.application.WordService; +import com.dnd.spaced.core.word.application.WordServiceFacade; import com.dnd.spaced.core.word.application.dto.request.ReadAllWordRequest; import com.dnd.spaced.core.word.application.dto.request.SearchWordRequest; import com.dnd.spaced.core.word.application.dto.response.PopularWordCollectionResponse; @@ -20,11 +20,11 @@ @RequiredArgsConstructor public class WordController { - private final WordService wordService; + private final WordServiceFacade wordServiceFacade; @GetMapping("/{wordId}") public ResponseEntity readWord(@PathVariable Long wordId) { - WordResponse response = wordService.readWord(wordId); + WordResponse response = wordServiceFacade.readWord(wordId); return ResponseEntity.ok(response); } @@ -34,7 +34,7 @@ public ResponseEntity readWords( ReadAllWordRequest request, @WordPageable Pageable pageable ) { - WordCollectionResponse response = wordService.readWords(request, pageable); + WordCollectionResponse response = wordServiceFacade.readWords(request, pageable); return ResponseEntity.ok(response); } @@ -44,14 +44,14 @@ public ResponseEntity searchWords( SearchWordRequest request, @WordPageable Pageable pageable ) { - WordCollectionResponse response = wordService.searchWord(request, pageable); + WordCollectionResponse response = wordServiceFacade.searchWord(request, pageable); return ResponseEntity.ok(response); } @GetMapping("/popular") public ResponseEntity readPopularWords() { - PopularWordCollectionResponse response = wordService.readPopularWords(); + PopularWordCollectionResponse response = wordServiceFacade.readPopularWords(); return ResponseEntity.ok(response); } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/AccountId.java b/space-d/src/main/java/com/dnd/spaced/global/auth/AccountId.java new file mode 100644 index 00000000..a9065e19 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/AccountId.java @@ -0,0 +1,4 @@ +package com.dnd.spaced.global.auth; + +public record AccountId(Long id) { +} diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/AccountInfo.java b/space-d/src/main/java/com/dnd/spaced/global/auth/AccountInfo.java deleted file mode 100644 index 6b290a41..00000000 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/AccountInfo.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.dnd.spaced.global.auth; - -public record AccountInfo(Long accountId) { -} diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/AuthStore.java b/space-d/src/main/java/com/dnd/spaced/global/auth/AuthStore.java index 223bc21d..0596b389 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/AuthStore.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/AuthStore.java @@ -5,13 +5,13 @@ @Component public class AuthStore { - private final ThreadLocal threadLocalAuthenticationStore = new ThreadLocal<>(); + private final ThreadLocal threadLocalAuthenticationStore = new ThreadLocal<>(); - public void set(AccountInfo userInfo) { + public void set(AccountId userInfo) { threadLocalAuthenticationStore.set(userInfo); } - public AccountInfo get() { + public AccountId get() { return threadLocalAuthenticationStore.get(); } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/interceptor/AuthInterceptor.java b/space-d/src/main/java/com/dnd/spaced/global/auth/interceptor/AuthInterceptor.java index be7f2142..5fb575b9 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/interceptor/AuthInterceptor.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/interceptor/AuthInterceptor.java @@ -1,6 +1,6 @@ package com.dnd.spaced.global.auth.interceptor; -import com.dnd.spaced.global.auth.AccountInfo; +import com.dnd.spaced.global.auth.AccountId; import com.dnd.spaced.global.auth.AuthStore; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -24,13 +24,13 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons .getAuthentication(); if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { - store.set(new AccountInfo(null)); + store.set(new AccountId(null)); return true; } String id = ((UserDetails) authentication.getPrincipal()).getUsername(); - store.set(new AccountInfo(Long.parseLong(id))); + store.set(new AccountId(Long.parseLong(id))); return true; } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountInfoArgumentResolver.java b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountArgumentResolver.java similarity index 58% rename from space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountInfoArgumentResolver.java rename to space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountArgumentResolver.java index 7325bd54..acf5efb6 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountInfoArgumentResolver.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountArgumentResolver.java @@ -1,7 +1,7 @@ package com.dnd.spaced.global.auth.resolver; import com.dnd.spaced.core.account.domain.repository.AccountRepository; -import com.dnd.spaced.global.auth.AccountInfo; +import com.dnd.spaced.global.auth.AccountId; import com.dnd.spaced.global.auth.AuthStore; import com.dnd.spaced.global.auth.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; @@ -14,15 +14,15 @@ @Component @RequiredArgsConstructor -public class GuestAccountInfoArgumentResolver implements HandlerMethodArgumentResolver { +public class AuthAccountArgumentResolver implements HandlerMethodArgumentResolver { private final AuthStore store; private final AccountRepository accountRepository; @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(CurrentAccountInfo.class) && parameter.getParameterType() - .equals(GuestAccountInfo.class); + return parameter.hasParameterAnnotation(CurrentAccount.class) && parameter.getParameterType() + .equals(AuthAccountId.class); } @Override @@ -32,24 +32,34 @@ public Object resolveArgument( NativeWebRequest webRequest, WebDataBinderFactory binderFactory ) { - AccountInfo accountInfo = store.get(); + AccountId accountId = store.get(); - if (isInvalidAccountPrincipal(accountInfo)) { - return new GuestAccountInfo(); - } + validateAccountPrincipal(accountId); + + Long id = accountId.id(); - validateExistsAccountId(accountInfo.accountId()); + validateExistsAccountId(id); + + return new AuthAccountId(accountId.id()); + } - return new GuestAccountInfo(accountInfo.accountId()); + private void validateAccountPrincipal(AccountId accountId) { + if (isEmptyAccountId(accountId)) { + throw new UnauthorizedException(); + } } - private boolean isInvalidAccountPrincipal(AccountInfo accountInfo) { - return accountInfo == null || accountInfo.accountId() == null; + private boolean isEmptyAccountId(AccountId accountId) { + return accountId == null || accountId.id() == null; } private void validateExistsAccountId(Long accountId) { - if (!accountRepository.existsBy(accountId)) { + if (isBrokenAccount(accountId)) { throw new UnauthorizedException(); } } + + private boolean isBrokenAccount(Long accountId) { + return !accountRepository.existsBy(accountId); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountInfo.java b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountId.java similarity index 50% rename from space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountInfo.java rename to space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountId.java index 807e55a8..e0b61a6e 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountId.java @@ -1,4 +1,4 @@ package com.dnd.spaced.global.auth.resolver; -public record AuthAccountInfo(Long accountId) { +public record AuthAccountId(Long id) { } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/CurrentAccountInfo.java b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/CurrentAccount.java similarity index 87% rename from space-d/src/main/java/com/dnd/spaced/global/auth/resolver/CurrentAccountInfo.java rename to space-d/src/main/java/com/dnd/spaced/global/auth/resolver/CurrentAccount.java index 2e8e48a6..8700ec3b 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/CurrentAccountInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/CurrentAccount.java @@ -7,5 +7,5 @@ @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) -public @interface CurrentAccountInfo { +public @interface CurrentAccount { } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountInfoArgumentResolver.java b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountArgumentResolver.java similarity index 62% rename from space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountInfoArgumentResolver.java rename to space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountArgumentResolver.java index acf9abe0..8e85cc89 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/AuthAccountInfoArgumentResolver.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountArgumentResolver.java @@ -1,7 +1,7 @@ package com.dnd.spaced.global.auth.resolver; import com.dnd.spaced.core.account.domain.repository.AccountRepository; -import com.dnd.spaced.global.auth.AccountInfo; +import com.dnd.spaced.global.auth.AccountId; import com.dnd.spaced.global.auth.AuthStore; import com.dnd.spaced.global.auth.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; @@ -14,15 +14,15 @@ @Component @RequiredArgsConstructor -public class AuthAccountInfoArgumentResolver implements HandlerMethodArgumentResolver { +public class GuestAccountArgumentResolver implements HandlerMethodArgumentResolver { private final AuthStore store; private final AccountRepository accountRepository; @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(CurrentAccountInfo.class) && parameter.getParameterType() - .equals(AuthAccountInfo.class); + return parameter.hasParameterAnnotation(CurrentAccount.class) && parameter.getParameterType() + .equals(GuestAccountId.class); } @Override @@ -32,26 +32,28 @@ public Object resolveArgument( NativeWebRequest webRequest, WebDataBinderFactory binderFactory ) { - AccountInfo accountInfo = store.get(); + AccountId accountId = store.get(); - validateAccountPrincipal(accountInfo); - - Long accountId = accountInfo.accountId(); + if (isEmptyAccountPrincipal(accountId)) { + return new GuestAccountId(); + } - validateExistsAccountId(accountId); + validateExistsAccountId(accountId.id()); - return new AuthAccountInfo(accountInfo.accountId()); + return new GuestAccountId(accountId.id()); } - private void validateAccountPrincipal(AccountInfo accountInfo) { - if (accountInfo == null || accountInfo.accountId() == null) { - throw new UnauthorizedException(); - } + private boolean isEmptyAccountPrincipal(AccountId accountId) { + return accountId == null || accountId.id() == null; } private void validateExistsAccountId(Long accountId) { - if (!accountRepository.existsBy(accountId)) { + if (isBrokenAccount(accountId)) { throw new UnauthorizedException(); } } + + private boolean isBrokenAccount(Long accountId) { + return !accountRepository.existsBy(accountId); + } } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountInfo.java b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountId.java similarity index 64% rename from space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountInfo.java rename to space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountId.java index 76700a26..eb2e736c 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountInfo.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/resolver/GuestAccountId.java @@ -2,9 +2,9 @@ import com.dnd.spaced.global.consts.AuthConst; -public record GuestAccountInfo(Long accountId) { +public record GuestAccountId(Long id) { - public GuestAccountInfo() { + public GuestAccountId() { this(AuthConst.GUEST_ACCOUNT_ID); } } diff --git a/space-d/src/main/java/com/dnd/spaced/global/auth/security/handler/OAuth2SuccessHandler.java b/space-d/src/main/java/com/dnd/spaced/global/auth/security/handler/OAuth2SuccessHandler.java index a6e22ac8..2cdee914 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/auth/security/handler/OAuth2SuccessHandler.java +++ b/space-d/src/main/java/com/dnd/spaced/global/auth/security/handler/OAuth2SuccessHandler.java @@ -1,8 +1,8 @@ package com.dnd.spaced.global.auth.security.handler; -import com.dnd.spaced.core.auth.application.internal.GenerateTokenService; -import com.dnd.spaced.core.auth.application.internal.LoginService; -import com.dnd.spaced.core.auth.application.dto.response.LoggedInAccountInfoDto; +import com.dnd.spaced.core.auth.application.GenerateTokenService; +import com.dnd.spaced.core.auth.application.LoginService; +import com.dnd.spaced.core.auth.application.dto.response.LoggedInAccountDto; import com.dnd.spaced.core.auth.application.dto.response.TokenDto; import com.dnd.spaced.global.auth.exception.InvalidResponseWriteException; import com.dnd.spaced.global.auth.security.dto.response.LoginResponse; @@ -45,7 +45,7 @@ public void onAuthenticationSuccess( String socialIdentifier = (String) oAuth2User.getAttributes() .get(StandardClaimNames.SUB); String registrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); - LoggedInAccountInfoDto accountInfoDto = loginService.login(registrationId, socialIdentifier); + LoggedInAccountDto accountInfoDto = loginService.login(registrationId, socialIdentifier); TokenDto tokenDto = generateTokenService.generate(accountInfoDto.id(), accountInfoDto.roleName()); writeResponse(response, tokenDto, accountInfoDto.isSignUp()); diff --git a/space-d/src/main/java/com/dnd/spaced/global/config/AppConfig.java b/space-d/src/main/java/com/dnd/spaced/global/config/AppConfig.java index 0d449bce..88b603b5 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/config/AppConfig.java +++ b/space-d/src/main/java/com/dnd/spaced/global/config/AppConfig.java @@ -1,8 +1,8 @@ package com.dnd.spaced.global.config; import com.dnd.spaced.global.auth.interceptor.AuthInterceptor; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfoArgumentResolver; -import com.dnd.spaced.global.auth.resolver.GuestAccountInfoArgumentResolver; +import com.dnd.spaced.global.auth.resolver.AuthAccountArgumentResolver; +import com.dnd.spaced.global.auth.resolver.GuestAccountArgumentResolver; import com.dnd.spaced.global.log.QueryTraceInterceptor; import com.dnd.spaced.global.resolver.admin.report.ReportPageableArgumentResolver; import com.dnd.spaced.global.resolver.bookmark.BookmarkPageableArgumentResolver; @@ -38,8 +38,8 @@ public class AppConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; private final QueryTraceInterceptor queryTraceInterceptor; - private final AuthAccountInfoArgumentResolver authAccountInfoArgumentResolver; - private final GuestAccountInfoArgumentResolver guestAccountInfoArgumentResolver; + private final AuthAccountArgumentResolver authAccountArgumentResolver; + private final GuestAccountArgumentResolver guestAccountArgumentResolver; private final ReportPageableArgumentResolver reportPageableArgumentResolver; private final BookmarkPageableArgumentResolver bookmarkPageableArgumentResolver; private final CommentPageableArgumentResolver commentPageableArgumentResolver; @@ -73,8 +73,8 @@ public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomiz @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(authAccountInfoArgumentResolver); - resolvers.add(guestAccountInfoArgumentResolver); + resolvers.add(authAccountArgumentResolver); + resolvers.add(guestAccountArgumentResolver); resolvers.add(wordPageableArgumentResolver); resolvers.add(commentPageableArgumentResolver); resolvers.add(reportPageableArgumentResolver); diff --git a/space-d/src/main/java/com/dnd/spaced/global/config/SecurityConfig.java b/space-d/src/main/java/com/dnd/spaced/global/config/SecurityConfig.java index 64946ffb..9d1c8d15 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/config/SecurityConfig.java +++ b/space-d/src/main/java/com/dnd/spaced/global/config/SecurityConfig.java @@ -1,8 +1,8 @@ package com.dnd.spaced.global.config; import com.dnd.spaced.core.auth.application.BlacklistTokenService; -import com.dnd.spaced.core.auth.application.internal.GenerateTokenService; -import com.dnd.spaced.core.auth.application.internal.LoginService; +import com.dnd.spaced.core.auth.application.GenerateTokenService; +import com.dnd.spaced.core.auth.application.LoginService; import com.dnd.spaced.core.auth.domain.TokenDecoder; import com.dnd.spaced.global.auth.security.core.OAuth2UserDetailsService; import com.dnd.spaced.global.auth.security.filter.OAuth2AuthenticationFilter; diff --git a/space-d/src/main/java/com/dnd/spaced/global/exception/code/BookmarkErrorCode.java b/space-d/src/main/java/com/dnd/spaced/global/exception/code/BookmarkErrorCode.java index 7e3fef27..bb06d88f 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/exception/code/BookmarkErrorCode.java +++ b/space-d/src/main/java/com/dnd/spaced/global/exception/code/BookmarkErrorCode.java @@ -5,5 +5,6 @@ public enum BookmarkErrorCode implements ErrorCode { BOOKMARK_NOT_FOUND_EXCEPTION, WORD_NOT_FOUND_EXCEPTION, ALREADY_EXISTS_BOOKMARK_EXCEPTION, - BOOKMARK_LOCK_EXCEPTION + BOOKMARK_LOCK_EXCEPTION, + BOOKMARK_INTERRUPTED_EXCEPTION } diff --git a/space-d/src/main/java/com/dnd/spaced/global/exception/translator/BookmarkExceptionTranslator.java b/space-d/src/main/java/com/dnd/spaced/global/exception/translator/BookmarkExceptionTranslator.java index 53d30016..3bf30add 100644 --- a/space-d/src/main/java/com/dnd/spaced/global/exception/translator/BookmarkExceptionTranslator.java +++ b/space-d/src/main/java/com/dnd/spaced/global/exception/translator/BookmarkExceptionTranslator.java @@ -28,6 +28,11 @@ public enum BookmarkExceptionTranslator implements ExceptionTranslator { BookmarkErrorCode.BOOKMARK_LOCK_EXCEPTION, HttpStatus.INTERNAL_SERVER_ERROR, "북마크 생성 도중 서버에 문제가 발생했습니다." + ), + BOOKMARK_INTERRUPTED_EXCEPTION( + BookmarkErrorCode.BOOKMARK_INTERRUPTED_EXCEPTION, + HttpStatus.INTERNAL_SERVER_ERROR, + "북마크 생성 도중 서버에 문제가 발생했습니다." ); private final ErrorCode errorCode; diff --git a/space-d/src/main/java/com/dnd/spaced/global/mapper/Mapper.java b/space-d/src/main/java/com/dnd/spaced/global/mapper/Mapper.java new file mode 100644 index 00000000..7c9e0746 --- /dev/null +++ b/space-d/src/main/java/com/dnd/spaced/global/mapper/Mapper.java @@ -0,0 +1,14 @@ +package com.dnd.spaced.global.mapper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.stereotype.Component; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface Mapper { + +} diff --git a/space-d/src/test/java/com/dnd/spaced/config/EventListenerSpyBeanTestConfig.java b/space-d/src/test/java/com/dnd/spaced/config/EventListenerSpyBeanTestConfig.java index f5fa7483..6db6a781 100644 --- a/space-d/src/test/java/com/dnd/spaced/config/EventListenerSpyBeanTestConfig.java +++ b/space-d/src/test/java/com/dnd/spaced/config/EventListenerSpyBeanTestConfig.java @@ -4,7 +4,7 @@ import com.dnd.spaced.core.skill.application.event.dto.FailedGradedQuizSkillEvent; import com.dnd.spaced.core.skill.application.event.dto.FailedGradedTodayQuizSkillEvent; import com.dnd.spaced.core.skill.domain.repository.SkillRepository; -import com.dnd.spaced.core.word.application.DeletedWordIdRepository; +import com.dnd.spaced.core.word.application.repository.DeletedWordIdRepository; import com.dnd.spaced.core.word.application.event.dto.FailedWordPersistedEvent; import com.dnd.spaced.core.word.domain.repository.PronunciationRepository; import com.dnd.spaced.core.word.domain.repository.WordExampleRepository; diff --git a/space-d/src/test/java/com/dnd/spaced/config/RetryTestConfig.java b/space-d/src/test/java/com/dnd/spaced/config/RetryTestConfig.java index 6f94949d..8a0c4892 100644 --- a/space-d/src/test/java/com/dnd/spaced/config/RetryTestConfig.java +++ b/space-d/src/test/java/com/dnd/spaced/config/RetryTestConfig.java @@ -1,15 +1,10 @@ package com.dnd.spaced.config; -import com.dnd.spaced.global.exception.base.BaseClientException; -import com.dnd.spaced.global.exception.base.BaseServerException; import java.util.HashMap; import java.util.Map; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; -import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.QueryTimeoutException; import org.springframework.dao.TransientDataAccessException; diff --git a/space-d/src/test/java/com/dnd/spaced/config/TestSecurityConfig.java b/space-d/src/test/java/com/dnd/spaced/config/TestSecurityConfig.java new file mode 100644 index 00000000..76065491 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/config/TestSecurityConfig.java @@ -0,0 +1,15 @@ +package com.dnd.spaced.config; + +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; + +@Profile("test") +@Configuration +public class TestSecurityConfig { + + @MockBean + JwtDecoderFactory jwtDecoderFactory; +} diff --git a/space-d/src/test/java/com/dnd/spaced/config/clean/CleanupExecutionListener.java b/space-d/src/test/java/com/dnd/spaced/config/clean/CleanupExecutionListener.java index 8e75f925..eb0bd9f8 100644 --- a/space-d/src/test/java/com/dnd/spaced/config/clean/CleanupExecutionListener.java +++ b/space-d/src/test/java/com/dnd/spaced/config/clean/CleanupExecutionListener.java @@ -1,9 +1,12 @@ package com.dnd.spaced.config.clean; +import jakarta.persistence.EntityManager; import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.redisson.spring.data.connection.RedissonConnectionFactory; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.ClassPathResource; @@ -22,20 +25,21 @@ public void beforeTestMethod(TestContext testContext) { return; } - cleanUpWithSql(testContext); - cleanUpWithRedis(testContext); + cleanupWithSql(testContext); + cleanupWithRedis(testContext); + cleanupWithMemoryCache(testContext); } @Override public int getOrder() { - return 4999; + return Ordered.HIGHEST_PRECEDENCE; } private boolean isNotIntegrationTest(TestContext testContext) { return AnnotationUtils.findAnnotation(testContext.getTestClass(), SpringBootTest.class) == null; } - private void cleanUpWithSql(TestContext testContext) { + private void cleanupWithSql(TestContext testContext) { DataSource dataSource = testContext.getApplicationContext().getBean(DataSource.class); ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); @@ -43,7 +47,7 @@ private void cleanUpWithSql(TestContext testContext) { populator.execute(dataSource); } - private void cleanUpWithRedis(TestContext testContext) { + private void cleanupWithRedis(TestContext testContext) { RedissonConnectionFactory lettuceConnectionFactory = findLettuceConnectionFactory(testContext); RedisConnection redisConnection = lettuceConnectionFactory.getConnection(); RedisServerCommands redisServerCommands = redisConnection.serverCommands(); @@ -51,6 +55,20 @@ private void cleanUpWithRedis(TestContext testContext) { redisServerCommands.flushAll(); } + private void cleanupWithMemoryCache(TestContext testContext) { + try { + CacheManager cacheManager = testContext.getApplicationContext().getBean(CacheManager.class); + cacheManager.getCacheNames().forEach(cacheName -> { + Cache cache = cacheManager.getCache(cacheName); + if (cache != null) { + cache.clear(); + } + }); + } catch (Exception e) { + log.warn("캐시 정리 중 오류 발생", e); + } + } + private RedissonConnectionFactory findLettuceConnectionFactory(TestContext testContext) { return testContext.getApplicationContext() .getBean(RedissonConnectionFactory.class); diff --git a/space-d/src/test/java/com/dnd/spaced/config/common/CommonControllerSliceTest.java b/space-d/src/test/java/com/dnd/spaced/config/common/CommonControllerSliceTest.java index 3411f42d..851be9ae 100644 --- a/space-d/src/test/java/com/dnd/spaced/config/common/CommonControllerSliceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/config/common/CommonControllerSliceTest.java @@ -8,8 +8,8 @@ import com.dnd.spaced.config.stub.StubAccountRepository; import com.dnd.spaced.global.auth.AuthStore; import com.dnd.spaced.global.auth.interceptor.AuthInterceptor; -import com.dnd.spaced.global.auth.resolver.AuthAccountInfoArgumentResolver; -import com.dnd.spaced.global.auth.resolver.GuestAccountInfoArgumentResolver; +import com.dnd.spaced.global.auth.resolver.AuthAccountArgumentResolver; +import com.dnd.spaced.global.auth.resolver.GuestAccountArgumentResolver; import com.dnd.spaced.global.exception.GlobalControllerAdvice; import com.dnd.spaced.global.resolver.admin.report.ReportPageableArgumentResolver; import com.dnd.spaced.global.resolver.bookmark.BookmarkPageableArgumentResolver; @@ -113,8 +113,8 @@ FixedStandaloneMockMvcBuilder configureMessageConverters() { FixedStandaloneMockMvcBuilder configureArgumentResolvers() { builder.setCustomArgumentResolvers( - new AuthAccountInfoArgumentResolver(store, new StubAccountRepository()), - new GuestAccountInfoArgumentResolver(store, new StubAccountRepository()), + new AuthAccountArgumentResolver(store, new StubAccountRepository()), + new GuestAccountArgumentResolver(store, new StubAccountRepository()), new WordPageableArgumentResolver(), new CommentPageableArgumentResolver(), new GradedAnswerPageableArgumentResolver(), diff --git a/space-d/src/test/java/com/dnd/spaced/config/stub/StubAccountRepository.java b/space-d/src/test/java/com/dnd/spaced/config/stub/StubAccountRepository.java index f18197cb..ef7d67bb 100644 --- a/space-d/src/test/java/com/dnd/spaced/config/stub/StubAccountRepository.java +++ b/space-d/src/test/java/com/dnd/spaced/config/stub/StubAccountRepository.java @@ -1,7 +1,7 @@ package com.dnd.spaced.config.stub; import com.dnd.spaced.core.account.domain.Account; -import com.dnd.spaced.core.account.domain.enums.RegistrationId; +import com.dnd.spaced.core.account.domain.embed.Social; import com.dnd.spaced.core.account.domain.repository.AccountRepository; import java.util.Optional; @@ -23,7 +23,7 @@ public Optional findBy(Long accountId) { } @Override - public Optional findBy(RegistrationId registrationId, String socialIdentifier) { + public Optional findBy(Social social) { throw new UnsupportedOperationException("지원하지 않는 기능입니다."); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/application/AccountServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/application/AccountServiceTest.java index ebf298a0..f30a39e5 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/account/application/AccountServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/account/application/AccountServiceTest.java @@ -5,8 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.dnd.spaced.core.account.application.dto.request.ChangeCareerInfoRequest; -import com.dnd.spaced.core.account.application.dto.request.ChangeProfileInfoRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeCareerRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeProfileRequest; import com.dnd.spaced.core.account.application.dto.response.AccountResponse; import com.dnd.spaced.core.account.application.exception.ForbiddenAccountException; import com.dnd.spaced.core.account.domain.enums.ProfileImageName; @@ -55,14 +55,14 @@ class AccountServiceTest { @Sql("classpath:sql/account/account.sql") void 회원_경력_정보를_변경한다() { // given - ChangeCareerInfoRequest request = new ChangeCareerInfoRequest( + ChangeCareerRequest request = new ChangeCareerRequest( "개발자", "비공개", "1~2년 차" ); // when - accountService.changeCareerInfo(1L, request); + accountService.changeCareer(1L, request); // then AccountResponse actual = accountService.readAccount(1L); @@ -79,14 +79,14 @@ class AccountServiceTest { @Sql("classpath:sql/account/account.sql") void 유효한_직군_이름이_아니라면_경력_정보를_변경할_수_없다(String invalidJobGroupName) { // given - ChangeCareerInfoRequest request = new ChangeCareerInfoRequest( + ChangeCareerRequest request = new ChangeCareerRequest( invalidJobGroupName, "비공개", "1~2년 차" ); // when & then - assertThatThrownBy(() -> accountService.changeCareerInfo(1L, request)) + assertThatThrownBy(() -> accountService.changeCareer(1L, request)) .isInstanceOf(InvalidJobGroupException.class) .hasMessageContaining("잘못된 직군 이름"); } @@ -96,14 +96,14 @@ class AccountServiceTest { @Sql("classpath:sql/account/account.sql") void 유효한_회사명이_아니라면_경력_정보를_변경할_수_없다(String invalidCompanyName) { // given - ChangeCareerInfoRequest request = new ChangeCareerInfoRequest( + ChangeCareerRequest request = new ChangeCareerRequest( "개발자", invalidCompanyName, "1~2년 차" ); // when & then - assertThatThrownBy(() -> accountService.changeCareerInfo(1L, request)) + assertThatThrownBy(() -> accountService.changeCareer(1L, request)) .isInstanceOf(InvalidCompanyException.class) .hasMessageContaining("잘못된 회사 이름"); } @@ -113,14 +113,14 @@ class AccountServiceTest { @Sql("classpath:sql/account/account.sql") void 유효한_경력이_아니라면_경력_정보를_변경할_수_없다(String invalidExperienceName) { // given - ChangeCareerInfoRequest request = new ChangeCareerInfoRequest( + ChangeCareerRequest request = new ChangeCareerRequest( "개발자", "비공개", invalidExperienceName ); // when & then - assertThatThrownBy(() -> accountService.changeCareerInfo(1L, request)) + assertThatThrownBy(() -> accountService.changeCareer(1L, request)) .isInstanceOf(InvalidExperienceException.class) .hasMessageContaining("잘못된 경력"); } @@ -128,14 +128,14 @@ class AccountServiceTest { @Test void 없거나_탈퇴한_회원의_ID라면_경력_정보를_변경할_수_없다() { // given - ChangeCareerInfoRequest request = new ChangeCareerInfoRequest( + ChangeCareerRequest request = new ChangeCareerRequest( "개발자", "비공개", "1~2년 차" ); // when & then - assertThatThrownBy(() -> accountService.changeCareerInfo(-999L, request)) + assertThatThrownBy(() -> accountService.changeCareer(-999L, request)) .isInstanceOf(ForbiddenAccountException.class) .hasMessage("존재하지 않는 회원이거나 이미 탈퇴한 회원입니다."); } @@ -150,13 +150,13 @@ private static Stream changeProfileInfoTestWithProfileImageKoreanName @Sql("classpath:sql/account/account.sql") void 회원_프로필_정보를_변경한다(ProfileImageName profileImageName) { // given - ChangeProfileInfoRequest request = new ChangeProfileInfoRequest( + ChangeProfileRequest request = new ChangeProfileRequest( "행복한지구001", profileImageName.getKorean() ); // when - accountService.changeProfileInfo(1L, request); + accountService.changeProfile(1L, request); // then AccountResponse actual = accountService.readAccount(1L); @@ -172,13 +172,13 @@ private static Stream changeProfileInfoTestWithProfileImageKoreanName @Sql("classpath:sql/account/account.sql") void 프로필_이미지_경로가_비어_있으면_프로필_정보를_변경할_수_없다(String invalidProfileImageKoreanName) { // given - ChangeProfileInfoRequest request = new ChangeProfileInfoRequest( + ChangeProfileRequest request = new ChangeProfileRequest( "재빠른지구001", invalidProfileImageKoreanName ); // when & then - assertThatThrownBy(() -> accountService.changeProfileInfo(1L, request)) + assertThatThrownBy(() -> accountService.changeProfile(1L, request)) .isInstanceOf(InvalidProfileImageNameException.class) .hasMessageContaining("잘못된 프로필 이미지 이름"); } @@ -186,13 +186,13 @@ private static Stream changeProfileInfoTestWithProfileImageKoreanName @Test void 없거나_탈퇴한_회원의_ID라면_프로필_정보를_변경할_수_없다() { // given - ChangeProfileInfoRequest request = new ChangeProfileInfoRequest( + ChangeProfileRequest request = new ChangeProfileRequest( "재빠른지구001", "earth.png" ); // when & then - assertThatThrownBy(() -> accountService.changeProfileInfo(-999L, request)) + assertThatThrownBy(() -> accountService.changeProfile(-999L, request)) .isInstanceOf(ForbiddenAccountException.class) .hasMessage("존재하지 않는 회원이거나 이미 탈퇴한 회원입니다."); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/domain/AccountTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/domain/AccountTest.java index 2818725d..a054795b 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/account/domain/AccountTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/account/domain/AccountTest.java @@ -5,12 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.dnd.spaced.core.account.domain.embed.CareerInfo; +import com.dnd.spaced.core.account.domain.embed.Career; import com.dnd.spaced.core.account.domain.embed.exception.InvalidNicknameException; import com.dnd.spaced.core.account.domain.embed.exception.InvalidProfileImageException; import com.dnd.spaced.core.account.domain.enums.Company; import com.dnd.spaced.core.account.domain.enums.Experience; import com.dnd.spaced.core.account.domain.enums.JobGroup; +import com.dnd.spaced.core.account.domain.enums.ProfileImageName; import com.dnd.spaced.core.account.domain.enums.RegistrationId; import com.dnd.spaced.core.account.domain.enums.Role; import com.dnd.spaced.core.account.domain.enums.exception.InvalidCompanyException; @@ -38,16 +39,16 @@ class AccountTest { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build() ); assertAll( - () -> assertThat(actual.getSocialInfo().getRegistrationId()).isEqualTo(RegistrationId.KAKAO), - () -> assertThat(actual.getSocialInfo().getSocialIdentifier()).isEqualTo("12345"), - () -> assertThat(actual.getProfileInfo().getNickname()).isEqualTo("재빠른지구001"), - () -> assertThat(actual.getProfileInfo().getProfileImage()).isEqualTo("earth.png"), + () -> assertThat(actual.getSocial().getRegistrationId()).isEqualTo(RegistrationId.KAKAO), + () -> assertThat(actual.getSocial().getSocialId()).isEqualTo("12345"), + () -> assertThat(actual.getProfile().getNickname()).isEqualTo("재빠른지구001"), + () -> assertThat(actual.getProfile().getProfileImageName()).isEqualTo("earth.png"), () -> assertThat(actual.getRole()).isEqualTo(Role.ROLE_USER) ); } @@ -71,27 +72,26 @@ private static Stream builderTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname(invalidNickname) - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build() ).isInstanceOf(InvalidNicknameException.class) .hasMessage("닉네임은 최소 5글자 이상, 최대 10글자 이하여야 합니다."); } - @ParameterizedTest(name = "프로필 이미지가 {0}일 때 예외가 발생한다") - @NullAndEmptySource - void 비어_있는_프로필_이미지_경로라면_회원을_초기화할_수_없다(String invalidProfileImage) { + @Test + void 비어_있는_프로필_이미지_경로라면_회원을_초기화할_수_없다() { // when & then assertThatThrownBy( () -> Account.builder() .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage(invalidProfileImage) + .profileImageName(null) .role(Role.ROLE_USER) .build() ).isInstanceOf(InvalidProfileImageException.class) - .hasMessage("프로필 이미지 정보는 null이거나 비어 있을 수 없습니다."); + .hasMessage("프로필 이미지 정보는 null일 수 없습니다."); } @Test @@ -101,20 +101,20 @@ private static Stream builderTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when - account.changeCareerInfo("개발자", "비공개", "1~2년 차"); + account.changeCareer("개발자", "비공개", "1~2년 차"); // then - CareerInfo careerInfo = account.getCareerInfo(); + Career career = account.getCareer(); assertAll( - () -> assertThat(careerInfo.getCompany()).isEqualTo(Company.BLIND), - () -> assertThat(careerInfo.getJobGroup()).isEqualTo(JobGroup.DEVELOP), - () -> assertThat(careerInfo.getExperience()).isEqualTo(Experience.BETWEEN_FIRST_SECOND) + () -> assertThat(career.getCompany()).isEqualTo(Company.BLIND), + () -> assertThat(career.getJobGroup()).isEqualTo(JobGroup.DEVELOP), + () -> assertThat(career.getExperience()).isEqualTo(Experience.BETWEEN_FIRST_SECOND) ); } @@ -126,13 +126,13 @@ private static Stream builderTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when & then assertThatThrownBy( - () -> account.changeCareerInfo( + () -> account.changeCareer( "개발자", invalidCompanyName, "1~2년 차" @@ -150,13 +150,13 @@ private static Stream builderTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when & then assertThatThrownBy( - () -> account.changeCareerInfo( + () -> account.changeCareer( invalidJobGroupName, "비공개", "1~2년 차" @@ -174,13 +174,13 @@ private static Stream builderTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when & then assertThatThrownBy( - () -> account.changeCareerInfo( + () -> account.changeCareer( "개발자", "비공개", invalidExperienceName @@ -197,39 +197,35 @@ private static Stream builderTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when - String changedNickname = "행복한화성001"; - String changedProfileImage = "mars.png"; - - account.changeProfileInfo(changedNickname, changedProfileImage); + account.changeProfile("행복한화성001", ProfileImageName.MARS); // then assertAll( - () -> assertThat(account.getProfileInfo().getNickname()).isEqualTo(changedNickname), - () -> assertThat(account.getProfileInfo().getProfileImage()).isEqualTo(changedProfileImage) + () -> assertThat(account.getProfile().getNickname()).isEqualTo("행복한화성001"), + () -> assertThat(account.getProfile().getProfileImageName()).isEqualTo(ProfileImageName.MARS.getImageName()) ); } - @ParameterizedTest(name = "프로필 이미지가 {0}일 때 예외가 발생한다") - @NullAndEmptySource - void 프로필_이미지_경로가_비어_있으면_회원_프로필_정보를_변환할_수_없다(String invalidProfileImage) { + @Test + void 프로필_이미지_경로가_비어_있으면_회원_프로필_정보를_변환할_수_없다() { // given Account account = Account.builder() .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when & then - assertThatThrownBy(() -> account.changeProfileInfo("행복한화성001", invalidProfileImage)) + assertThatThrownBy(() -> account.changeProfile("행복한화성001", null)) .isInstanceOf(InvalidProfileImageException.class) - .hasMessage("프로필 이미지 정보는 null이거나 비어 있을 수 없습니다."); + .hasMessage("프로필 이미지 정보는 null일 수 없습니다."); } private static Stream changeProfileInfoTestWithInvalidNickname() { @@ -250,12 +246,12 @@ private static Stream changeProfileInfoTestWithInvalidNickname() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); // when & then - assertThatThrownBy(() -> account.changeProfileInfo(invalidNickname, "mars.png")) + assertThatThrownBy(() -> account.changeProfile(invalidNickname, ProfileImageName.MARS)) .isInstanceOf(InvalidNicknameException.class) .hasMessage("닉네임은 최소 5글자 이상, 최대 10글자 이하여야 합니다."); } @@ -275,7 +271,7 @@ private static Stream isEqualToTestArguments() { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); ReflectionTestUtils.setField(account, "id", 1L); diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/CareerInfoTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/CareerTest.java similarity index 65% rename from space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/CareerInfoTest.java rename to space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/CareerTest.java index 31054d41..503a2a56 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/CareerInfoTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/CareerTest.java @@ -14,17 +14,17 @@ @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class CareerInfoTest { +class CareerTest { @Test void 경력_정보를_초기화한다() { // when & then assertDoesNotThrow( - () -> CareerInfo.builder() - .jobGroupName("개발자") - .experienceName("1~2년 차") - .companyName("비공개") - .build() + () -> Career.builder() + .jobGroupName("개발자") + .experienceName("1~2년 차") + .companyName("비공개") + .build() ); } @@ -33,11 +33,11 @@ class CareerInfoTest { void 유효한_경력이_아니라면_경력_정보를_초기화할_수_없다(String invalidExperienceName) { // when & then assertThatThrownBy( - () -> CareerInfo.builder() - .jobGroupName("개발자") - .experienceName(invalidExperienceName) - .companyName("비공개") - .build() + () -> Career.builder() + .jobGroupName("개발자") + .experienceName(invalidExperienceName) + .companyName("비공개") + .build() ).isInstanceOf(InvalidExperienceException.class) .hasMessageContaining("잘못된 경력"); } @@ -47,11 +47,11 @@ class CareerInfoTest { void 유효한_회사명이_아니라면_경력_정보를_초기화할_수_없다(String invalidCompanyName) { // when & then assertThatThrownBy( - () -> CareerInfo.builder() - .jobGroupName("개발자") - .experienceName("1~2년 차") - .companyName(invalidCompanyName) - .build() + () -> Career.builder() + .jobGroupName("개발자") + .experienceName("1~2년 차") + .companyName(invalidCompanyName) + .build() ).isInstanceOf(InvalidCompanyException.class) .hasMessageContaining("잘못된 회사 이름"); } @@ -61,11 +61,11 @@ class CareerInfoTest { void 유효한_직군_이름이_아니라면_경력_정보를_초기화할_수_없다(String invalidJobGroupName) { // when & then assertThatThrownBy( - () -> CareerInfo.builder() - .jobGroupName(invalidJobGroupName) - .experienceName("1~2년 차") - .companyName("비공개") - .build() + () -> Career.builder() + .jobGroupName(invalidJobGroupName) + .experienceName("1~2년 차") + .companyName("비공개") + .build() ).isInstanceOf(InvalidJobGroupException.class) .hasMessageContaining("잘못된 직군 이름"); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/ProfileInfoTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/ProfileInfoTest.java deleted file mode 100644 index e749402f..00000000 --- a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/ProfileInfoTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.dnd.spaced.core.account.domain.embed; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -import com.dnd.spaced.core.account.domain.embed.exception.InvalidNicknameException; -import com.dnd.spaced.core.account.domain.embed.exception.InvalidProfileImageException; -import java.util.stream.Stream; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.NullAndEmptySource; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class ProfileInfoTest { - - @Test - void 프로필_정보를_초기화한다() { - // when & then - assertDoesNotThrow(() -> ProfileInfo.of("재빠른지구001", "earth.png")); - } - - private static Stream constructorTestWithInvalidNickname() { - return Stream.of( - Arguments.of((Object) null), - Arguments.of(""), - Arguments.of(" "), - Arguments.of("1234"), - Arguments.of("12345678901") - ); - } - - @ParameterizedTest(name = "닉네임이 {0}일 때 프로필 정보를 초기화할 수 없다") - @MethodSource("constructorTestWithInvalidNickname") - void 유효한_길이의_닉네임이_아니라면_프로필_정보를_초기화할_수_없다(String invalidNickname) { - // when & then - assertThatThrownBy(() -> ProfileInfo.of(invalidNickname, "earth.png")) - .isInstanceOf(InvalidNicknameException.class) - .hasMessage("닉네임은 최소 5글자 이상, 최대 10글자 이하여야 합니다."); - } - - @ParameterizedTest(name = "프로필 이미지가 {0}일 때 프로필 정보를 초기화할 수 없다") - @NullAndEmptySource - void 비어_있는_프로필_이미지_경로라면_프로필_정보를_초기화할_수_없다(String invalidProfileImage) { - // when & then - assertThatThrownBy(() -> ProfileInfo.of("행복한지구001", invalidProfileImage)) - .isInstanceOf(InvalidProfileImageException.class) - .hasMessage("프로필 이미지 정보는 null이거나 비어 있을 수 없습니다."); - } - - @Test - void 프로필_정보를_변경한다() { - // given - ProfileInfo profileInfo = ProfileInfo.of("재빠른지구001", "earth.png"); - - // when - String changedNickname = "행복한화성001"; - String changedProfileImage = "mars.png"; - - profileInfo.changeProfileInfo(changedNickname, changedProfileImage); - - // then - assertAll( - () -> assertThat(profileInfo.getNickname()).isEqualTo(changedNickname), - () -> assertThat(profileInfo.getProfileImage()).isEqualTo(changedProfileImage) - ); - } - - @ParameterizedTest(name = "프로필 이미지가 {0}일 때 프로필 정보를 변경할 수 없다") - @NullAndEmptySource - void 비어_있는_프로필_이미지_경로라면_프로필_정보를_변경할_수_없다(String invalidProfileImage) { - // given - ProfileInfo profileInfo = ProfileInfo.of("재빠른지구001", "earth.png"); - - // when & then - assertThatThrownBy(() -> profileInfo.changeProfileInfo("행복한화성001", invalidProfileImage)) - .isInstanceOf(InvalidProfileImageException.class) - .hasMessage("프로필 이미지 정보는 null이거나 비어 있을 수 없습니다."); - } - - private static Stream changeProfileInfoTestWithInvalidNickname() { - return Stream.of( - Arguments.of((Object) null), - Arguments.of(""), - Arguments.of(" "), - Arguments.of("1234"), - Arguments.of("12345678901") - ); - } - - @ParameterizedTest(name = "닉네임이 {0}일 때 프로필 정보를 변경할 수 없다") - @MethodSource("changeProfileInfoTestWithInvalidNickname") - void 유효한_닉네임_길이가_아니라면_프로필_정보를_변경할_수_없다(String invalidNickname) { - // given - ProfileInfo profileInfo = ProfileInfo.of("nickname", "profileImage"); - - // when & then - assertThatThrownBy(() -> profileInfo.changeProfileInfo(invalidNickname, "profileImage")) - .isInstanceOf(InvalidNicknameException.class) - .hasMessage("닉네임은 최소 5글자 이상, 최대 10글자 이하여야 합니다."); - } -} diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/ProfileTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/ProfileTest.java new file mode 100644 index 00000000..0dfc55a0 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/ProfileTest.java @@ -0,0 +1,53 @@ +package com.dnd.spaced.core.account.domain.embed; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.account.domain.embed.exception.InvalidNicknameException; +import com.dnd.spaced.core.account.domain.embed.exception.InvalidProfileImageException; +import com.dnd.spaced.core.account.domain.enums.ProfileImageName; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ProfileTest { + + @Test + void 프로필_정보를_초기화한다() { + // when & then + assertDoesNotThrow(() -> Profile.of("재빠른지구001", ProfileImageName.EARTH)); + } + + private static Stream constructorTestWithInvalidNickname() { + return Stream.of( + Arguments.of((Object) null), + Arguments.of(""), + Arguments.of(" "), + Arguments.of("1234"), + Arguments.of("12345678901") + ); + } + + @ParameterizedTest(name = "닉네임이 {0}일 때 프로필 정보를 초기화할 수 없다") + @MethodSource("constructorTestWithInvalidNickname") + void 유효한_길이의_닉네임이_아니라면_프로필_정보를_초기화할_수_없다(String invalidNickname) { + // when & then + assertThatThrownBy(() -> Profile.of(invalidNickname, ProfileImageName.EARTH)) + .isInstanceOf(InvalidNicknameException.class) + .hasMessage("닉네임은 최소 5글자 이상, 최대 10글자 이하여야 합니다."); + } + + @Test + void 비어_있는_프로필_이미지_경로라면_프로필_정보를_초기화할_수_없다() { + // when & then + assertThatThrownBy(() -> Profile.of("행복한지구001", null)) + .isInstanceOf(InvalidProfileImageException.class) + .hasMessage("프로필 이미지 정보는 null일 수 없습니다."); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/SocialInfoTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/SocialTest.java similarity index 71% rename from space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/SocialInfoTest.java rename to space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/SocialTest.java index bd8ddd2e..b2f17baf 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/SocialInfoTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/account/domain/embed/SocialTest.java @@ -10,7 +10,7 @@ @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class SocialInfoTest { +class SocialTest { @Test void 회원의_소셜_정보를_초기화한다() { @@ -18,12 +18,12 @@ class SocialInfoTest { RegistrationId registrationId = RegistrationId.findBy("kakao"); // when - SocialInfo socialInfo = new SocialInfo(registrationId, "41258"); + Social social = new Social(registrationId, "41258"); // then assertAll( - () -> assertThat(socialInfo.getSocialIdentifier()).isEqualTo("41258"), - () -> assertThat(socialInfo.getRegistrationId()).isEqualTo(registrationId) + () -> assertThat(social.getSocialId()).isEqualTo("41258"), + () -> assertThat(social.getRegistrationId()).isEqualTo(registrationId) ); } -} \ No newline at end of file +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/domain/enums/ProfileImageNameTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/domain/enums/ProfileImageNameTest.java index 61cef830..ba34cd94 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/account/domain/enums/ProfileImageNameTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/account/domain/enums/ProfileImageNameTest.java @@ -24,7 +24,7 @@ class ProfileImageNameTest { assertDoesNotThrow(ProfileImageName::findRandom); } - private static Stream findByTestWithProfileImageKoreanName() { + private static Stream findByKoreanTestWithKoreanName() { return Stream.of( Arguments.of("수성", ProfileImageName.MERCURY), Arguments.of("금성", ProfileImageName.VENUS), @@ -37,11 +37,11 @@ private static Stream findByTestWithProfileImageKoreanName() { ); } - @ParameterizedTest(name = "프로필 이미지 이름이 {0}일 때 {1}을 반환한다") - @MethodSource("findByTestWithProfileImageKoreanName") - void 프로필_이미지를_찾는다(String korean, ProfileImageName expected) { + @ParameterizedTest(name = "프로필 이미지의 한글 이름이 {0}일 때 {1}을 반환한다") + @MethodSource("findByKoreanTestWithKoreanName") + void 한글_이름으로_프로필_이미지를_찾는다(String korean, ProfileImageName expected) { // when - ProfileImageName actual = ProfileImageName.findBy(korean); + ProfileImageName actual = ProfileImageName.findByKorean(korean); // then assertThat(actual).isEqualTo(expected); @@ -49,9 +49,41 @@ private static Stream findByTestWithProfileImageKoreanName() { @ParameterizedTest @NullAndEmptySource - void 유효한_이름이_아니라면_프로필_이미지를_찾을_수_없다(String invalidKoreanName) { + void 유효한_한글_이름이_아니라면_프로필_이미지를_찾을_수_없다(String invalidKoreanName) { // when & then - assertThatThrownBy(() -> ProfileImageName.findBy(invalidKoreanName)) + assertThatThrownBy(() -> ProfileImageName.findByKorean(invalidKoreanName)) + .isInstanceOf(InvalidProfileImageNameException.class) + .hasMessageContaining("잘못된 프로필 이미지 이름"); + } + + private static Stream findByImageNameTestWithImageName() { + return Stream.of( + Arguments.of("mercury.png", ProfileImageName.MERCURY), + Arguments.of("venus.png", ProfileImageName.VENUS), + Arguments.of("earth.png", ProfileImageName.EARTH), + Arguments.of("mars.png", ProfileImageName.MARS), + Arguments.of("jupiter.png", ProfileImageName.JUPITER), + Arguments.of("saturn.png", ProfileImageName.SATURN), + Arguments.of("uranus.png", ProfileImageName.URANUS), + Arguments.of("neptune.png", ProfileImageName.NEPTUNE) + ); + } + + @ParameterizedTest(name = "프로필 이미지의 이미지 이름이 {0}일 때 {1}을 반환한다") + @MethodSource("findByImageNameTestWithImageName") + void 이미지_이름으로_프로필_이미지를_찾는다(String korean, ProfileImageName expected) { + // when + ProfileImageName actual = ProfileImageName.findByImageName(korean); + + // then + assertThat(actual).isEqualTo(expected); + } + + @ParameterizedTest + @NullAndEmptySource + void 유효한_이미지_이름이_아니라면_프로필_이미지를_찾을_수_없다(String invalidImageName) { + // when & then + assertThatThrownBy(() -> ProfileImageName.findByImageName(invalidImageName)) .isInstanceOf(InvalidProfileImageNameException.class) .hasMessageContaining("잘못된 프로필 이미지 이름"); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepositoryTest.java new file mode 100644 index 00000000..338033c9 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/account/infrastructure/persistence/AccountGatewayRepositoryTest.java @@ -0,0 +1,160 @@ +package com.dnd.spaced.core.account.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.account.domain.Account; +import com.dnd.spaced.core.account.domain.embed.Social; +import com.dnd.spaced.core.account.domain.enums.ProfileImageName; +import com.dnd.spaced.core.account.domain.enums.RegistrationId; +import com.dnd.spaced.core.account.domain.enums.Role; +import com.dnd.spaced.core.account.domain.repository.AccountRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class AccountGatewayRepositoryTest { + + private static final long ACCOUNT_ID = 1L; + private static final long DELETED_ACCOUNT_ID = 2L; + private static final long PRE_INIT_ACCOUNT_ID = 3L; + private static final long DELETED_PRE_INIT_ACCOUNT_ID = 4L; + private static final RegistrationId REGISTRATION_ID = RegistrationId.KAKAO; + private static final String SOCIAL_ID = "12345"; + private static final String DELETED_SOCIAL_ID = "54321"; + + @Autowired + AccountRepository accountRepository; + + @Test + @Sql("classpath:sql/account/account.sql") + void 탈퇴하지_않은_회원의_id로_영속화_여부를_확인한다() { + // when + boolean actual = accountRepository.existsBy(ACCOUNT_ID); + + // then + assertThat(actual).isTrue(); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 탈퇴한_회원의_id로_영속화_여부를_확인한다() { + // when + boolean actual = accountRepository.existsBy(DELETED_ACCOUNT_ID); + + // then + assertThat(actual).isFalse(); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 탈퇴하지_않은_회원을_id로_조회한다() { + // when + Optional actual = accountRepository.findBy(ACCOUNT_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getId()).isEqualTo(ACCOUNT_ID) + ); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 탈퇴한_회원은_id로_조회할_수_없다() { + // when + Optional actual = accountRepository.findBy(DELETED_ACCOUNT_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + void 회원을_영속화_한다() { + // given + Account account = Account.builder() + .registrationId(RegistrationId.KAKAO) + .socialIdentifier("12345") + .nickname("재빠른지구001") + .profileImageName(ProfileImageName.EARTH) + .role(Role.ROLE_USER) + .build(); + + // when + Account actual = accountRepository.save(account); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 회원의_소셜_id로_탈퇴하지_않은_회원을_조회한다() { + // given + Social social = new Social(REGISTRATION_ID, SOCIAL_ID); + + // when + Optional actual = accountRepository.findBy(social); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getSocial().getRegistrationId()).isEqualTo(REGISTRATION_ID), + () -> assertThat(actual.get().getSocial().getSocialId()).isEqualTo(SOCIAL_ID) + ); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 탈퇴한_회원은_소셜_id로_조회할_수_없다() { + // given + Social social = new Social(REGISTRATION_ID, DELETED_SOCIAL_ID); + + // when + Optional actual = accountRepository.findBy(social); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 초기_설정만_진행한_회원을_id로_조회한다() { + // when + Optional actual = accountRepository.findPreInitializationAccountBy(PRE_INIT_ACCOUNT_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getId()).isEqualTo(PRE_INIT_ACCOUNT_ID) + ); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 설정을_모두_완료한_회원을_id로_조회할_수_없다() { + // when + Optional actual = accountRepository.findPreInitializationAccountBy(ACCOUNT_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql("classpath:sql/account/account.sql") + void 초기_설정_진행_도중_탈퇴한_회원을_id로_조회할_수_없다() { + // when + Optional actual = accountRepository.findPreInitializationAccountBy(DELETED_PRE_INIT_ACCOUNT_ID); + + // then + assertThat(actual).isEmpty(); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/account/presentation/AccountControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/account/presentation/AccountControllerTest.java index c75b2f7a..b8803717 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/account/presentation/AccountControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/account/presentation/AccountControllerTest.java @@ -20,12 +20,11 @@ import com.dnd.spaced.config.common.CommonControllerSliceTest; import com.dnd.spaced.config.docs.link.DocumentLinkGenerator.DocsUrl; import com.dnd.spaced.core.account.application.AccountService; -import com.dnd.spaced.core.account.application.dto.request.ChangeCareerInfoRequest; -import com.dnd.spaced.core.account.application.dto.request.ChangeProfileInfoRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeCareerRequest; +import com.dnd.spaced.core.account.application.dto.request.ChangeProfileRequest; import com.dnd.spaced.core.account.application.dto.response.AccountResponse; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; @@ -66,7 +65,7 @@ class AccountControllerTest extends CommonControllerSliceTest { @WithMockUser("1") void 회원_경력_정보_변경_요청_성공_테스트() throws Exception { // given - ChangeCareerInfoRequest request = new ChangeCareerInfoRequest("개발자", "중소기업", "비공개"); + ChangeCareerRequest request = new ChangeCareerRequest("개발자", "중소기업", "비공개"); // when & then @@ -78,7 +77,7 @@ class AccountControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(accountService).changeCareerInfo(anyLong(), any(ChangeCareerInfoRequest.class)); + verify(accountService).changeCareer(anyLong(), any(ChangeCareerRequest.class)); 회원_경력_정보_변경_요청_문서화(resultActions); } @@ -102,7 +101,7 @@ class AccountControllerTest extends CommonControllerSliceTest { @WithMockUser("1") void 회원_프로필_정보_변경_요청_성공_테스트() throws Exception { // given - ChangeProfileInfoRequest request = new ChangeProfileInfoRequest("행복한금성001", "금성"); + ChangeProfileRequest request = new ChangeProfileRequest("행복한금성001", "금성"); // when & then @@ -114,7 +113,7 @@ class AccountControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(accountService).changeProfileInfo(anyLong(), any(ChangeProfileInfoRequest.class)); + verify(accountService).changeProfile(anyLong(), any(ChangeProfileRequest.class)); 회원_프로필_정보_변경_요청_문서화(resultActions); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceFacadeTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceFacadeTest.java new file mode 100644 index 00000000..911a4a24 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceFacadeTest.java @@ -0,0 +1,74 @@ +package com.dnd.spaced.core.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.admin.application.exception.WordMetadataNotFoundException; +import com.dnd.spaced.core.quiz.application.event.dto.AddedTodayQuizQuestionEvent; +import com.dnd.spaced.core.quiz.application.exception.InvalidTodayQuizWordCountException; +import com.dnd.spaced.global.consts.CacheConst; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cache.CacheManager; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@RecordApplicationEvents +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class AdminTodayQuizServiceFacadeTest { + + @Autowired + ApplicationEvents events; + + @Autowired + AdminTodayQuizServiceFacade adminTodayQuizServiceFacade; + + @Autowired + CacheManager memoryCacheManager; + + @Test + void 용어_메타데이터가_정상적으로_설정되지_않다면_오늘의_퀴즈를_생성할_수_없다() { + // when & then + assertThatThrownBy(() -> adminTodayQuizServiceFacade.createTodayQuiz()) + .isInstanceOf(WordMetadataNotFoundException.class) + .hasMessage("용어 메타데이터가 정상적으로 설정되지 않았습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/quiz/word_metadata.sql", + "classpath:sql/admin/quiz/quiz_metadata.sql" + }) + void 등록된_용어_수가_퀴즈_생성_시_필요한_용어_수보다_적으면_퀴즈를_생성할_수_없다() { + // when & then + assertThatThrownBy(() -> adminTodayQuizServiceFacade.createTodayQuiz()) + .isInstanceOf(InvalidTodayQuizWordCountException.class) + .hasMessage("오늘의 퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/quiz/word_metadata.sql", + "classpath:sql/admin/quiz/quiz_metadata.sql", + "classpath:sql/admin/quiz/word.sql" + }) + void 오늘의_퀴즈를_생성한다() { + // when + Long savedTodayQuizId = adminTodayQuizServiceFacade.createTodayQuiz(); + + // then + assertAll( + () -> assertThat(savedTodayQuizId).isPositive(), + () -> assertThat(events.stream(AddedTodayQuizQuestionEvent.class).count()).isOne(), + () -> assertThat(memoryCacheManager.getCache(CacheConst.TODAY_QUIZ_CACHE_NAME)).isNotNull() + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminWordServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminWordServiceFacadeTest.java similarity index 84% rename from space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminWordServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminWordServiceFacadeTest.java index 8868a82f..7d8347c0 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminWordServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminWordServiceFacadeTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; @@ -11,6 +12,7 @@ import com.dnd.spaced.core.admin.application.exception.PronunciationNotFoundException; import com.dnd.spaced.core.admin.application.exception.WordExampleDeletionNotAllowedException; import com.dnd.spaced.core.admin.application.exception.WordExampleNotFoundException; +import com.dnd.spaced.core.word.application.event.dto.PersistedWordEvent; import com.dnd.spaced.core.word.application.exception.WordNotFoundException; import com.dnd.spaced.core.word.domain.exception.InvalidWordExampleContentException; import java.util.List; @@ -30,10 +32,10 @@ @SuppressWarnings("NonAsciiCharacters") @RecordApplicationEvents @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class AdminWordServiceTest { +class AdminWordServiceFacadeTest { @Autowired - AdminWordService adminWordService; + AdminWordServiceFacade adminWordServiceFacade; @Autowired ApplicationEvents events; @@ -55,10 +57,13 @@ class AdminWordServiceTest { ); // when - Long actual = adminWordService.createWord(request); + Long actual = adminWordServiceFacade.createWord(request); // then - assertThat(actual).isPositive(); + assertAll( + () -> assertThat(actual).isPositive(), + () -> assertThat(events.stream(PersistedWordEvent.class).count()).isOne() + ); } @Test @@ -68,7 +73,7 @@ class AdminWordServiceTest { }) void 용어_예문을_변경한다() { // when & then - assertDoesNotThrow(() -> adminWordService.updateWordExample( + assertDoesNotThrow(() -> adminWordServiceFacade.updateWordExample( 1L, "이 기능은 일반 사용자의 Authorization 범위를 벗어나므로, 관리자 권한이 필요합니다.") ); @@ -78,7 +83,7 @@ class AdminWordServiceTest { void 잘못된_용어_예문_ID라면_용어_예문을_변경할_수_없다() { // when & then assertThatThrownBy( - () -> adminWordService.updateWordExample( + () -> adminWordServiceFacade.updateWordExample( -999L, "이 기능은 일반 사용자의 Authorization 범위를 벗어나므로, 관리자 권한이 필요합니다." ) @@ -95,7 +100,7 @@ class AdminWordServiceTest { void 유효하지_않은_길이의_예문으로_용어_예문을_변경할_수_없다(String invalidContent) { // when & then assertThatThrownBy( - () -> adminWordService.updateWordExample( + () -> adminWordServiceFacade.updateWordExample( 1L, invalidContent ) @@ -111,7 +116,7 @@ class AdminWordServiceTest { void 용어_예문을_삭제한다() { // when & then assertDoesNotThrow( - () -> adminWordService.deleteWordExample(1L, 1L) + () -> adminWordServiceFacade.deleteWordExample(1L, 1L) ); } @@ -122,7 +127,7 @@ class AdminWordServiceTest { }) void 잘못된_용어_예문_ID로_용어_예문을_삭제할_수_없다() { // when & then - assertThatThrownBy(() -> adminWordService.deleteWordExample(1L, -999L)) + assertThatThrownBy(() -> adminWordServiceFacade.deleteWordExample(1L, -999L)) .isInstanceOf(WordExampleNotFoundException.class) .hasMessage("지정한 용어 예문을 찾을 수 없습니다."); } @@ -135,7 +140,7 @@ class AdminWordServiceTest { void 용어_예문의_개수가_최소치라면_용어_예문을_삭제할_수_없다() { // when & then assertThatThrownBy( - () -> adminWordService.deleteWordExample(2L, 3L) + () -> adminWordServiceFacade.deleteWordExample(2L, 3L) ).isInstanceOf(WordExampleDeletionNotAllowedException.class) .hasMessage("해당 용어의 예문 개수가 최소치입니다."); } @@ -147,7 +152,7 @@ class AdminWordServiceTest { }) void 잘못된_용어_발음_ID로_용어_발음을_삭제할_수_없다() { // when & then - assertThatThrownBy(() -> adminWordService.deletePronunciation(1L, -999L)) + assertThatThrownBy(() -> adminWordServiceFacade.deletePronunciation(1L, -999L)) .isInstanceOf(PronunciationNotFoundException.class) .hasMessage("지정한 발음을 찾지 못했습니다."); } @@ -160,7 +165,7 @@ class AdminWordServiceTest { void 용어_발음_정보를_삭제한다() { // when & then assertDoesNotThrow( - () -> adminWordService.deletePronunciation(1L, 1L) + () -> adminWordServiceFacade.deletePronunciation(1L, 1L) ); } @@ -172,7 +177,7 @@ class AdminWordServiceTest { void 용어_발음_정보의_개수가_최소치라면_용어_발음_정보를_삭제할_수_없다() { // when & then assertThatThrownBy( - () -> adminWordService.deletePronunciation(2L, 1L) + () -> adminWordServiceFacade.deletePronunciation(2L, 1L) ).isInstanceOf(PronunciationDeletionNotAllowedException.class) .hasMessage("해당 용어의 발음 정보 개수가 최소치입니다."); } @@ -180,7 +185,7 @@ class AdminWordServiceTest { @Test void 유효하지_않은_용어_ID로_용어를_삭제할_수_없다() { // when & then - assertThatThrownBy(() -> adminWordService.deleteWord(-999L)) + assertThatThrownBy(() -> adminWordServiceFacade.deleteWord(-999L)) .isInstanceOf(WordNotFoundException.class) .hasMessage("지정한 용어를 찾을 수 없습니다."); } @@ -192,7 +197,7 @@ class AdminWordServiceTest { }) void 용어를_삭제한다() { // when & then - assertDoesNotThrow(() -> adminWordService.deleteWord(1L)); + assertDoesNotThrow(() -> adminWordServiceFacade.deleteWord(1L)); assertThat(events.stream(DeletedWordEvent.class).count()).isOne(); } } diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/CreateTodayQuizServiceTest.java similarity index 78% rename from space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/admin/application/CreateTodayQuizServiceTest.java index 3b0fb8a9..2991abdf 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/admin/application/AdminTodayQuizServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/CreateTodayQuizServiceTest.java @@ -5,32 +5,28 @@ import com.dnd.spaced.core.admin.application.exception.WordMetadataNotFoundException; import com.dnd.spaced.core.quiz.application.exception.InvalidTodayQuizWordCountException; +import com.dnd.spaced.core.quiz.domain.TodayQuiz; +import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.event.ApplicationEvents; -import org.springframework.test.context.event.RecordApplicationEvents; import org.springframework.test.context.jdbc.Sql; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@RecordApplicationEvents @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class AdminTodayQuizServiceTest { +class CreateTodayQuizServiceTest { @Autowired - ApplicationEvents events; - - @Autowired - AdminTodayQuizService adminTodayQuizService; + CreateTodayQuizService createTodayQuizService; @Test void 용어_메타데이터가_정상적으로_설정되지_않다면_오늘의_퀴즈를_생성할_수_없다() { // when & then - assertThatThrownBy(() -> adminTodayQuizService.createTodayQuiz()) + assertThatThrownBy(() -> createTodayQuizService.createTodayQuiz(QuizCategory.DEVELOP)) .isInstanceOf(WordMetadataNotFoundException.class) .hasMessage("용어 메타데이터가 정상적으로 설정되지 않았습니다."); } @@ -42,7 +38,7 @@ class AdminTodayQuizServiceTest { }) void 등록된_용어_수가_퀴즈_생성_시_필요한_용어_수보다_적으면_퀴즈를_생성할_수_없다() { // when & then - assertThatThrownBy(() -> adminTodayQuizService.createTodayQuiz()) + assertThatThrownBy(() -> createTodayQuizService.createTodayQuiz(QuizCategory.DEVELOP)) .isInstanceOf(InvalidTodayQuizWordCountException.class) .hasMessage("오늘의 퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); } @@ -55,9 +51,9 @@ class AdminTodayQuizServiceTest { }) void 오늘의_퀴즈를_생성한다() { // when - Long savedTodayQuizId = adminTodayQuizService.createTodayQuiz(); + TodayQuiz actual = createTodayQuizService.createTodayQuiz(QuizCategory.DEVELOP); // then - assertThat(savedTodayQuizId).isPositive(); + assertThat(actual.getId()).isPositive(); } } diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/CreateWordServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/CreateWordServiceTest.java new file mode 100644 index 00000000..20c5608c --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/CreateWordServiceTest.java @@ -0,0 +1,52 @@ +package com.dnd.spaced.core.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; +import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest.CreatePronunciationRequest; +import com.dnd.spaced.core.admin.application.dto.resposne.PersistWordDto; +import com.dnd.spaced.core.word.domain.enums.Category; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreateWordServiceTest { + + @Autowired + CreateWordService createWordService; + + @Test + @Sql("classpath:sql/admin/word/word_metadata.sql") + void 용어를_추가한다() { + // given + List createPronunciationRequests = List.of( + new CreatePronunciationRequest("어써라이제이션", "한글 발음") + ); + List examples = List.of("게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다."); + CreateWordRequest request = new CreateWordRequest( + "Authorization", + "Authorization(권한 부여)은 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘", + "개발", + createPronunciationRequests, + examples + ); + + // when + PersistWordDto actual = createWordService.createWord(request); + + // then + assertAll( + () -> assertThat(actual.id()).isPositive(), + () -> assertThat(actual.category()).isEqualTo(Category.DEVELOP) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/DeleteWordServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/DeleteWordServiceTest.java new file mode 100644 index 00000000..c97b9923 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/DeleteWordServiceTest.java @@ -0,0 +1,118 @@ +package com.dnd.spaced.core.admin.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.admin.application.exception.PronunciationDeletionNotAllowedException; +import com.dnd.spaced.core.admin.application.exception.PronunciationNotFoundException; +import com.dnd.spaced.core.admin.application.exception.WordExampleDeletionNotAllowedException; +import com.dnd.spaced.core.admin.application.exception.WordExampleNotFoundException; +import com.dnd.spaced.core.word.application.exception.WordNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DeleteWordServiceTest { + + @Autowired + DeleteWordService deleteWordService; + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 용어_예문을_삭제한다() { + // when & then + assertDoesNotThrow( + () -> deleteWordService.deleteWordExample(1L, 1L) + ); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 잘못된_용어_예문_ID로_용어_예문을_삭제할_수_없다() { + // when & then + assertThatThrownBy(() -> deleteWordService.deleteWordExample(1L, -999L)) + .isInstanceOf(WordExampleNotFoundException.class) + .hasMessage("지정한 용어 예문을 찾을 수 없습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 용어_예문의_개수가_최소치라면_용어_예문을_삭제할_수_없다() { + // when & then + assertThatThrownBy( + () -> deleteWordService.deleteWordExample(2L, 3L) + ).isInstanceOf(WordExampleDeletionNotAllowedException.class) + .hasMessage("해당 용어의 예문 개수가 최소치입니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 잘못된_용어_발음_ID로_용어_발음을_삭제할_수_없다() { + // when & then + assertThatThrownBy(() -> deleteWordService.deletePronunciation(1L, -999L)) + .isInstanceOf(PronunciationNotFoundException.class) + .hasMessage("지정한 발음을 찾지 못했습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 용어_발음_정보를_삭제한다() { + // when & then + assertDoesNotThrow( + () -> deleteWordService.deletePronunciation(1L, 1L) + ); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 용어_발음_정보의_개수가_최소치라면_용어_발음_정보를_삭제할_수_없다() { + // when & then + assertThatThrownBy( + () -> deleteWordService.deletePronunciation(2L, 1L) + ).isInstanceOf(PronunciationDeletionNotAllowedException.class) + .hasMessage("해당 용어의 발음 정보 개수가 최소치입니다."); + } + + @Test + void 유효하지_않은_용어_ID로_용어를_삭제할_수_없다() { + // when & then + assertThatThrownBy(() -> deleteWordService.deleteWord(-999L)) + .isInstanceOf(WordNotFoundException.class) + .hasMessage("지정한 용어를 찾을 수 없습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 용어를_삭제한다() { + // when & then + assertDoesNotThrow(() -> deleteWordService.deleteWord(1L)); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/UpdateWordServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/UpdateWordServiceTest.java new file mode 100644 index 00000000..1942d249 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/UpdateWordServiceTest.java @@ -0,0 +1,47 @@ +package com.dnd.spaced.core.admin.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.admin.application.exception.WordExampleNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UpdateWordServiceTest { + + @Autowired + UpdateWordService updateWordService; + + @Test + @Sql(scripts = { + "classpath:sql/admin/word/word_metadata.sql", + "classpath:sql/admin/word/word.sql" + }) + void 용어_예문을_변경한다() { + // when & then + assertDoesNotThrow(() -> updateWordService.updateWordExample( + 1L, + "이 기능은 일반 사용자의 Authorization 범위를 벗어나므로, 관리자 권한이 필요합니다.") + ); + } + + @Test + void 잘못된_용어_예문_ID라면_용어_예문을_변경할_수_없다() { + // when & then + assertThatThrownBy( + () -> updateWordService.updateWordExample( + -999L, + "이 기능은 일반 사용자의 Authorization 범위를 벗어나므로, 관리자 권한이 필요합니다." + ) + ).isInstanceOf(WordExampleNotFoundException.class) + .hasMessage("지정한 용어 예문을 찾을 수 없습니다."); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListenerTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListenerTest.java index 5454408a..74e007c7 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListenerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/event/listener/AdminWordEventListenerTest.java @@ -10,7 +10,7 @@ import static org.mockito.Mockito.verify; import com.dnd.spaced.core.admin.application.event.dto.DeletedWordEvent; -import com.dnd.spaced.core.word.application.DeletedWordIdRepository; +import com.dnd.spaced.core.word.application.repository.DeletedWordIdRepository; import com.dnd.spaced.core.word.domain.repository.PronunciationRepository; import com.dnd.spaced.core.word.domain.repository.WordExampleRepository; import java.time.LocalDateTime; diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/application/schedule/CreateTodayQuizSchedulerTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/application/schedule/CreateTodayQuizSchedulerTest.java new file mode 100644 index 00000000..5c6d476d --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/application/schedule/CreateTodayQuizSchedulerTest.java @@ -0,0 +1,55 @@ +package com.dnd.spaced.core.admin.application.schedule; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import com.dnd.spaced.core.quiz.domain.repository.TodayQuizRepository; +import jakarta.persistence.EntityManager; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreateTodayQuizSchedulerTest { + + @Autowired + CreateTodayQuizScheduler createTodayQuizScheduler; + + @Autowired + TodayQuizRepository todayQuizRepository; + + @Autowired + EntityManager em; + + @Test + @Sql(scripts = { + "classpath:sql/admin/quiz/word_metadata.sql", + "classpath:sql/admin/quiz/quiz_metadata.sql", + "classpath:sql/admin/quiz/word.sql" + }) + void 오늘의_퀴즈를_생성한다() { + // given + Optional todayQuiz = todayQuizRepository.findLatest(); + + assertThat(todayQuiz).isEmpty(); + + // when + createTodayQuizScheduler.schedule(); + + // then + em.clear(); + + Optional actual = todayQuizRepository.findLatest(); + + assertThat(actual).hasValueSatisfying(value -> { + assertThat(value.id()).isEqualTo(1L); + }); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizControllerTest.java index 42178ea8..5a69b264 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminTodayQuizControllerTest.java @@ -10,10 +10,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.dnd.spaced.config.common.CommonControllerSliceTest; -import com.dnd.spaced.core.admin.application.AdminTodayQuizService; +import com.dnd.spaced.core.admin.application.AdminTodayQuizServiceFacade; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpHeaders; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.ResultActions; @@ -22,13 +21,13 @@ class AdminTodayQuizControllerTest extends CommonControllerSliceTest { @Autowired - AdminTodayQuizService adminTodayQuizService; + AdminTodayQuizServiceFacade adminTodayQuizServiceFacade; @Test @WithMockUser(value = "1", roles = "ADMIN") void 오늘의_퀴즈_수동_생성_요청_성공_테스트() throws Exception { // given - given(adminTodayQuizService.createTodayQuiz()).willReturn(1L); + given(adminTodayQuizServiceFacade.createTodayQuiz()).willReturn(1L); // when & then ResultActions resultActions = mockMvc.perform( @@ -38,7 +37,7 @@ class AdminTodayQuizControllerTest extends CommonControllerSliceTest { header().string("Location", "/today-quizzes/1") ); - verify(adminTodayQuizService).createTodayQuiz(); + verify(adminTodayQuizServiceFacade).createTodayQuiz(); 오늘의_퀴즈_수동_생성_요청_문서화(resultActions); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminWordControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminWordControllerTest.java index 5676a241..96988505 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminWordControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/admin/presentation/AdminWordControllerTest.java @@ -22,7 +22,7 @@ import com.dnd.spaced.config.common.CommonControllerSliceTest; import com.dnd.spaced.config.docs.link.DocumentLinkGenerator.DocsUrl; -import com.dnd.spaced.core.admin.application.AdminWordService; +import com.dnd.spaced.core.admin.application.AdminWordServiceFacade; import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest.CreatePronunciationRequest; import com.dnd.spaced.core.admin.application.dto.request.UpdateWordExampleRequest; @@ -38,7 +38,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { @Autowired - AdminWordService adminWordService; + AdminWordServiceFacade adminWordServiceFacade; @Test @WithMockUser(value = "1", roles = "ADMIN") @@ -56,7 +56,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { example ); - given(adminWordService.createWord(any(CreateWordRequest.class))).willReturn(1L); + given(adminWordServiceFacade.createWord(any(CreateWordRequest.class))).willReturn(1L); // when & then ResultActions resultActions = mockMvc.perform( @@ -68,7 +68,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { header().stringValues("Location", "/words/1") ); - verify(adminWordService).createWord(any(CreateWordRequest.class)); + verify(adminWordServiceFacade).createWord(any(CreateWordRequest.class)); 용어_등록_요청_문서화(resultActions); } @@ -116,7 +116,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(adminWordService).updateWordExample(anyLong(), anyString()); + verify(adminWordServiceFacade).updateWordExample(anyLong(), anyString()); 용어_예문_변경_요청_문서화(resultActions); } @@ -148,7 +148,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(adminWordService).deleteWordExample(anyLong(), anyLong()); + verify(adminWordServiceFacade).deleteWordExample(anyLong(), anyLong()); 용어_예문_삭제_요청_문서화(resultActions); } @@ -178,7 +178,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(adminWordService).deletePronunciation(anyLong(), anyLong()); + verify(adminWordServiceFacade).deletePronunciation(anyLong(), anyLong()); 용어_발음_정보_삭제_요청_문서화(resultAction); } @@ -206,7 +206,7 @@ class AdminWordControllerTest extends CommonControllerSliceTest { .header(HttpHeaders.AUTHORIZATION, "Bearer AccessToken") ).andExpectAll(status().isNoContent()); - verify(adminWordService).deleteWord(anyLong()); + verify(adminWordServiceFacade).deleteWord(anyLong()); 용어_삭제_요청_문서화(resultAction); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/GenerateRefreshTokenServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/auth/application/GenerateRefreshTokenServiceTest.java similarity index 95% rename from space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/GenerateRefreshTokenServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/auth/application/GenerateRefreshTokenServiceTest.java index b2b7c093..9d197365 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/GenerateRefreshTokenServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/auth/application/GenerateRefreshTokenServiceTest.java @@ -1,4 +1,4 @@ -package com.dnd.spaced.core.auth.application.internal; +package com.dnd.spaced.core.auth.application; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; diff --git a/space-d/src/test/java/com/dnd/spaced/core/auth/application/InitAccountCareerInfoServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/auth/application/InitAccountCareerServiceTest.java similarity index 78% rename from space-d/src/test/java/com/dnd/spaced/core/auth/application/InitAccountCareerInfoServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/auth/application/InitAccountCareerServiceTest.java index c483f6c5..7fa2a31f 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/auth/application/InitAccountCareerInfoServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/auth/application/InitAccountCareerServiceTest.java @@ -6,7 +6,7 @@ import com.dnd.spaced.core.account.domain.enums.exception.InvalidCompanyException; import com.dnd.spaced.core.account.domain.enums.exception.InvalidExperienceException; import com.dnd.spaced.core.account.domain.enums.exception.InvalidJobGroupException; -import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerInfoRequest; +import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerRequest; import com.dnd.spaced.core.auth.application.exception.ForbiddenInitCareerInfoException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; @@ -21,23 +21,23 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class InitAccountCareerInfoServiceTest { +class InitAccountCareerServiceTest { @Autowired - InitAccountCareerInfoService initAccountCareerInfoService; + InitAccountCareerService initAccountCareerService; @Test @Sql("classpath:sql/auth/account.sql") void 경력_정보를_초기화한다() { // given - InitAccountCareerInfoRequest request = new InitAccountCareerInfoRequest( + InitAccountCareerRequest request = new InitAccountCareerRequest( "개발자", "비공개", "1~2년 차" ); // when & then - assertDoesNotThrow(() -> initAccountCareerInfoService.initCareerInfo(1L, request)); + assertDoesNotThrow(() -> initAccountCareerService.initCareer(1L, request)); } @ParameterizedTest(name = "회사명이 {0}일 때 경력 정보를 초기화할 수 없다") @@ -45,14 +45,14 @@ class InitAccountCareerInfoServiceTest { @Sql("classpath:sql/auth/account.sql") void 유효한_회사명이_아닌_경우_경력_정보를_초기화할_수_없다(String invalidCompanyName) { // given - InitAccountCareerInfoRequest request = new InitAccountCareerInfoRequest( + InitAccountCareerRequest request = new InitAccountCareerRequest( "개발자", invalidCompanyName, "1~2년 차" ); // when & then - assertThatThrownBy(() -> initAccountCareerInfoService.initCareerInfo(1L, request)) + assertThatThrownBy(() -> initAccountCareerService.initCareer(1L, request)) .isInstanceOf(InvalidCompanyException.class) .hasMessageContaining("잘못된 회사 이름"); } @@ -62,14 +62,14 @@ class InitAccountCareerInfoServiceTest { @Sql("classpath:sql/auth/account.sql") void 유효한_직군이_아닌_경우_경력_정보를_초기화할_수_없다(String invalidJobGroupName) { // given - InitAccountCareerInfoRequest request = new InitAccountCareerInfoRequest( + InitAccountCareerRequest request = new InitAccountCareerRequest( invalidJobGroupName, "비공개", "1~2년 차" ); // when & then - assertThatThrownBy(() -> initAccountCareerInfoService.initCareerInfo(1L, request)) + assertThatThrownBy(() -> initAccountCareerService.initCareer(1L, request)) .isInstanceOf(InvalidJobGroupException.class) .hasMessageContaining("잘못된 직군 이름"); } @@ -79,14 +79,14 @@ class InitAccountCareerInfoServiceTest { @Sql("classpath:sql/auth/account.sql") void 유효한_경력이_아닌_경우_경력_정보를_초기화할_수_없다(String invalidExperienceName) { // given - InitAccountCareerInfoRequest request = new InitAccountCareerInfoRequest( + InitAccountCareerRequest request = new InitAccountCareerRequest( "개발자", "비공개", invalidExperienceName ); // when & then - assertThatThrownBy(() -> initAccountCareerInfoService.initCareerInfo(1L, request)) + assertThatThrownBy(() -> initAccountCareerService.initCareer(1L, request)) .isInstanceOf(InvalidExperienceException.class) .hasMessageContaining("잘못된 경력"); } @@ -94,14 +94,14 @@ class InitAccountCareerInfoServiceTest { @Test void 회원_ID가_없거나_이미_탈퇴한_경우_경력_정보를_초기화할_수_없다() { // given - InitAccountCareerInfoRequest request = new InitAccountCareerInfoRequest( + InitAccountCareerRequest request = new InitAccountCareerRequest( "개발자", "비공개", "1~2년 차" ); // when & then - assertThatThrownBy(() -> initAccountCareerInfoService.initCareerInfo(-999L, request)) + assertThatThrownBy(() -> initAccountCareerService.initCareer(-999L, request)) .isInstanceOf(ForbiddenInitCareerInfoException.class) .hasMessage("최초로 가입한 회원이 아닙니다."); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/LoginServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/auth/application/LoginServiceTest.java similarity index 92% rename from space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/LoginServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/auth/application/LoginServiceTest.java index dd38cf62..f18e2c15 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/LoginServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/auth/application/LoginServiceTest.java @@ -1,10 +1,10 @@ -package com.dnd.spaced.core.auth.application.internal; +package com.dnd.spaced.core.auth.application; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.dnd.spaced.core.auth.application.dto.response.LoggedInAccountInfoDto; +import com.dnd.spaced.core.auth.application.dto.response.LoggedInAccountDto; import com.dnd.spaced.core.auth.application.exception.NicknameMetadataNotFoundException; import com.dnd.spaced.core.skill.application.event.dto.InitializedAccountEvent; import org.junit.jupiter.api.DisplayNameGeneration; @@ -33,7 +33,7 @@ class LoginServiceTest { @Sql("classpath:sql/auth/nickname_metadata.sql") void 회원가입하지_않은_회원이_로그인하면_회원_가입과_로그인_절차를_진행한다() { // when - LoggedInAccountInfoDto actual = loginService.login("kakao", "12345"); + LoggedInAccountDto actual = loginService.login("kakao", "12345"); // then assertAll( @@ -52,7 +52,7 @@ class LoginServiceTest { void 회원가입한_회원이_로그인하면_로그인_절차를_진행한다() { // given // when - LoggedInAccountInfoDto actual = loginService.login("kakao", "12345"); + LoggedInAccountDto actual = loginService.login("kakao", "12345"); // then assertAll( diff --git a/space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/SignUpServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/auth/application/SignUpServiceTest.java similarity index 87% rename from space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/SignUpServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/auth/application/SignUpServiceTest.java index 4bfe1591..f7cd9491 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/auth/application/internal/SignUpServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/auth/application/SignUpServiceTest.java @@ -1,4 +1,4 @@ -package com.dnd.spaced.core.auth.application.internal; +package com.dnd.spaced.core.auth.application; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -42,8 +42,8 @@ class SignUpServiceTest { // then assertAll( () -> assertThat(actual.getId()).isEqualTo(1L), - () -> assertThat(actual.getSocialInfo().getSocialIdentifier()).isEqualTo("12345"), - () -> assertThat(actual.getSocialInfo().getRegistrationId()).isEqualTo(RegistrationId.KAKAO) + () -> assertThat(actual.getSocial().getSocialId()).isEqualTo("12345"), + () -> assertThat(actual.getSocial().getRegistrationId()).isEqualTo(RegistrationId.KAKAO) ); } } diff --git a/space-d/src/test/java/com/dnd/spaced/core/auth/presentation/AuthControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/auth/presentation/AuthControllerTest.java index 706d0b32..8e1d90e3 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/auth/presentation/AuthControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/auth/presentation/AuthControllerTest.java @@ -24,9 +24,9 @@ import com.dnd.spaced.config.common.CommonControllerSliceTest; import com.dnd.spaced.config.docs.link.DocumentLinkGenerator.DocsUrl; -import com.dnd.spaced.core.auth.application.InitAccountCareerInfoService; +import com.dnd.spaced.core.auth.application.InitAccountCareerService; import com.dnd.spaced.core.auth.application.RefreshTokenService; -import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerInfoRequest; +import com.dnd.spaced.core.auth.application.dto.request.InitAccountCareerRequest; import com.dnd.spaced.core.auth.application.dto.response.TokenDto; import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.Test; @@ -40,7 +40,7 @@ class AuthControllerTest extends CommonControllerSliceTest { @Autowired - InitAccountCareerInfoService initAccountCareerInfoService; + InitAccountCareerService initAccountCareerService; @Autowired RefreshTokenService refreshTokenService; @@ -49,9 +49,9 @@ class AuthControllerTest extends CommonControllerSliceTest { @WithMockUser("1") void 회원_프로필_초기화_요청_성공_테스트() throws Exception { // given - willDoNothing().given(initAccountCareerInfoService) - .initCareerInfo(anyLong(), any(InitAccountCareerInfoRequest.class)); - InitAccountCareerInfoRequest request = new InitAccountCareerInfoRequest( + willDoNothing().given(initAccountCareerService) + .initCareer(anyLong(), any(InitAccountCareerRequest.class)); + InitAccountCareerRequest request = new InitAccountCareerRequest( "개발자", "중소기업", "1년 차 미만" @@ -66,7 +66,7 @@ class AuthControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(initAccountCareerInfoService).initCareerInfo(anyLong(), any(InitAccountCareerInfoRequest.class)); + verify(initAccountCareerService).initCareer(anyLong(), any(InitAccountCareerRequest.class)); 회원_프로필_초기화_요청_문서화(resultActions); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceFacadeTest.java similarity index 78% rename from space-d/src/test/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceFacadeTest.java index 797b87c2..03c81868 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/BookmarkServiceFacadeTest.java @@ -27,22 +27,28 @@ @RecordApplicationEvents @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class BookmarkServiceTest { +class BookmarkServiceFacadeTest { + + private static final Long ACCOUNT_ID = 1L; + private static final Long NOT_FOUND_WORD_ID = -999L; + private static final Long WORD_ID = 1L; + private static final Long BOOKMARK_ID = 1L; + private static final Long LAST_BOOKMARK_ID = 1L; @Autowired ApplicationEvents events; @Autowired - BookmarkService bookmarkService; + BookmarkServiceFacade bookmarkServiceFacade; @Test @Sql("classpath:sql/bookmark/word.sql") void 북마크를_추가한다() { // given - CreateBookmarkRequest request = new CreateBookmarkRequest(1L); + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); // when - bookmarkService.createBookmark(1L, request); + bookmarkServiceFacade.createBookmark(ACCOUNT_ID, request); // then assertThat(events.stream(WordBookmarkCountIncrementedEvent.class).count()).isOne(); @@ -55,10 +61,10 @@ class BookmarkServiceTest { }) void 이미_북마크에_추가된_용어를_북마에_추가할_수_없다() { // given - CreateBookmarkRequest request = new CreateBookmarkRequest(1L); + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); // when & then - assertThatThrownBy(() -> bookmarkService.createBookmark(1L, request)) + assertThatThrownBy(() -> bookmarkServiceFacade.createBookmark(ACCOUNT_ID, request)) .isInstanceOf(AlreadyExistsBookmarkException.class) .hasMessage("이미 북마크에 추가된 용어입니다."); } @@ -66,10 +72,10 @@ class BookmarkServiceTest { @Test void 지정한_용어_식별자로_용어를_찾지_못하면_북마크를_추가할_수_없다() { // given - CreateBookmarkRequest request = new CreateBookmarkRequest(-999L); + CreateBookmarkRequest request = new CreateBookmarkRequest(NOT_FOUND_WORD_ID); // when & then - assertThatThrownBy(() -> bookmarkService.createBookmark(1L, request)) + assertThatThrownBy(() -> bookmarkServiceFacade.createBookmark(ACCOUNT_ID, request)) .isInstanceOf(WordNotFoundException.class) .hasMessage("지정한 식별자의 용어를 찾지 못했습니다."); @@ -79,31 +85,34 @@ class BookmarkServiceTest { @Sql("classpath:sql/bookmark/bookmark.sql") void 북마크를_삭제한다() { // given - DeleteBookmarkRequest request = new DeleteBookmarkRequest(1L); + DeleteBookmarkRequest request = new DeleteBookmarkRequest(WORD_ID); // when - bookmarkService.deleteBookmark(1L, request); + bookmarkServiceFacade.deleteBookmark(ACCOUNT_ID, request); // then assertThat(events.stream(WordBookmarkCountDecrementedEvent.class).count()).isOne(); } @Test - @Sql("classpath:sql/bookmark/bookmark.sql") + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) void 회원이_생성한_북마크를_모두_조회한다() { // given ReadAllBookmarkRequest request = new ReadAllBookmarkRequest(null); // when - BookmarkCollectionResponse actual = bookmarkService.readBookmarks(1L, request, PageRequest.of(0, 10)); + BookmarkCollectionResponse actual = bookmarkServiceFacade.readBookmarks(ACCOUNT_ID, request, PageRequest.of(0, 10)); // then assertAll( () -> assertThat(actual.bookmarks()).hasSize(1), - () -> assertThat(actual.lastBookmarkId()).isEqualTo(1L), - () -> assertThat(actual.bookmarks().get(0).bookmarkId()).isEqualTo(1L), - () -> assertThat(actual.bookmarks().get(0).accountId()).isEqualTo(1L), - () -> assertThat(actual.bookmarks().get(0).wordId()).isEqualTo(1L) + () -> assertThat(actual.lastBookmarkId()).isEqualTo(LAST_BOOKMARK_ID), + () -> assertThat(actual.bookmarks().get(0).bookmarkId()).isEqualTo(BOOKMARK_ID), + () -> assertThat(actual.bookmarks().get(0).accountId()).isEqualTo(ACCOUNT_ID), + () -> assertThat(actual.bookmarks().get(0).wordId()).isEqualTo(WORD_ID) ); } } diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyBookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyBookmarkServiceFacadeTest.java similarity index 72% rename from space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyBookmarkServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyBookmarkServiceFacadeTest.java index 3749e4db..476a8149 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyBookmarkServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyBookmarkServiceFacadeTest.java @@ -1,5 +1,6 @@ package com.dnd.spaced.core.bookmark.application; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -12,6 +13,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -23,10 +25,13 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class ConcurrencyBookmarkServiceTest { +class ConcurrencyBookmarkServiceFacadeTest { + + private static final Long WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; @Autowired - BookmarkService bookmarkService; + BookmarkServiceFacade bookmarkServiceFacade; @Autowired BookmarkRepository bookmarkRepository; @@ -36,29 +41,31 @@ class ConcurrencyBookmarkServiceTest { void 동시에_동일한_용어에_북마크_생성_요청을_하더라도_단_하나의_북마크만_생성되어야_한다() throws InterruptedException { int numberOfThreads = 10; ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); - CountDownLatch latch = new CountDownLatch(numberOfThreads); - CreateBookmarkRequest request = new CreateBookmarkRequest(1L); + CountDownLatch startLatch = new CountDownLatch(numberOfThreads); + CountDownLatch completionLatch = new CountDownLatch(numberOfThreads); + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); for (int i = 0; i < numberOfThreads; i++) { executorService.submit(() -> { try { - latch.countDown(); - latch.await(); + startLatch.countDown(); + startLatch.await(); - bookmarkService.createBookmark(1L, request); + bookmarkServiceFacade.createBookmark(ACCOUNT_ID, request); } catch (Exception e) { throw new RuntimeException(e); + } finally { + completionLatch.countDown(); } }); } executorService.shutdown(); - while (!executorService.isTerminated()) { - Thread.sleep(100); - } + boolean isCompleted = completionLatch.await(5, TimeUnit.SECONDS); assertAll( + () -> assertThat(isCompleted).isTrue(), () -> verify(bookmarkRepository, times(10)).existsBy(anyLong(), anyLong()), () -> verify(bookmarkRepository).save(any(Bookmark.class)) ); diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyCreateBookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyCreateBookmarkServiceTest.java new file mode 100644 index 00000000..06684bf8 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ConcurrencyCreateBookmarkServiceTest.java @@ -0,0 +1,73 @@ +package com.dnd.spaced.core.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; +import com.dnd.spaced.core.bookmark.domain.Bookmark; +import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ConcurrencyCreateBookmarkServiceTest { + + private static final Long WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; + + @Autowired + CreateBookmarkService createBookmarkService; + + @Autowired + BookmarkRepository bookmarkRepository; + + @Test + @Sql("classpath:sql/bookmark/word.sql") + void 동시에_동일한_용어에_북마크_생성_요청을_하더라도_단_하나의_북마크만_생성되어야_한다() throws InterruptedException { + int numberOfThreads = 10; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + CountDownLatch startLatch = new CountDownLatch(numberOfThreads); + CountDownLatch completionLatch = new CountDownLatch(numberOfThreads); + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); + + for (int i = 0; i < numberOfThreads; i++) { + executorService.submit(() -> { + try { + startLatch.countDown(); + startLatch.await(); + + createBookmarkService.createBookmark(ACCOUNT_ID, request); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + completionLatch.countDown(); + } + }); + } + + executorService.shutdown(); + + boolean isCompleted = completionLatch.await(5, TimeUnit.SECONDS); + + assertAll( + () -> assertThat(isCompleted).isTrue(), + () -> verify(bookmarkRepository, times(10)).existsBy(anyLong(), anyLong()), + () -> verify(bookmarkRepository).save(any(Bookmark.class)) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/CreateBookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/CreateBookmarkServiceTest.java new file mode 100644 index 00000000..3885e193 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/CreateBookmarkServiceTest.java @@ -0,0 +1,74 @@ +package com.dnd.spaced.core.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.exception.AlreadyExistsBookmarkException; +import com.dnd.spaced.core.bookmark.application.exception.WordNotFoundException; +import com.dnd.spaced.core.bookmark.domain.Bookmark; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreateBookmarkServiceTest { + + private static final Long WORD_ID = 1L; + private static final Long NOT_FOUND_WORD_ID = -999L; + private static final Long ACCOUNT_ID = 1L; + + @Autowired + CreateBookmarkService createBookmarkService; + + @Test + @Sql("classpath:sql/bookmark/word.sql") + void 북마크를_추가한다() { + // given + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); + + // when + Bookmark actual = createBookmarkService.createBookmark(ACCOUNT_ID, request); + + // then + assertAll( + () -> assertThat(actual.getId()).isPositive(), + () -> assertThat(actual.getAccountId()).isEqualTo(ACCOUNT_ID), + () -> assertThat(actual.getWordId()).isEqualTo(WORD_ID) + ); + } + + @Test + @Sql(scripts = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 이미_북마크에_추가된_용어를_북마에_추가할_수_없다() { + // given + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); + + // when & then + assertThatThrownBy(() -> createBookmarkService.createBookmark(ACCOUNT_ID, request)) + .isInstanceOf(AlreadyExistsBookmarkException.class) + .hasMessage("이미 북마크에 추가된 용어입니다."); + } + + @Test + void 지정한_용어_식별자로_용어를_찾지_못하면_북마크를_추가할_수_없다() { + // given + CreateBookmarkRequest request = new CreateBookmarkRequest(NOT_FOUND_WORD_ID); + + // when & then + assertThatThrownBy(() -> createBookmarkService.createBookmark(ACCOUNT_ID, request)) + .isInstanceOf(WordNotFoundException.class) + .hasMessage("지정한 식별자의 용어를 찾지 못했습니다."); + + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/DeleteBookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/DeleteBookmarkServiceTest.java new file mode 100644 index 00000000..d9424ae4 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/DeleteBookmarkServiceTest.java @@ -0,0 +1,47 @@ +package com.dnd.spaced.core.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.dnd.spaced.core.bookmark.application.dto.request.DeleteBookmarkRequest; +import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DeleteBookmarkServiceTest { + + private static final Long WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; + + @Autowired + DeleteBookmarkService deleteBookmarkService; + + @Autowired + BookmarkRepository bookmarkRepository; + + @Test + @Sql(scripts = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 북마크를_삭제한다() { + // given + assertThat(bookmarkRepository.existsBy(ACCOUNT_ID, WORD_ID)).isTrue(); + + DeleteBookmarkRequest request = new DeleteBookmarkRequest(WORD_ID); + + // when + deleteBookmarkService.deleteBookmark(ACCOUNT_ID, request); + + // then + assertThat(bookmarkRepository.existsBy(ACCOUNT_ID, WORD_ID)).isFalse(); + } + +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ReadBookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ReadBookmarkServiceTest.java new file mode 100644 index 00000000..bac18229 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/ReadBookmarkServiceTest.java @@ -0,0 +1,51 @@ +package com.dnd.spaced.core.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.bookmark.application.dto.request.ReadAllBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.dto.response.BookmarkCollectionResponse; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReadBookmarkServiceTest { + + private static final Long WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; + private static final Long BOOKMARK_ID = 1L; + private static final Long LAST_BOOKMARK_ID = 1L; + + @Autowired + ReadBookmarkService readBookmarkService; + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 회원이_생성한_북마크를_모두_조회한다() { + // given + ReadAllBookmarkRequest request = new ReadAllBookmarkRequest(null); + + // when + BookmarkCollectionResponse actual = readBookmarkService.readBookmarks(ACCOUNT_ID, request, PageRequest.of(0, 10)); + + // then + assertAll( + () -> assertThat(actual.bookmarks()).hasSize(1), + () -> assertThat(actual.lastBookmarkId()).isEqualTo(LAST_BOOKMARK_ID), + () -> assertThat(actual.bookmarks().get(0).bookmarkId()).isEqualTo(BOOKMARK_ID), + () -> assertThat(actual.bookmarks().get(0).accountId()).isEqualTo(ACCOUNT_ID), + () -> assertThat(actual.bookmarks().get(0).wordId()).isEqualTo(WORD_ID) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/TryLockCreateBookmarkServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/TryLockCreateBookmarkServiceTest.java new file mode 100644 index 00000000..b1bd292b --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/TryLockCreateBookmarkServiceTest.java @@ -0,0 +1,151 @@ +package com.dnd.spaced.core.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; +import com.dnd.spaced.core.bookmark.application.exception.BookmarkLockException; +import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class TryLockCreateBookmarkServiceTest { + + private static final Long WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; + + @Autowired + CreateBookmarkService createBookmarkService; + + @Autowired + RedissonClient redissonClient; + + @Autowired + BookmarkRepository bookmarkRepository; + + @Test + @Sql("classpath:sql/bookmark/word.sql") + void 락_획득에_성공하면_북마크를_추가할_수_있다() { + // given + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); + + // when & then + assertDoesNotThrow(() -> createBookmarkService.createBookmark(ACCOUNT_ID, request)); + + assertThat(bookmarkRepository.existsBy(ACCOUNT_ID, WORD_ID)).isTrue(); + } + + @Test + @Sql("classpath:sql/bookmark/word.sql") + void 락_획득에_실패하면_북마크를_추가할_수_없다() throws InterruptedException { + // given + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); + + String lockName = "bookmark:create:" + WORD_ID + ":" + ACCOUNT_ID; + RLock lock = redissonClient.getLock(lockName); + + CountDownLatch lockAcquired = new CountDownLatch(1); + CountDownLatch testCompleted = new CountDownLatch(1); + + Thread lockHolder = new Thread(() -> { + lock.lock(); + lockAcquired.countDown(); + + try { + testCompleted.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + }); + + lockHolder.start(); + lockAcquired.await(); + + // when & then + try { + assertThatThrownBy(() -> createBookmarkService.createBookmark(ACCOUNT_ID, request)) + .isInstanceOf(BookmarkLockException.class) + .hasMessage("북마크 생성 중 락 획득 실패"); + + assertThat(bookmarkRepository.existsBy(ACCOUNT_ID, WORD_ID)).isFalse(); + } finally { + testCompleted.countDown(); + lockHolder.join(); + } + } + + @Test + @Sql("classpath:sql/bookmark/word.sql") + void 락_대기_중_인터럽트_발생_예외_세부_검증() throws Exception { + // given + CreateBookmarkRequest request = new CreateBookmarkRequest(WORD_ID); + + String lockName = "bookmark:create:" + WORD_ID + ":" + ACCOUNT_ID; + RLock lock = redissonClient.getLock(lockName); + + CountDownLatch lockAcquired = new CountDownLatch(1); + CountDownLatch serviceStarted = new CountDownLatch(1); + CountDownLatch testCompleted = new CountDownLatch(1); + + AtomicReference exceptionHolder = new AtomicReference<>(); + + Thread lockHolder = new Thread(() -> { + lock.lock(); + lockAcquired.countDown(); + try { + testCompleted.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lock.unlock(); + } + }); + + Thread serviceThread = new Thread(() -> { + try { + serviceStarted.countDown(); + createBookmarkService.createBookmark(ACCOUNT_ID, request); + } catch (Exception e) { + exceptionHolder.set(e); + } + }); + + lockHolder.start(); + lockAcquired.await(); + + serviceThread.start(); + serviceStarted.await(); + + // when + serviceThread.interrupt(); + serviceThread.join(2000); + + // then + Exception exception = exceptionHolder.get(); + + assertAll( + () -> assertThat(exception).isNotNull(), + () -> assertThat(exception.getCause()).isInstanceOf(InterruptedException.class), + () -> assertThat(bookmarkRepository.existsBy(ACCOUNT_ID, WORD_ID)).isFalse() + ); + + testCompleted.countDown(); + lockHolder.join(); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkSchedulerTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkSchedulerTest.java index 30efa6fb..d6538903 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkSchedulerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/application/schedule/DeleteBookmarkSchedulerTest.java @@ -8,16 +8,14 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import com.dnd.spaced.core.bookmark.domain.repository.BookmarkRepository; -import com.dnd.spaced.core.word.application.DeletedWordIdRepository; +import com.dnd.spaced.core.word.application.repository.DeletedWordIdRepository; import java.time.Clock; -import java.time.Instant; import java.time.LocalDateTime; import java.util.List; import java.util.Set; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; -import org.mockito.internal.util.MockUtil; import ch.qos.logback.classic.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.Level; @@ -46,7 +44,7 @@ class DeleteBookmarkSchedulerTest { @Test @Sql(scripts = { - "classpath:sql/bookmark/deleted_word.sql", + "classpath:sql/bookmark/word.sql", "classpath:sql/bookmark/bookmark.sql" }) void 삭제한_용어에_등록된_북마크를_삭제한다() { diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepositoryTest.java new file mode 100644 index 00000000..9c38297d --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/infrastructure/persistence/BookmarkGatewayRepositoryTest.java @@ -0,0 +1,139 @@ +package com.dnd.spaced.core.bookmark.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.bookmark.domain.Bookmark; +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class BookmarkGatewayRepositoryTest { + + private static final long ACCOUNT_ID = 1L; + private static final long WORD_ID = 1L; + private static final long DELETED_WORD_ID = 2L; + + @Autowired + BookmarkGatewayRepository bookmarkGatewayRepository; + + @Autowired + EntityManager em; + + @Test + @Sql("classpath:sql/bookmark/word.sql") + void 북마크를_영속화_한다() { + // given + Bookmark bookmark = new Bookmark(ACCOUNT_ID, WORD_ID); + + // when + bookmarkGatewayRepository.save(bookmark); + + // then + Optional actual = bookmarkGatewayRepository.findBy(ACCOUNT_ID, WORD_ID); + + assertThat(actual).isPresent(); + } + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 삭제하지_않은_용어에_등록된_북마크를_회원_id와_용어_id로_조회한다() { + // when + Optional actual = bookmarkGatewayRepository.findBy(ACCOUNT_ID, WORD_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getAccountId()).isEqualTo(ACCOUNT_ID), + () -> assertThat(actual.get().getWordId()).isEqualTo(WORD_ID) + ); + } + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 삭제한_용어에_등록된_북마크는_회원_id와_용어_id로_조회할_수_없다() { + // when + Optional actual = bookmarkGatewayRepository.findBy(ACCOUNT_ID, DELETED_WORD_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 삭제하지_않은_용어에_등록된_북마크를_회원_id와_용어_id로_영속화_여부를_확인한다() { + // when + boolean actual = bookmarkGatewayRepository.existsBy(ACCOUNT_ID, WORD_ID); + + // then + assertThat(actual).isTrue(); + } + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 삭제한_않은_용어에_등록된_북마크를_회원_id와_용어_id로_영속화_여부를_확인한다() { + // when + boolean actual = bookmarkGatewayRepository.existsBy(ACCOUNT_ID, DELETED_WORD_ID); + + // then + assertThat(actual).isFalse(); + } + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + @Transactional + void 특정_용어_id에_등록된_모든_북마크를_삭제한다() { + // when + bookmarkGatewayRepository.deleteAllBy(Set.of(WORD_ID)); + + // then + boolean actual = bookmarkGatewayRepository.existsBy(ACCOUNT_ID, WORD_ID); + + assertThat(actual).isFalse(); + } + + @Test + @Sql(value = { + "classpath:sql/bookmark/word.sql", + "classpath:sql/bookmark/bookmark.sql" + }) + void 특정_회원이_삭제하지_않은_용어에_등록한_모든_북마크를_조회한다() { + // when + List actual = bookmarkGatewayRepository.findAllBy(ACCOUNT_ID, null, PageRequest.of(0, 10)); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).getAccountId()).isEqualTo(ACCOUNT_ID), + () -> assertThat(actual.get(0).getWordId()).isEqualTo(WORD_ID) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/bookmark/presentation/BookmarkControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/bookmark/presentation/BookmarkControllerTest.java index 39000b83..d5059527 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/bookmark/presentation/BookmarkControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/bookmark/presentation/BookmarkControllerTest.java @@ -17,7 +17,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.dnd.spaced.config.common.CommonControllerSliceTest; -import com.dnd.spaced.core.bookmark.application.BookmarkService; +import com.dnd.spaced.core.bookmark.application.BookmarkServiceFacade; import com.dnd.spaced.core.bookmark.application.dto.request.CreateBookmarkRequest; import com.dnd.spaced.core.bookmark.application.dto.request.DeleteBookmarkRequest; import com.dnd.spaced.core.bookmark.application.dto.request.ReadAllBookmarkRequest; @@ -27,7 +27,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -39,7 +38,7 @@ class BookmarkControllerTest extends CommonControllerSliceTest { @Autowired - BookmarkService bookmarkService; + BookmarkServiceFacade bookmarkServiceFacade; @Test @WithMockUser("1") @@ -55,7 +54,7 @@ class BookmarkControllerTest extends CommonControllerSliceTest { .content(objectMapper.writeValueAsString(request)) ).andExpectAll(status().isNoContent()); - verify(bookmarkService).createBookmark(anyLong(), any(CreateBookmarkRequest.class)); + verify(bookmarkServiceFacade).createBookmark(anyLong(), any(CreateBookmarkRequest.class)); 북마크_생성_요청_문서화(resultActions); } @@ -86,7 +85,7 @@ class BookmarkControllerTest extends CommonControllerSliceTest { .content(objectMapper.writeValueAsString(request)) ).andExpectAll(status().isNoContent()); - verify(bookmarkService).deleteBookmark(anyLong(), any(DeleteBookmarkRequest.class)); + verify(bookmarkServiceFacade).deleteBookmark(anyLong(), any(DeleteBookmarkRequest.class)); 북마크_삭제_요청_문서화(resultActions); } @@ -111,7 +110,7 @@ class BookmarkControllerTest extends CommonControllerSliceTest { BookmarkResponse bookmarkResponse = new BookmarkResponse(1L, 1L, 1L, LocalDateTime.now()); BookmarkCollectionResponse response = new BookmarkCollectionResponse(List.of(bookmarkResponse), 1L); - given(bookmarkService.readBookmarks(anyLong(), any(ReadAllBookmarkRequest.class), any(Pageable.class))) + given(bookmarkServiceFacade.readBookmarks(anyLong(), any(ReadAllBookmarkRequest.class), any(Pageable.class))) .willReturn(response); // when & then @@ -127,7 +126,7 @@ class BookmarkControllerTest extends CommonControllerSliceTest { jsonPath("lastBookmarkId", is(1L), Long.class) ); - verify(bookmarkService).readBookmarks(anyLong(), any(ReadAllBookmarkRequest.class), any(Pageable.class)); + verify(bookmarkServiceFacade).readBookmarks(anyLong(), any(ReadAllBookmarkRequest.class), any(Pageable.class)); 북마크_목록_조회_요청_문서화(resultActions); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/application/CommentServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/application/CommentServiceFacadeTest.java similarity index 85% rename from space-d/src/test/java/com/dnd/spaced/core/comment/application/CommentServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/comment/application/CommentServiceFacadeTest.java index 80e1d5a6..3cd0cae2 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/comment/application/CommentServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/application/CommentServiceFacadeTest.java @@ -12,8 +12,6 @@ import com.dnd.spaced.core.comment.application.exception.ForbiddenCommentException; import com.dnd.spaced.core.comment.application.exception.WordNotFoundException; import com.dnd.spaced.core.comment.domain.exception.InvalidCommentContentException; -import com.dnd.spaced.core.comment.domain.repository.CommentRepository; -import com.dnd.spaced.core.like.domain.repository.LikeRepository; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -28,16 +26,10 @@ @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class CommentServiceTest { +class CommentServiceFacadeTest { @Autowired - CommentService commentService; - - @Autowired - CommentRepository commentRepository; - - @Autowired - LikeRepository likeRepository; + CommentServiceFacade commentServiceFacade; @Test void 댓글을_작성할_용어가_없는_경우_댓글을_작성할_수_없다() { @@ -45,7 +37,7 @@ class CommentServiceTest { CreateCommentRequest request = new CreateCommentRequest("이 용어는 언제 쓰는건가요?"); // when & then - assertThatThrownBy(() -> commentService.createComment(1L, -1L, request)) + assertThatThrownBy(() -> commentServiceFacade.createComment(1L, -1L, request)) .isInstanceOf(WordNotFoundException.class) .hasMessage("댓글과 관련된 용어를 찾을 수 없습니다."); } @@ -58,7 +50,7 @@ class CommentServiceTest { CreateCommentRequest request = new CreateCommentRequest(invalidContent); // when & then - assertThatThrownBy(() -> commentService.createComment(1L, 1L, request)) + assertThatThrownBy(() -> commentServiceFacade.createComment(1L, 1L, request)) .isInstanceOf(InvalidCommentContentException.class) .hasMessage("댓글 내용은 최소 1글자 이상, 최소 100글자 이하여야 합니다"); } @@ -70,13 +62,13 @@ class CommentServiceTest { CreateCommentRequest request = new CreateCommentRequest("이 용어는 언제 쓰는건가요?"); // when - assertDoesNotThrow(() -> commentService.createComment(1L, 1L, request)); + assertDoesNotThrow(() -> commentServiceFacade.createComment(1L, 1L, request)); } @Test void 없는_댓글_식별자를_통해_댓글을_삭제할_수_없다() { // when & then - assertThatThrownBy(() -> commentService.deleteComment(1L, -999L)) + assertThatThrownBy(() -> commentServiceFacade.deleteComment(1L, -999L)) .isInstanceOf(CommentNotFoundException.class) .hasMessage("지정한 ID에 해당하는 댓글이 없습니다."); } @@ -88,7 +80,7 @@ class CommentServiceTest { }) void 댓글_작성자가_아니라면_댓글을_삭제할_수_없다() { // when & then - assertThatThrownBy(() -> commentService.deleteComment(2L, 1L)) + assertThatThrownBy(() -> commentServiceFacade.deleteComment(2L, 1L)) .isInstanceOf(ForbiddenCommentException.class) .hasMessage("댓글을 삭제할 권한이 없습니다."); } @@ -100,7 +92,7 @@ class CommentServiceTest { }) void 댓글을_삭제한다() { // when & then - assertDoesNotThrow(() -> commentService.deleteComment(1L, 1L)); + assertDoesNotThrow(() -> commentServiceFacade.deleteComment(1L, 1L)); } @Test @@ -109,7 +101,7 @@ class CommentServiceTest { UpdateCommentRequest request = new UpdateCommentRequest("처음 보는 용어인데 잘 쓰지는 않나보네요"); // when & then - assertThatThrownBy(() -> commentService.updateComment(1L, -999L, request)) + assertThatThrownBy(() -> commentServiceFacade.updateComment(1L, -999L, request)) .isInstanceOf(CommentNotFoundException.class) .hasMessage("지정한 ID에 해당하는 댓글이 없습니다."); } @@ -124,7 +116,7 @@ class CommentServiceTest { UpdateCommentRequest request = new UpdateCommentRequest("처음 보는 용어인데 잘 쓰지는 않나보네요"); // when & then - assertThatThrownBy(() -> commentService.updateComment(2L, 1L, request)) + assertThatThrownBy(() -> commentServiceFacade.updateComment(2L, 1L, request)) .isInstanceOf(ForbiddenCommentException.class) .hasMessage("댓글을 수정할 권한이 없습니다."); } @@ -140,7 +132,7 @@ class CommentServiceTest { UpdateCommentRequest request = new UpdateCommentRequest(invalidContent); // when & then - assertThatThrownBy(() -> commentService.updateComment(1L, 1L, request)) + assertThatThrownBy(() -> commentServiceFacade.updateComment(1L, 1L, request)) .isInstanceOf(InvalidCommentContentException.class) .hasMessage("댓글 내용은 최소 1글자 이상, 최소 100글자 이하여야 합니다"); } @@ -155,7 +147,7 @@ class CommentServiceTest { UpdateCommentRequest request = new UpdateCommentRequest("처음 보는 용어인데 잘 쓰지는 않나보네요"); // when & then - assertDoesNotThrow(() -> commentService.updateComment(1L, 1L, request)); + assertDoesNotThrow(() -> commentServiceFacade.updateComment(1L, 1L, request)); } @Test @@ -166,7 +158,7 @@ class CommentServiceTest { }) void 로그인_하지_않고_특정_용어의_댓글_목록을_조회한다() { // when - CommentCollectionResponse actual = commentService.readComments( + CommentCollectionResponse actual = commentServiceFacade.readComments( -1L, 1L, null, @@ -189,7 +181,7 @@ class CommentServiceTest { }) void 로그인하고_특정_용어의_댓글_목록을_조회한다() { // when - CommentCollectionResponse actual = commentService.readComments( + CommentCollectionResponse actual = commentServiceFacade.readComments( 2L, 1L, null, diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/application/CreateCommentServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/application/CreateCommentServiceTest.java new file mode 100644 index 00000000..2b427dc5 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/application/CreateCommentServiceTest.java @@ -0,0 +1,64 @@ +package com.dnd.spaced.core.comment.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.comment.application.dto.request.CreateCommentRequest; +import com.dnd.spaced.core.comment.application.exception.WordNotFoundException; +import com.dnd.spaced.core.comment.domain.exception.InvalidCommentContentException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreateCommentServiceTest { + + private static final Long ACCOUNT_ID = 1L; + private static final Long WORD_ID = 1L; + private static final Long NOT_FOUND_WORD_ID = -1L; + + @Autowired + CreateCommentService createCommentService; + + @Test + void 댓글을_작성할_용어_ID로_용어를_찾지_못하는_경우_댓글을_작성할_수_없다() { + // given + CreateCommentRequest request = new CreateCommentRequest("이 용어는 언제 쓰는건가요?"); + + // when & then + assertThatThrownBy(() -> createCommentService.createComment(ACCOUNT_ID, NOT_FOUND_WORD_ID, request)) + .isInstanceOf(WordNotFoundException.class) + .hasMessage("댓글과 관련된 용어를 찾을 수 없습니다."); + } + + @ParameterizedTest(name = "댓글 내용이 {0}일 때 댓글을 작성할 수 없다") + @NullAndEmptySource + @Sql("classpath:sql/comment/word.sql") + void 유효한_길이의_댓글_내용이_아니라면_댓글을_작성할_수_없다(String invalidContent) { + // given + CreateCommentRequest request = new CreateCommentRequest(invalidContent); + + // when & then + assertThatThrownBy(() -> createCommentService.createComment(ACCOUNT_ID, WORD_ID, request)) + .isInstanceOf(InvalidCommentContentException.class) + .hasMessage("댓글 내용은 최소 1글자 이상, 최소 100글자 이하여야 합니다"); + } + + @Test + @Sql("classpath:sql/comment/word.sql") + void 댓글을_작성한다() { + // given + CreateCommentRequest request = new CreateCommentRequest("이 용어는 언제 쓰는건가요?"); + + // when + assertDoesNotThrow(() -> createCommentService.createComment(ACCOUNT_ID, WORD_ID, request)); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/application/DeleteCommentServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/application/DeleteCommentServiceTest.java new file mode 100644 index 00000000..e5189d28 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/application/DeleteCommentServiceTest.java @@ -0,0 +1,58 @@ +package com.dnd.spaced.core.comment.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.comment.application.exception.CommentNotFoundException; +import com.dnd.spaced.core.comment.application.exception.ForbiddenCommentException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class DeleteCommentServiceTest { + + private static final Long WRITER_ID = 1L; + private static final Long READER_ID = 2L; + private static final Long COMMENT_ID = 1L; + private static final Long NOT_FOUND_COMMENT_ID = -999L; + + @Autowired + DeleteCommentService commentService; + + @Test + void 없는_댓글_ID를_통해_댓글을_삭제할_수_없다() { + // when & then + assertThatThrownBy(() -> commentService.deleteComment(WRITER_ID, NOT_FOUND_COMMENT_ID)) + .isInstanceOf(CommentNotFoundException.class) + .hasMessage("지정한 ID에 해당하는 댓글이 없습니다."); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 댓글_작성자가_아니라면_댓글을_삭제할_수_없다() { + // when & then + assertThatThrownBy(() -> commentService.deleteComment(READER_ID, COMMENT_ID)) + .isInstanceOf(ForbiddenCommentException.class) + .hasMessage("댓글을 삭제할 권한이 없습니다."); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 댓글을_삭제한다() { + // when & then + assertDoesNotThrow(() -> commentService.deleteComment(WRITER_ID, COMMENT_ID)); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/application/ReadCommentServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/application/ReadCommentServiceTest.java new file mode 100644 index 00000000..1653f390 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/application/ReadCommentServiceTest.java @@ -0,0 +1,74 @@ +package com.dnd.spaced.core.comment.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.comment.domain.dto.LikedComment; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReadCommentServiceTest { + + private static final Long GUEST_ID = -1L; + private static final Long READER_ID = 2L; + private static final Long WORD_ID = 1L; + + @Autowired + ReadCommentService readCommentService; + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql", + "classpath:sql/comment/like.sql" + }) + void 로그인_하지_않고_특정_용어의_댓글_목록을_조회한다() { + // when + List actual = readCommentService.readComments( + GUEST_ID, + WORD_ID, + null, + PageRequest.of(0, 10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).comment().getContent()).isEqualTo("이 용어는 언제 쓰는건가요?"), + () -> assertThat(actual.get(0).isLiked()).isFalse() + ); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql", + "classpath:sql/comment/like.sql" + }) + void 로그인하고_특정_용어의_댓글_목록을_조회한다() { + // when + List actual = readCommentService.readComments( + READER_ID, + WORD_ID, + null, + PageRequest.of(0, 10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).comment().getContent()).isEqualTo("이 용어는 언제 쓰는건가요?"), + () -> assertThat(actual.get(0).isLiked()).isTrue() + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/application/UpdateCommentServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/application/UpdateCommentServiceTest.java new file mode 100644 index 00000000..e9bd835c --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/application/UpdateCommentServiceTest.java @@ -0,0 +1,87 @@ +package com.dnd.spaced.core.comment.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.comment.application.dto.request.UpdateCommentRequest; +import com.dnd.spaced.core.comment.application.exception.CommentNotFoundException; +import com.dnd.spaced.core.comment.application.exception.ForbiddenCommentException; +import com.dnd.spaced.core.comment.domain.exception.InvalidCommentContentException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UpdateCommentServiceTest { + + private static final Long WRITER_ID = 1L; + private static final Long READER_ID = 2L; + private static final Long COMMENT_ID = 1L; + private static final Long NOT_FOUND_COMMENT_ID = -999L; + + @Autowired + UpdateCommentService updateCommentService; + + @Test + void 댓글_ID로_찾을_수_없는_댓글은_수정할_수_없다() { + // given + UpdateCommentRequest request = new UpdateCommentRequest("처음 보는 용어인데 잘 쓰지는 않나보네요"); + + // when & then + assertThatThrownBy(() -> updateCommentService.updateComment(WRITER_ID, NOT_FOUND_COMMENT_ID, request)) + .isInstanceOf(CommentNotFoundException.class) + .hasMessage("지정한 ID에 해당하는 댓글이 없습니다."); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 댓글_작성자가_아니라면_댓글을_수정할_수_없다() { + // given + UpdateCommentRequest request = new UpdateCommentRequest("처음 보는 용어인데 잘 쓰지는 않나보네요"); + + // when & then + assertThatThrownBy(() -> updateCommentService.updateComment(READER_ID, COMMENT_ID, request)) + .isInstanceOf(ForbiddenCommentException.class) + .hasMessage("댓글을 수정할 권한이 없습니다."); + } + + @ParameterizedTest(name = "댓글 내용이 {0}일 때 예외가 발생한다") + @NullAndEmptySource + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 비어_있는_내용으로_댓글을_수정할_수_없다(String invalidContent) { + // given + UpdateCommentRequest request = new UpdateCommentRequest(invalidContent); + + // when & then + assertThatThrownBy(() -> updateCommentService.updateComment(WRITER_ID, COMMENT_ID, request)) + .isInstanceOf(InvalidCommentContentException.class) + .hasMessage("댓글 내용은 최소 1글자 이상, 최소 100글자 이하여야 합니다"); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 댓글을_수정한다() { + // given + UpdateCommentRequest request = new UpdateCommentRequest("처음 보는 용어인데 잘 쓰지는 않나보네요"); + + // when & then + assertDoesNotThrow(() -> updateCommentService.updateComment(WRITER_ID, COMMENT_ID, request)); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/domain/CommentTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/domain/CommentTest.java index 44bace1a..fa56e82f 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/comment/domain/CommentTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/domain/CommentTest.java @@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.dnd.spaced.core.account.domain.Account; +import com.dnd.spaced.core.account.domain.enums.ProfileImageName; import com.dnd.spaced.core.account.domain.enums.RegistrationId; import com.dnd.spaced.core.account.domain.enums.Role; import com.dnd.spaced.core.comment.domain.exception.InvalidCommentContentException; @@ -50,14 +51,14 @@ class CommentTest { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); ReflectionTestUtils.setField(account, "id", 1L); Comment comment = new Comment(2L, 1L, "이 용어 언제 쓰는건가요?"); // when - boolean actual = comment.isNotWriter(account); + boolean actual = comment.isReader(account); // then assertThat(actual).isTrue(); @@ -70,14 +71,14 @@ class CommentTest { .registrationId(RegistrationId.KAKAO) .socialIdentifier("12345") .nickname("재빠른지구001") - .profileImage("earth.png") + .profileImageName(ProfileImageName.EARTH) .role(Role.ROLE_USER) .build(); ReflectionTestUtils.setField(writer, "id", 1L); Comment comment = new Comment(writer.getId(), 1L, "이 용어 언제 쓰는건가요?"); // when - boolean actual = comment.isNotWriter(writer); + boolean actual = comment.isReader(writer); // then assertThat(actual).isFalse(); @@ -101,7 +102,7 @@ class CommentTest { Comment comment = new Comment(1L, 1L, "이 용어 언제 쓰는건가요?"); // when - boolean actual = comment.isNotWriter(1L); + boolean actual = comment.isReader(1L); // then assertThat(actual).isFalse(); diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepositoryTest.java new file mode 100644 index 00000000..f4f3a107 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/infrastructure/persistence/CommentGatewayRepositoryTest.java @@ -0,0 +1,212 @@ +package com.dnd.spaced.core.comment.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.comment.domain.Comment; +import com.dnd.spaced.core.comment.domain.dto.LikedComment; +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CommentGatewayRepositoryTest { + + private static final Long WRITER_ID = 1L; + private static final Long READER_ID = 2L; + private static final Long GUEST_ID = -1L; + private static final Long WORD_ID = 1L; + private static final Long COMMENT_ID = 1L; + private static final Long DELETED_COMMENT_ID = 2L; + + @Autowired + CommentGatewayRepository commentGatewayRepository; + + @Autowired + CommentCrudRepository commentCrudRepository; + + @Autowired + EntityManager em; + + @Test + void 댓글을_영속화_한다() { + // given + Comment comment = new Comment(WRITER_ID, WORD_ID, "이 용어 언제 쓰는건가요?"); + + // when + Comment actual = commentGatewayRepository.save(comment); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 삭제하지_않은_댓글을_id로_조회한다() { + // when + Optional actual = commentGatewayRepository.findBy(COMMENT_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getId()).isEqualTo(COMMENT_ID) + ); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + void 삭제한_댓글은_id로_조회할_수_없다() { + // when + Optional actual = commentGatewayRepository.findBy(DELETED_COMMENT_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql", + "classpath:sql/comment/like.sql", + }) + void 로그인하지_않은_상태로_용어의_삭제하지_않은_모든_댓글을_조회한다() { + // when + List actual = commentGatewayRepository.findAllBy(GUEST_ID, WORD_ID, null, PageRequest.of(0, 10)); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).comment().getId()).isEqualTo(1L), + () -> assertThat(actual.get(0).isLiked()).isFalse() + ); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql", + "classpath:sql/comment/like.sql", + }) + void 로그인한_상태로_용어의_삭제하지_않은_모든_댓글을_조회한다() { + // when + List actual = commentGatewayRepository.findAllBy(READER_ID, WORD_ID, null, PageRequest.of(0, 10)); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).comment().getId()).isEqualTo(1L), + () -> assertThat(actual.get(0).isLiked()).isTrue() + ); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql", + "classpath:sql/comment/like.sql" + }) + @Transactional + void 삭제하지_않은_댓글에_좋아요_카운트를_1_증가시킨다() { + // given + Comment comment = commentGatewayRepository.findBy(COMMENT_ID).get(); + + assertThat(comment.getLikeCount()).isEqualTo(1L); + + // when + commentGatewayRepository.addLikeCount(COMMENT_ID); + + // then + em.clear(); + + Optional actual = commentGatewayRepository.findBy(COMMENT_ID); + + assertThat(actual.get().getLikeCount()).isEqualTo(2L); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + @Transactional + void 삭제한_댓글에_좋아요_카운트를_증가시킬_수_없다() { + // given + Comment deletedComment = commentCrudRepository.findById(DELETED_COMMENT_ID).get(); + + assertThat(deletedComment.getLikeCount()).isEqualTo(1L); + + // when + commentGatewayRepository.addLikeCount(DELETED_COMMENT_ID); + + // then + em.clear(); + + Optional actual = commentCrudRepository.findById(DELETED_COMMENT_ID); + + assertThat(actual.get().getLikeCount()).isEqualTo(1L); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql", + "classpath:sql/comment/like.sql", + }) + @Transactional + void 삭제하지_않은_댓글에_좋아요_카운트를_1_감소시킨다() { + // given + Comment comment = commentGatewayRepository.findBy(COMMENT_ID).get(); + + assertThat(comment.getLikeCount()).isEqualTo(1L); + + // when + commentGatewayRepository.subtractLikeCount(COMMENT_ID); + + // then + em.clear(); + + Comment actual = commentGatewayRepository.findBy(COMMENT_ID).get(); + + assertThat(actual.getLikeCount()).isZero(); + } + + @Test + @Sql(value = { + "classpath:sql/comment/word.sql", + "classpath:sql/comment/comment.sql" + }) + @Transactional + void 삭제한_댓글에_좋아요_카운트를_감소시킬_수_없다() { + // given + Comment deletedComment = commentCrudRepository.findById(DELETED_COMMENT_ID).get(); + + assertThat(deletedComment.getLikeCount()).isEqualTo(1L); + + // when + commentGatewayRepository.subtractLikeCount(DELETED_COMMENT_ID); + + // then + em.clear(); + + Optional actual = commentCrudRepository.findById(DELETED_COMMENT_ID); + + assertThat(actual.get().getLikeCount()).isEqualTo(1L); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/comment/presentation/CommentControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/comment/presentation/CommentControllerTest.java index c4cd3b8c..b500f8ea 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/comment/presentation/CommentControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/comment/presentation/CommentControllerTest.java @@ -23,7 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.dnd.spaced.config.common.CommonControllerSliceTest; -import com.dnd.spaced.core.comment.application.CommentService; +import com.dnd.spaced.core.comment.application.CommentServiceFacade; import com.dnd.spaced.core.comment.application.dto.request.CreateCommentRequest; import com.dnd.spaced.core.comment.application.dto.request.UpdateCommentRequest; import com.dnd.spaced.core.comment.application.dto.response.CommentCollectionResponse; @@ -33,7 +33,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -45,7 +44,7 @@ class CommentControllerTest extends CommonControllerSliceTest { @Autowired - CommentService commentService; + CommentServiceFacade commentServiceFacade; @Test @WithMockUser("1") @@ -63,7 +62,7 @@ class CommentControllerTest extends CommonControllerSliceTest { header().string("Location", "/words/1") ); - verify(commentService).createComment(anyLong(), anyLong(), any(CreateCommentRequest.class)); + verify(commentServiceFacade).createComment(anyLong(), anyLong(), any(CreateCommentRequest.class)); 댓글_작성_요청_문서화(resultActions); } @@ -94,7 +93,7 @@ class CommentControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(commentService).deleteComment(anyLong(), anyLong()); + verify(commentServiceFacade).deleteComment(anyLong(), anyLong()); 댓글_삭제_요청_문서화(resultActions); } @@ -127,7 +126,7 @@ class CommentControllerTest extends CommonControllerSliceTest { status().isNoContent() ); - verify(commentService).updateComment(anyLong(), anyLong(), any(UpdateCommentRequest.class)); + verify(commentServiceFacade).updateComment(anyLong(), anyLong(), any(UpdateCommentRequest.class)); 댓글_수정_요청_문서화(resultActions); } @@ -156,7 +155,7 @@ class CommentControllerTest extends CommonControllerSliceTest { CommentResponse commentResponse = new CommentResponse(commentContentResponse, commentWriterResponse, false); CommentCollectionResponse response = new CommentCollectionResponse(List.of(commentResponse), 1L); - given(commentService.readComments(anyLong(), anyLong(), eq(null), any())).willReturn(response); + given(commentServiceFacade.readComments(anyLong(), anyLong(), eq(null), any())).willReturn(response); // when & then ResultActions resultActions = mockMvc.perform( @@ -177,7 +176,7 @@ class CommentControllerTest extends CommonControllerSliceTest { jsonPath("lastCommentId", is(1L), Long.class) ); - verify(commentService).readComments(any(), anyLong(), any(), any(Pageable.class)); + verify(commentServiceFacade).readComments(any(), anyLong(), any(), any(Pageable.class)); 댓글_전체_조회_문서화(resultActions); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/CreateQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/CreateQuizServiceTest.java new file mode 100644 index 00000000..45feede7 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/CreateQuizServiceTest.java @@ -0,0 +1,62 @@ +package com.dnd.spaced.core.quiz.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.dnd.spaced.core.quiz.application.dto.request.CreateQuizRequest; +import com.dnd.spaced.core.quiz.application.exception.InvalidQuizWordCountException; +import com.dnd.spaced.core.quiz.application.exception.WordMetadataNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class CreateQuizServiceTest { + + private static final Long QUIZ_CREATOR_ID = 1L; + + @Autowired + CreateQuizService createQuizService; + + @Test + void 용어_메타데이터가_정상적으로_설정되지_않다면_퀴즈를_생성할_수_없다() { + // given + CreateQuizRequest request = new CreateQuizRequest("전체 실무"); + + // when & then + assertThatThrownBy(() -> createQuizService.createQuiz(QUIZ_CREATOR_ID, request)) + .isInstanceOf(WordMetadataNotFoundException.class) + .hasMessage("용어 메타데이터가 정상적으로 설정되지 않았습니다."); + } + + @Test + @Sql("classpath:sql/quiz/word_metadata.sql") + void 등록된_용어_수가_퀴즈_생성_시_필요한_용어_수보다_적으면_퀴즈를_생성할_수_없다() { + // given + CreateQuizRequest request = new CreateQuizRequest("전체 실무"); + + // when & then + assertThatThrownBy(() -> createQuizService.createQuiz(QUIZ_CREATOR_ID, request)) + .isInstanceOf(InvalidQuizWordCountException.class) + .hasMessage("퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); + } + + @Test + @Sql(scripts = {"classpath:sql/quiz/word_metadata.sql", "classpath:sql/quiz/word.sql"}) + void 퀴즈를_생성한다() { + // given + CreateQuizRequest request = new CreateQuizRequest("전체 실무"); + + // when + Long actual = createQuizService.createQuiz(QUIZ_CREATOR_ID, request); + + // then + assertThat(actual).isPositive(); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/GradeQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/GradeQuizServiceTest.java new file mode 100644 index 00000000..de8d3f80 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/GradeQuizServiceTest.java @@ -0,0 +1,92 @@ +package com.dnd.spaced.core.quiz.application; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest.SubmitAnswerRequest; +import com.dnd.spaced.core.quiz.application.exception.AlreadyGradeQuizException; +import com.dnd.spaced.core.quiz.application.exception.QuizNotFoundException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class GradeQuizServiceTest { + + private static final Long QUIZ_CREATOR_ID = 1L; + private static final Long UNSOLVED_QUIZ_ID = 1L; + private static final Long SOLVED_QUIZ_ID = 2L; + private static final Long NOT_FOUND_QUIZ_ID = -999L; + + @Autowired + GradeQuizService gradeQuizService; + + @Test + void 유효하지_않는_퀴즈_ID로_퀴즈_답을_제출할_수_없다() { + // given + SubmitAnswerRequest[] submitAnswers = { + new SubmitAnswerRequest(1L, "Authorization"), + new SubmitAnswerRequest(2L, "Domain"), + new SubmitAnswerRequest(3L, "Controller"), + new SubmitAnswerRequest(2L, "Web"), + new SubmitAnswerRequest(1L, "HTTP") + }; + GradeQuizRequest request = new GradeQuizRequest(submitAnswers); + + // when & then + assertThatThrownBy(() -> gradeQuizService.gradeQuiz(QUIZ_CREATOR_ID, NOT_FOUND_QUIZ_ID, request)) + .isInstanceOf(QuizNotFoundException.class) + .hasMessage("지정한 id의 퀴즈를 찾지 못했습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/quiz.sql" + }) + void 퀴즈_정답을_제출한다() { + // given + SubmitAnswerRequest[] submitAnswers = { + new SubmitAnswerRequest(1L, "Authorization"), + new SubmitAnswerRequest(2L, "Domain"), + new SubmitAnswerRequest(3L, "Controller"), + new SubmitAnswerRequest(2L, "Web"), + new SubmitAnswerRequest(1L, "HTTP") + }; + GradeQuizRequest request = new GradeQuizRequest(submitAnswers); + + // when & then + assertDoesNotThrow(() -> gradeQuizService.gradeQuiz(QUIZ_CREATOR_ID, UNSOLVED_QUIZ_ID, request)); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/solved_quiz.sql" + }) + void 이미_푼_퀴즈인_경우_정답을_제출할_수_없다() { + // given + SubmitAnswerRequest[] submitAnswers = { + new SubmitAnswerRequest(1L, "Authorization"), + new SubmitAnswerRequest(2L, "Domain"), + new SubmitAnswerRequest(3L, "Controller"), + new SubmitAnswerRequest(2L, "Web"), + new SubmitAnswerRequest(1L, "HTTP") + }; + GradeQuizRequest request = new GradeQuizRequest(submitAnswers); + + // when & then + assertThatThrownBy(() -> gradeQuizService.gradeQuiz(QUIZ_CREATOR_ID, SOLVED_QUIZ_ID, request)) + .isInstanceOf(AlreadyGradeQuizException.class) + .hasMessage("이미 풀었던 퀴즈입니다."); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/GradeTodayQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/GradeTodayQuizServiceTest.java new file mode 100644 index 00000000..1e06a2a8 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/GradeTodayQuizServiceTest.java @@ -0,0 +1,84 @@ +package com.dnd.spaced.core.quiz.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; +import com.dnd.spaced.core.quiz.application.exception.AlreadyGradeTodayQuizException; +import com.dnd.spaced.core.quiz.application.exception.TodayQuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.repository.TodayQuizGradedAnswerRepository; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class GradeTodayQuizServiceTest { + + private static final Long SELECTED_WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; + private static final Long NOT_FOUND_TODAY_QUIZ_ID = -999L; + private static final Long TODAY_QUIZ_ID = 1L; + + @Autowired + TodayQuizServiceFacade todayQuizServiceFacade; + + @Autowired + TodayQuizGradedAnswerRepository todayQuizGradedAnswerRepository; + + @Test + void 지정한_오늘의_퀴즈_id가_없다면_퀴즈_정답을_제출할_수_없다() { + // when & then + GradeTodayQuizRequest request = new GradeTodayQuizRequest(SELECTED_WORD_ID, "Authorization"); + + assertThatThrownBy(() -> todayQuizServiceFacade.gradeTodayQuiz(ACCOUNT_ID, NOT_FOUND_TODAY_QUIZ_ID, request)) + .isInstanceOf(TodayQuizNotFoundException.class) + .hasMessage("지정한 id의 오늘의 퀴즈를 찾지 못했습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/today_quiz.sql", + "classpath:sql/quiz/today_quiz_graded_answer.sql" + }) + void 이미_푼_오늘의_퀴즈인_경우_정답을_제출할_수_없다() { + // given + GradeTodayQuizRequest request = new GradeTodayQuizRequest(SELECTED_WORD_ID, "Authorization"); + + // when & then + assertThatThrownBy(() -> todayQuizServiceFacade.gradeTodayQuiz(ACCOUNT_ID, TODAY_QUIZ_ID, request)) + .isInstanceOf(AlreadyGradeTodayQuizException.class) + .hasMessage("이미 오늘의 퀴즈를 풀었습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/today_quiz.sql" + }) + void 오늘의_퀴즈_정답을_제출한다() { + // when + GradeTodayQuizRequest request = new GradeTodayQuizRequest(SELECTED_WORD_ID, "Authorization"); + + todayQuizServiceFacade.gradeTodayQuiz(ACCOUNT_ID, TODAY_QUIZ_ID, request); + + // then + TodayQuizGradedAnswer actual = todayQuizGradedAnswerRepository.findBy(ACCOUNT_ID, TODAY_QUIZ_ID) + .get(); + + assertAll( + () -> assertThat(actual.getTodayQuiz().getId()).isEqualTo(TODAY_QUIZ_ID), + () -> assertThat(actual.getAccountId()).isEqualTo(ACCOUNT_ID) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/QuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/QuizServiceFacadeTest.java similarity index 81% rename from space-d/src/test/java/com/dnd/spaced/core/quiz/application/QuizServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/quiz/application/QuizServiceFacadeTest.java index ba2f9274..0abfe1e7 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/QuizServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/QuizServiceFacadeTest.java @@ -34,13 +34,19 @@ @RecordApplicationEvents @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class QuizServiceTest { +class QuizServiceFacadeTest { + + private static final Long QUIZ_CREATOR_ID = 1L; + private static final Long UNSOLVED_QUIZ_ID = 1L; + private static final Long SOLVED_QUIZ_ID = 2L; + private static final Long NOT_FOUND_QUIZ_ID = -999L; + private static final Long NON_QUIZ_CREATOR_ID = 5L; @Autowired ApplicationEvents events; @Autowired - QuizService quizService; + QuizServiceFacade quizServiceFacade; @Test void 용어_메타데이터가_정상적으로_설정되지_않다면_퀴즈를_생성할_수_없다() { @@ -48,7 +54,7 @@ class QuizServiceTest { CreateQuizRequest request = new CreateQuizRequest("전체 실무"); // when & then - assertThatThrownBy(() -> quizService.createQuiz(1L, request)) + assertThatThrownBy(() -> quizServiceFacade.createQuiz(QUIZ_CREATOR_ID, request)) .isInstanceOf(WordMetadataNotFoundException.class) .hasMessage("용어 메타데이터가 정상적으로 설정되지 않았습니다."); } @@ -60,7 +66,7 @@ class QuizServiceTest { CreateQuizRequest request = new CreateQuizRequest("전체 실무"); // when & then - assertThatThrownBy(() -> quizService.createQuiz(1L, request)) + assertThatThrownBy(() -> quizServiceFacade.createQuiz(QUIZ_CREATOR_ID, request)) .isInstanceOf(InvalidQuizWordCountException.class) .hasMessage("퀴즈를 진행할 수 있는 용어 개수가 부족합니다."); } @@ -73,12 +79,12 @@ class QuizServiceTest { }) void 퀴즈를_조회한다() { // when - QuizResponse actual = quizService.readQuiz(1L, 1L); + QuizResponse actual = quizServiceFacade.readQuiz(QUIZ_CREATOR_ID, UNSOLVED_QUIZ_ID); // then assertAll( - () -> assertThat(actual.id()).isEqualTo(1L), - () -> assertThat(actual.accountId()).isEqualTo(1L), + () -> assertThat(actual.id()).isEqualTo(UNSOLVED_QUIZ_ID), + () -> assertThat(actual.accountId()).isEqualTo(QUIZ_CREATOR_ID), () -> assertThat(actual.quizQuestions()).hasSize(5), () -> assertThat(actual.quizQuestions().get(0).quizOptions()).hasSize(4), () -> assertThat(actual.quizQuestions().get(1).quizOptions()).hasSize(4), @@ -89,13 +95,16 @@ class QuizServiceTest { } @Test - @Sql(scripts = {"classpath:sql/quiz/word_metadata.sql", "classpath:sql/quiz/word.sql"}) + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql" + }) void 퀴즈를_생성한다() { // given CreateQuizRequest request = new CreateQuizRequest("전체 실무"); // when - Long actual = quizService.createQuiz(1L, request); + Long actual = quizServiceFacade.createQuiz(QUIZ_CREATOR_ID, request); // then assertAll( @@ -107,22 +116,25 @@ class QuizServiceTest { @Test void 유효하지_않는_퀴즈_id로_퀴즈를_조회할_수_없다() { // when & then - assertThatThrownBy(() -> quizService.readQuiz(1L, -999L)) + assertThatThrownBy(() -> quizServiceFacade.readQuiz(QUIZ_CREATOR_ID, NOT_FOUND_QUIZ_ID)) .isInstanceOf(QuizNotFoundException.class) .hasMessage("지정한 id의 퀴즈를 찾지 못했습니다."); } @Test - @Sql(scripts = {"classpath:sql/quiz/word_metadata.sql", "classpath:sql/quiz/quiz.sql"}) - void 회원이_생성한_퀴즈가_아니라면_존재하는_퀴즈_id더라도_퀴즈_정보를_조회할_수_없다() { + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/quiz.sql" + }) + void 회원이_생성한_퀴즈가_아니라면_존재하는_퀴즈_ID더라도_퀴즈_정보를_조회할_수_없다() { // when & then - assertThatThrownBy(() -> quizService.readQuiz(5L, 1L)) + assertThatThrownBy(() -> quizServiceFacade.readQuiz(NON_QUIZ_CREATOR_ID, UNSOLVED_QUIZ_ID)) .isInstanceOf(QuizNotFoundException.class) .hasMessage("지정한 id의 퀴즈를 찾지 못했습니다."); } @Test - void 유효하지_않는_퀴즈_id로_퀴즈_답을_제출할_수_없다() { + void 유효하지_않는_퀴즈_ID로_퀴즈_답을_제출할_수_없다() { // given SubmitAnswerRequest[] submitAnswers = { new SubmitAnswerRequest(1L, "Authorization"), @@ -134,7 +146,7 @@ class QuizServiceTest { GradeQuizRequest request = new GradeQuizRequest(submitAnswers); // when & then - assertThatThrownBy(() -> quizService.grade(1L, -999L, request)) + assertThatThrownBy(() -> quizServiceFacade.grade(QUIZ_CREATOR_ID, NOT_FOUND_QUIZ_ID, request)) .isInstanceOf(QuizNotFoundException.class) .hasMessage("지정한 id의 퀴즈를 찾지 못했습니다."); } @@ -157,7 +169,7 @@ class QuizServiceTest { GradeQuizRequest request = new GradeQuizRequest(submitAnswers); // when & then - assertDoesNotThrow(() -> quizService.grade(1L, 1L, request)); + assertDoesNotThrow(() -> quizServiceFacade.grade(QUIZ_CREATOR_ID, UNSOLVED_QUIZ_ID, request)); } @Test @@ -178,7 +190,7 @@ class QuizServiceTest { GradeQuizRequest request = new GradeQuizRequest(submitAnswers); // when & then - assertThatThrownBy(() -> quizService.grade(1L, 1L, request)) + assertThatThrownBy(() -> quizServiceFacade.grade(QUIZ_CREATOR_ID, SOLVED_QUIZ_ID, request)) .isInstanceOf(AlreadyGradeQuizException.class) .hasMessage("이미 풀었던 퀴즈입니다."); } @@ -195,7 +207,7 @@ class QuizServiceTest { ReadQuizGradedAnswerSearchRequest request = new ReadQuizGradedAnswerSearchRequest(null); // when - QuizGradedAnswerCollectionResponse actual = quizService.readGradedAnswers( + QuizGradedAnswerCollectionResponse actual = quizServiceFacade.readGradedAnswers( 1L, request, PageRequest.of(0, 10) ); @@ -214,12 +226,12 @@ class QuizServiceTest { @Sql(scripts = { "classpath:sql/quiz/word_metadata.sql", "classpath:sql/quiz/word.sql", - "classpath:sql/quiz/quiz.sql", + "classpath:sql/quiz/solved_quiz.sql", "classpath:sql/quiz/quiz_graded_answer.sql" }) void 특정_퀴즈의_제출했던_답을_조회한다() { // when - QuizGradedAnswerCollectionResponse actual = quizService.readGradedAnswers(1L, 1L); + QuizGradedAnswerCollectionResponse actual = quizServiceFacade.readGradedAnswers(QUIZ_CREATOR_ID, SOLVED_QUIZ_ID); // then assertAll( @@ -243,12 +255,12 @@ class QuizServiceTest { ReadAllQuizRequest request = new ReadAllQuizRequest(null); // when - QuizCollectionResponse actual = quizService.readQuizzes(1L, request, Pageable.ofSize(10)); + QuizCollectionResponse actual = quizServiceFacade.readQuizzes(QUIZ_CREATOR_ID, request, Pageable.ofSize(10)); // then assertAll( () -> assertThat(actual.quizzes()).hasSize(1), - () -> assertThat(actual.lastQuizId()).isEqualTo(1L) + () -> assertThat(actual.lastQuizId()).isEqualTo(UNSOLVED_QUIZ_ID) ); } } diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/ReadQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/ReadQuizServiceTest.java new file mode 100644 index 00000000..eff0a5b0 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/ReadQuizServiceTest.java @@ -0,0 +1,149 @@ +package com.dnd.spaced.core.quiz.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.quiz.application.dto.request.ReadAllQuizRequest; +import com.dnd.spaced.core.quiz.application.dto.request.ReadQuizGradedAnswerSearchRequest; +import com.dnd.spaced.core.quiz.application.exception.QuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.QuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.dto.QuizDto; +import com.dnd.spaced.core.quiz.domain.dto.SimpleQuizDto; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReadQuizServiceTest { + + private static final Long QUIZ_CREATOR_ID = 1L; + private static final Long UNSOLVED_QUIZ_ID = 1L; + private static final Long SOLVED_QUIZ_ID = 2L; + private static final Long NOT_FOUND_QUIZ_ID = -999L; + private static final Long NON_QUIZ_CREATOR_ID = 5L; + + @Autowired + ReadQuizService readQuizService; + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/quiz.sql" + }) + void 퀴즈를_조회한다() { + // when + QuizDto actual = readQuizService.readQuiz(QUIZ_CREATOR_ID, UNSOLVED_QUIZ_ID); + + // then + assertAll( + () -> assertThat(actual.id()).isEqualTo(UNSOLVED_QUIZ_ID), + () -> assertThat(actual.accountId()).isEqualTo(QUIZ_CREATOR_ID), + () -> assertThat(actual.quizQuestions()).hasSize(5), + () -> assertThat(actual.quizQuestions().get(0).quizOptions()).hasSize(4), + () -> assertThat(actual.quizQuestions().get(1).quizOptions()).hasSize(4), + () -> assertThat(actual.quizQuestions().get(2).quizOptions()).hasSize(4), + () -> assertThat(actual.quizQuestions().get(3).quizOptions()).hasSize(4), + () -> assertThat(actual.quizQuestions().get(4).quizOptions()).hasSize(4) + ); + } + + @Test + void 유효하지_않는_퀴즈_ID로_퀴즈를_조회할_수_없다() { + // when & then + assertThatThrownBy(() -> readQuizService.readQuiz(QUIZ_CREATOR_ID, NOT_FOUND_QUIZ_ID)) + .isInstanceOf(QuizNotFoundException.class) + .hasMessage("지정한 id의 퀴즈를 찾지 못했습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/quiz.sql" + }) + void 회원이_생성한_퀴즈가_아니라면_존재하는_퀴즈_ID더라도_퀴즈_정보를_조회할_수_없다() { + // when & then + assertThatThrownBy(() -> readQuizService.readQuiz(NON_QUIZ_CREATOR_ID, UNSOLVED_QUIZ_ID)) + .isInstanceOf(QuizNotFoundException.class) + .hasMessage("지정한 id의 퀴즈를 찾지 못했습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/quiz.sql", + "classpath:sql/quiz/quiz_graded_answer.sql" + }) + void 모든_퀴즈의_제출했던_답을_조회한다() { + // given + ReadQuizGradedAnswerSearchRequest request = new ReadQuizGradedAnswerSearchRequest(null); + + // when + List actual = readQuizService.readGradedAnswers( + 1L, request, PageRequest.of(0, 10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(5), + () -> assertThat(actual.get(0).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(1).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(2).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(3).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(4).getSelectedContent()).isNotBlank() + ); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/quiz.sql", + "classpath:sql/quiz/quiz_graded_answer.sql" + }) + void 특정_퀴즈의_제출했던_답을_조회한다() { + // when + List actual = readQuizService.readGradedAnswers(QUIZ_CREATOR_ID, SOLVED_QUIZ_ID); + + // then + assertAll( + () -> assertThat(actual).hasSize(5), + () -> assertThat(actual.get(0).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(1).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(2).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(3).getSelectedContent()).isNotBlank(), + () -> assertThat(actual.get(4).getSelectedContent()).isNotBlank() + ); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/quiz.sql" + }) + void 회원이_생성한_퀴즈_목록을_조회한다() { + // given + ReadAllQuizRequest request = new ReadAllQuizRequest(null); + + // when + List actual = readQuizService.readQuizzes(QUIZ_CREATOR_ID, request, Pageable.ofSize(10)); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).id()).isEqualTo(UNSOLVED_QUIZ_ID) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/ReadTodayQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/ReadTodayQuizServiceTest.java new file mode 100644 index 00000000..c30115eb --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/ReadTodayQuizServiceTest.java @@ -0,0 +1,148 @@ +package com.dnd.spaced.core.quiz.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.quiz.application.dto.request.ReadTodayQuizGradedAnswerSearchRequest; +import com.dnd.spaced.core.quiz.application.dto.response.ReadTodayQuizDto; +import com.dnd.spaced.core.quiz.application.exception.TodayQuizNotFoundException; +import com.dnd.spaced.core.quiz.domain.TodayQuizGradedAnswer; +import com.dnd.spaced.core.quiz.domain.dto.SimpleTodayQuizDto; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cache.CacheManager; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReadTodayQuizServiceTest { + + private static final Long ACCOUNT_ID = 1L; + private static final Long NOT_FOUND_TODAY_QUIZ_ID = -999L; + private static final Long TODAY_QUIZ_ID = 1L; + + @Autowired + ReadTodayQuizService todayQuizService; + + @Autowired + CacheManager cacheManager; + + @BeforeEach + void setUp() { + cacheManager.getCacheNames() + .forEach(name -> cacheManager.getCache(name).clear()); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/today_quiz.sql" + }) + void 최근에_생성한_오늘의_퀴즈를_조회한다() { + // when + SimpleTodayQuizDto actual = todayQuizService.readLatestTodayQuiz(); + + // then + assertAll( + () -> assertThat(actual.id()).isPositive(), + () -> assertThat(actual.todayQuizAnswerOption()).isNotNull() + ); + } + + @Test + void 오늘의_퀴즈가_생성된_적이_없다면_최근에_생성한_오늘의_퀴즈를_조회할_수_없다() { + // when & then + assertThatThrownBy(() -> todayQuizService.readLatestTodayQuiz()) + .isInstanceOf(TodayQuizNotFoundException.class) + .hasMessage("오늘의 퀴즈가 생성되지 않았습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/today_quiz.sql", + "classpath:sql/quiz/today_quiz_graded_answer.sql" + }) + void 사용자가_제출한_모든_오늘의_퀴즈_채점_결과를_조회한다() { + // given + ReadTodayQuizGradedAnswerSearchRequest request = new ReadTodayQuizGradedAnswerSearchRequest( + null + ); + + // when + List actual = todayQuizService.readTodayQuizGradedAnswers( + ACCOUNT_ID, request, PageRequest.of(0, 10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).getTodayQuiz().getId()).isEqualTo(TODAY_QUIZ_ID) + ); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/today_quiz.sql", + "classpath:sql/quiz/today_quiz_graded_answer.sql" + }) + void 사용자가_제출한_오늘의_퀴즈_채점_결과를_조회한다() { + // when + TodayQuizGradedAnswer actual = todayQuizService.readTargetTodayQuizGradedAnswers( + ACCOUNT_ID, + TODAY_QUIZ_ID + ); + + // then + assertAll( + () -> assertThat(actual.getTodayQuiz().getId()).isEqualTo(TODAY_QUIZ_ID), + () -> assertThat(actual.getAccountId()).isEqualTo(ACCOUNT_ID) + ); + } + + @Test + void 지정한_오늘의_퀴즈_id가_없다면_사용자가_제출한_오늘의_퀴즈_채점_결과를_조회할_수_없다() { + // when & then + assertThatThrownBy(() -> todayQuizService.readTargetTodayQuizGradedAnswers(ACCOUNT_ID, TODAY_QUIZ_ID)) + .isInstanceOf(TodayQuizNotFoundException.class) + .hasMessage("지정한 오늘의 퀴즈 답안지를 찾지 못했습니다."); + } + + @Test + @Sql(scripts = { + "classpath:sql/quiz/word_metadata.sql", + "classpath:sql/quiz/word.sql", + "classpath:sql/quiz/today_quiz.sql" + }) + void 지정한_id의_오늘의_퀴즈를_조회한다() { + // when + ReadTodayQuizDto actual = todayQuizService.readTodayQuiz(ACCOUNT_ID, TODAY_QUIZ_ID); + + // then + assertAll( + () -> assertThat(actual.todayQuiz().getId()).isEqualTo(TODAY_QUIZ_ID), + () -> assertThat(actual.todayQuiz().getTodayQuizQuestion()).isNotNull() + ); + } + + @Test + void 없는_ID의_오늘의_퀴즈를_조회할_수_없다() { + // when & then + assertThatThrownBy(() -> todayQuizService.readTodayQuiz(ACCOUNT_ID, NOT_FOUND_TODAY_QUIZ_ID)) + .isInstanceOf(TodayQuizNotFoundException.class) + .hasMessage("지정한 id의 오늘의 퀴즈를 찾지 못했습니다."); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceFacadeTest.java similarity index 78% rename from space-d/src/test/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceFacadeTest.java index bf3c6f4e..efdba79d 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/TodayQuizServiceFacadeTest.java @@ -32,13 +32,18 @@ @RecordApplicationEvents @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class TodayQuizServiceTest { +class TodayQuizServiceFacadeTest { + + private static final Long SELECTED_WORD_ID = 1L; + private static final Long ACCOUNT_ID = 1L; + private static final Long NOT_FOUND_TODAY_QUIZ_ID = -999L; + private static final Long TODAY_QUIZ_ID = 1L; @Autowired ApplicationEvents events; @Autowired - TodayQuizService todayQuizService; + TodayQuizServiceFacade todayQuizServiceFacade; @Autowired TodayQuizGradedAnswerRepository todayQuizGradedAnswerRepository; @@ -60,7 +65,7 @@ void setUp() { }) void 최근에_생성한_오늘의_퀴즈를_조회한다() { // when - SimpleTodayQuizResponse actual = todayQuizService.readLatestTodayQuiz(); + SimpleTodayQuizResponse actual = todayQuizServiceFacade.readLatestTodayQuiz(); // then assertAll( @@ -72,17 +77,17 @@ void setUp() { @Test void 오늘의_퀴즈가_생성된_적이_없다면_최근에_생성한_오늘의_퀴즈를_조회할_수_없다() { // when & then - assertThatThrownBy(() -> todayQuizService.readLatestTodayQuiz()) + assertThatThrownBy(() -> todayQuizServiceFacade.readLatestTodayQuiz()) .isInstanceOf(TodayQuizNotFoundException.class) .hasMessage("오늘의 퀴즈가 생성되지 않았습니다."); } @Test - void 지정한_오늘의_퀴즈_id가_없다면_퀴즈_정답을_제출할_수_없다() { + void 지정한_오늘의_퀴즈_ID가_없다면_퀴즈_정답을_제출할_수_없다() { // when & then - GradeTodayQuizRequest request = new GradeTodayQuizRequest(1L, "Authorization"); + GradeTodayQuizRequest request = new GradeTodayQuizRequest(SELECTED_WORD_ID, "Authorization"); - assertThatThrownBy(() -> todayQuizService.grade(1L, -999L, request)) + assertThatThrownBy(() -> todayQuizServiceFacade.gradeTodayQuiz(ACCOUNT_ID, NOT_FOUND_TODAY_QUIZ_ID, request)) .isInstanceOf(TodayQuizNotFoundException.class) .hasMessage("지정한 id의 오늘의 퀴즈를 찾지 못했습니다."); } @@ -95,17 +100,17 @@ void setUp() { }) void 오늘의_퀴즈_정답을_제출한다() { // when - GradeTodayQuizRequest request = new GradeTodayQuizRequest(1L, "Authorization"); + GradeTodayQuizRequest request = new GradeTodayQuizRequest(SELECTED_WORD_ID, "Authorization"); - todayQuizService.grade(1L, 1L, request); + todayQuizServiceFacade.gradeTodayQuiz(ACCOUNT_ID, TODAY_QUIZ_ID, request); // then - TodayQuizGradedAnswer actual = todayQuizGradedAnswerRepository.findBy(1L, 1L) + TodayQuizGradedAnswer actual = todayQuizGradedAnswerRepository.findBy(ACCOUNT_ID, TODAY_QUIZ_ID) .get(); assertAll( - () -> assertThat(actual.getTodayQuiz().getId()).isEqualTo(1L), - () -> assertThat(actual.getAccountId()).isEqualTo(1L), + () -> assertThat(actual.getTodayQuiz().getId()).isEqualTo(TODAY_QUIZ_ID), + () -> assertThat(actual.getAccountId()).isEqualTo(ACCOUNT_ID), () -> assertThat(events.stream(GradedTodayQuizEvent.class).count()).isOne() ); } @@ -117,21 +122,21 @@ void setUp() { "classpath:sql/quiz/today_quiz.sql", "classpath:sql/quiz/today_quiz_graded_answer.sql" }) - void 사용자가_제출한_모든_오늘의_퀴즈_채점_결과를_반환한다() { + void 사용자가_제출한_모든_오늘의_퀴즈_채점_결과를_조회한다() { // given ReadTodayQuizGradedAnswerSearchRequest request = new ReadTodayQuizGradedAnswerSearchRequest( null ); // when - TodayQuizGradedAnswerCollectionResponse actual = todayQuizService.readTodayQuizGradedAnswers( - 1L, request, PageRequest.of(0, 10) + TodayQuizGradedAnswerCollectionResponse actual = todayQuizServiceFacade.readTodayQuizGradedAnswers( + ACCOUNT_ID, request, PageRequest.of(0, 10) ); // then assertAll( () -> assertThat(actual.answers()).hasSize(1), - () -> assertThat(actual.answers().get(0).todayQuizId()).isEqualTo(1L) + () -> assertThat(actual.answers().get(0).todayQuizId()).isEqualTo(TODAY_QUIZ_ID) ); } @@ -144,10 +149,10 @@ void setUp() { }) void 이미_푼_오늘의_퀴즈인_경우_정답을_제출할_수_없다() { // given - GradeTodayQuizRequest request = new GradeTodayQuizRequest(1L, "Authorization"); + GradeTodayQuizRequest request = new GradeTodayQuizRequest(SELECTED_WORD_ID, "Authorization"); // when & then - assertThatThrownBy(() -> todayQuizService.grade(1L, 1L, request)) + assertThatThrownBy(() -> todayQuizServiceFacade.gradeTodayQuiz(ACCOUNT_ID, TODAY_QUIZ_ID, request)) .isInstanceOf(AlreadyGradeTodayQuizException.class) .hasMessage("이미 오늘의 퀴즈를 풀었습니다."); } @@ -161,22 +166,22 @@ void setUp() { }) void 사용자가_제출한_오늘의_퀴즈_채점_결과를_조회한다() { // when - TodayQuizGradedAnswerResponse actual = todayQuizService.readTargetTodayQuizGradedAnswers( - 1L, - 1L + TodayQuizGradedAnswerResponse actual = todayQuizServiceFacade.readTargetTodayQuizGradedAnswers( + ACCOUNT_ID, + TODAY_QUIZ_ID ); // then assertAll( - () -> assertThat(actual.todayQuizId()).isEqualTo(1L), - () -> assertThat(actual.accountId()).isEqualTo(1L) + () -> assertThat(actual.todayQuizId()).isEqualTo(TODAY_QUIZ_ID), + () -> assertThat(actual.accountId()).isEqualTo(ACCOUNT_ID) ); } @Test void 지정한_오늘의_퀴즈_id가_없다면_사용자가_제출한_오늘의_퀴즈_채점_결과를_조회할_수_없다() { // when & then - assertThatThrownBy(() -> todayQuizService.readTargetTodayQuizGradedAnswers(1L, 1L)) + assertThatThrownBy(() -> todayQuizServiceFacade.readTargetTodayQuizGradedAnswers(ACCOUNT_ID, TODAY_QUIZ_ID)) .isInstanceOf(TodayQuizNotFoundException.class) .hasMessage("지정한 오늘의 퀴즈 답안지를 찾지 못했습니다."); } @@ -189,19 +194,19 @@ void setUp() { }) void 지정한_id의_오늘의_퀴즈를_조회한다() { // when - TodayQuizResponse actual = todayQuizService.readTodayQuiz(1L, 1L); + TodayQuizResponse actual = todayQuizServiceFacade.readTodayQuiz(ACCOUNT_ID, TODAY_QUIZ_ID); // then assertAll( - () -> assertThat(actual.id()).isEqualTo(1L), + () -> assertThat(actual.id()).isEqualTo(TODAY_QUIZ_ID), () -> assertThat(actual.todayQuizQuestion()).isNotNull() ); } @Test - void 없는_id의_오늘의_퀴즈를_조회할_수_없다() { + void 없는_ID의_오늘의_퀴즈를_조회할_수_없다() { // when & then - assertThatThrownBy(() -> todayQuizService.readTodayQuiz(1L, -999L)) + assertThatThrownBy(() -> todayQuizServiceFacade.readTodayQuiz(ACCOUNT_ID, NOT_FOUND_TODAY_QUIZ_ID)) .isInstanceOf(TodayQuizNotFoundException.class) .hasMessage("지정한 id의 오늘의 퀴즈를 찾지 못했습니다."); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/enums/QuizWordCountValidatorTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/application/enums/QuizWordCountValidatorTest.java deleted file mode 100644 index ac647d77..00000000 --- a/space-d/src/test/java/com/dnd/spaced/core/quiz/application/enums/QuizWordCountValidatorTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.dnd.spaced.core.quiz.application.enums; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; -import com.dnd.spaced.core.quiz.application.enums.exception.QuizCategoryNotFoundException; -import com.dnd.spaced.core.word.domain.WordMetadata; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class QuizWordCountValidatorTest { - - @Test - void 디자인_퀴즈를_생성할_수_있는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isValidate(QuizCategory.DESIGN, wordMetadata, 5); - - // then - assertThat(actual).isFalse(); - } - - @Test - void 디자인_퀴즈를_생성할_수_없는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isInvalidate(QuizCategory.DESIGN, wordMetadata, 5); - - // then - assertThat(actual).isTrue(); - } - - @Test - void 비즈니스_퀴즈를_생성할_수_있는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isValidate(QuizCategory.BUSINESS, wordMetadata, 5); - - // then - assertThat(actual).isFalse(); - } - - @Test - void 비즈니스_퀴즈를_생성할_수_없는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isInvalidate(QuizCategory.BUSINESS, wordMetadata, 5); - - // then - assertThat(actual).isTrue(); - } - - @Test - void 개발_퀴즈를_생성할_수_있는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isValidate(QuizCategory.DEVELOP, wordMetadata, 5); - - // then - assertThat(actual).isFalse(); - } - - @Test - void 개발_퀴즈를_생성할_수_없는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isInvalidate(QuizCategory.DEVELOP, wordMetadata, 5); - - // then - assertThat(actual).isTrue(); - } - - @Test - void 전체_실무_퀴즈를_생성할_수_있는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isValidate(QuizCategory.TOTAL, wordMetadata, 5); - - // then - assertThat(actual).isFalse(); - } - - @Test - void 전체_실무_퀴즈를_생성할_수_없는지_여부를_확인한다() { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when - boolean actual = QuizWordCountValidator.isInvalidate(QuizCategory.TOTAL, wordMetadata, 5); - - // then - assertThat(actual).isTrue(); - } - - @ParameterizedTest(name = "카테고리가 {0} 일 때 퀴즈 생성 여부를 판단할 수 없다") - @NullSource - void 유효한_퀴즈_카테고리가_아니라면_퀴즈_생성_가능_여부를_판단할_수_없다(QuizCategory invalidQuizCategory) { - // given - WordMetadata wordMetadata = new WordMetadata(); - - // when & then - assertThatThrownBy( - () -> QuizWordCountValidator.isValidate(invalidQuizCategory, wordMetadata, 5) - ).isInstanceOf(QuizCategoryNotFoundException.class) - .hasMessage("지정한 퀴즈 카테고리를 찾을 수 없습니다."); - } -} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/QuizTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/QuizTest.java index 781519e3..968f8842 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/QuizTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/QuizTest.java @@ -65,6 +65,7 @@ class QuizTest { // then assertAll( + () -> assertThat(quiz.isSolved()).isTrue(), () -> assertThat(actual).hasSize(5), () -> assertThat(actual.get(0).getAccountId()).isEqualTo(1L), () -> assertThat(actual.get(0).getQuizId()).isEqualTo(6L), @@ -109,16 +110,4 @@ class QuizTest { // when & then assertThatThrownBy(() -> quizQuestions.remove(0)).isInstanceOf(UnsupportedOperationException.class); } - - @Test - void 퀴즈를_푼_상태로_변경한다() { - // given - Quiz quiz = new Quiz(1L); - - // when - quiz.solve(); - - // then - assertThat(quiz.isSolved()).isEqualTo(true); - } } diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/service/QuizWordCountValidatorTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/service/QuizWordCountValidatorTest.java new file mode 100644 index 00000000..bc2163db --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/domain/service/QuizWordCountValidatorTest.java @@ -0,0 +1,160 @@ +package com.dnd.spaced.core.quiz.domain.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.dnd.spaced.core.quiz.application.exception.QuizCategoryNotFoundException; +import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; +import com.dnd.spaced.core.word.domain.WordMetadata; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class QuizWordCountValidatorTest { + + @Test + void 디자인_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_크거나_같다면_퀴즈를_생성할_수_있다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + wordMetadata.addDesignWordCount(); + wordMetadata.addDesignWordCount(); + wordMetadata.addDesignWordCount(); + wordMetadata.addDesignWordCount(); + wordMetadata.addDesignWordCount(); + + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.DESIGN, wordMetadata, 5); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 디자인_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_적다면_퀴즈를_생성할_수_없다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.DESIGN, wordMetadata, 5); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 비즈니스_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_크거나_같다면_퀴즈를_생성할_수_있다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + wordMetadata.addBusinessWordCount(); + wordMetadata.addBusinessWordCount(); + wordMetadata.addBusinessWordCount(); + wordMetadata.addBusinessWordCount(); + wordMetadata.addBusinessWordCount(); + + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.BUSINESS, wordMetadata, 5); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 비즈니스_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_적다면_퀴즈를_생성할_수_없다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.BUSINESS, wordMetadata, 5); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 개발_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_크거나_같다면_퀴즈를_생성할_수_있다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + wordMetadata.addDevelopWordCount(); + wordMetadata.addDevelopWordCount(); + wordMetadata.addDevelopWordCount(); + wordMetadata.addDevelopWordCount(); + wordMetadata.addDevelopWordCount(); + + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.DEVELOP, wordMetadata, 5); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 개발_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_적다면_퀴즈를_생성할_수_없다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.DEVELOP, wordMetadata, 5); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 전체_실무_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_크거나_같다면_퀴즈를_생성할_수_있다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + wordMetadata.addDevelopWordCount(); + wordMetadata.addDevelopWordCount(); + wordMetadata.addBusinessWordCount(); + wordMetadata.addBusinessWordCount(); + wordMetadata.addDesignWordCount(); + + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.TOTAL, wordMetadata, 5); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 전체_실무_카테고리_용어_개수가_퀴즈_생성_시_필요한_개수보다_적다면_퀴즈를_생성할_수_없다() { + // given + WordMetadata wordMetadata = new WordMetadata(); + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when + boolean actual = quizWordCountValidator.isValidate(QuizCategory.TOTAL, wordMetadata, 5); + + // then + assertThat(actual).isFalse(); + } + + @ParameterizedTest(name = "카테고리가 {0} 일 때 퀴즈 생성 여부를 판단할 수 없다") + @NullSource + void 유효한_퀴즈_카테고리가_아니라면_퀴즈_생성_가능_여부를_판단할_수_없다(QuizCategory invalidQuizCategory) { + // given + WordMetadata wordMetadata = new WordMetadata(); + QuizWordCountValidator quizWordCountValidator = QuizWordCountValidator.create(); + + // when & then + assertThatThrownBy( + () -> quizWordCountValidator.isValidate(invalidQuizCategory, wordMetadata, 5) + ).isInstanceOf(QuizCategoryNotFoundException.class) + .hasMessage("지정한 퀴즈 카테고리를 찾을 수 없습니다."); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/QuizControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/QuizControllerTest.java index 37320dbb..dbd4dc0b 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/QuizControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/QuizControllerTest.java @@ -21,7 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.dnd.spaced.config.common.CommonControllerSliceTest; -import com.dnd.spaced.core.quiz.application.QuizService; +import com.dnd.spaced.core.quiz.application.QuizServiceFacade; import com.dnd.spaced.core.quiz.application.dto.request.CreateQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest.SubmitAnswerRequest; @@ -47,13 +47,13 @@ class QuizControllerTest extends CommonControllerSliceTest { @Autowired - QuizService quizService; + QuizServiceFacade quizServiceFacade; @Test @WithMockUser("1") void 퀴즈_생성_요청_성공_테스트() throws Exception { // given - given(quizService.createQuiz(anyLong(), any(CreateQuizRequest.class))).willReturn(1L); + given(quizServiceFacade.createQuiz(anyLong(), any(CreateQuizRequest.class))).willReturn(1L); CreateQuizRequest request = new CreateQuizRequest("전체 실무"); @@ -67,7 +67,7 @@ class QuizControllerTest extends CommonControllerSliceTest { header().string("Location", "/quizzes/1") ); - verify(quizService).createQuiz(anyLong(), any(CreateQuizRequest.class)); + verify(quizServiceFacade).createQuiz(anyLong(), any(CreateQuizRequest.class)); 퀴즈_생성_요청_문서화(resultActions); } @@ -86,7 +86,7 @@ class QuizControllerTest extends CommonControllerSliceTest { @WithMockUser("1") void 퀴즈_채점_요청_성공_테스트() throws Exception { // given - willDoNothing().given(quizService).grade(anyLong(), anyLong(), any(GradeQuizRequest.class)); + willDoNothing().given(quizServiceFacade).grade(anyLong(), anyLong(), any(GradeQuizRequest.class)); SubmitAnswerRequest[] submitAnswers = { new SubmitAnswerRequest(1L, "Authorization"), @@ -107,7 +107,7 @@ class QuizControllerTest extends CommonControllerSliceTest { header().string("Location", "/quizzes/1/graded-answer") ); - verify(quizService).grade(anyLong(), anyLong(), any(GradeQuizRequest.class)); + verify(quizServiceFacade).grade(anyLong(), anyLong(), any(GradeQuizRequest.class)); 퀴즈_채점_요청_문서화(resultActions); } @@ -224,7 +224,7 @@ class QuizControllerTest extends CommonControllerSliceTest { ), quizGradedAnswerResponse5.id() ); - given(quizService.readGradedAnswers(anyLong(), any(ReadQuizGradedAnswerSearchRequest.class), any(Pageable.class))).willReturn(response); + given(quizServiceFacade.readGradedAnswers(anyLong(), any(ReadQuizGradedAnswerSearchRequest.class), any(Pageable.class))).willReturn(response); // when & then ResultActions resultActions = mockMvc.perform( @@ -246,7 +246,7 @@ class QuizControllerTest extends CommonControllerSliceTest { jsonPath("answers[*].corrected").exists() ); - verify(quizService).readGradedAnswers( + verify(quizServiceFacade).readGradedAnswers( anyLong(), any(ReadQuizGradedAnswerSearchRequest.class), any(Pageable.class) @@ -379,7 +379,7 @@ class QuizControllerTest extends CommonControllerSliceTest { quizGradedAnswerResponse5.id() ); - given(quizService.readGradedAnswers(anyLong(), anyLong())).willReturn(response); + given(quizServiceFacade.readGradedAnswers(anyLong(), anyLong())).willReturn(response); // when & then ResultActions resultActions = mockMvc.perform( @@ -401,7 +401,7 @@ class QuizControllerTest extends CommonControllerSliceTest { jsonPath("answers[*].corrected").exists() ); - verify(quizService).readGradedAnswers(anyLong(), anyLong()); + verify(quizServiceFacade).readGradedAnswers(anyLong(), anyLong()); 특정_퀴즈에_대한_회원이_제출한_답_목록_조회_요청_문서화(resultActions); } @@ -438,7 +438,7 @@ class QuizControllerTest extends CommonControllerSliceTest { @WithMockUser("1") void 퀴즈_조회_요청_성공_테스트() throws Exception { // given - given(quizService.readQuiz(anyLong(), anyLong())).willReturn(createQuizResponse()); + given(quizServiceFacade.readQuiz(anyLong(), anyLong())).willReturn(createQuizResponse()); // when & then ResultActions resultActions = mockMvc.perform( @@ -464,7 +464,7 @@ class QuizControllerTest extends CommonControllerSliceTest { jsonPath("quizQuestions[0].answerOptionWordId", is(1L), Long.class) ); - verify(quizService).readQuiz(anyLong(), anyLong()); + verify(quizServiceFacade).readQuiz(anyLong(), anyLong()); 퀴즈_조회_요청_문서화(resultActions); } @@ -514,7 +514,7 @@ class QuizControllerTest extends CommonControllerSliceTest { ); QuizCollectionResponse response = new QuizCollectionResponse(List.of(quizResponse), 1L); - given(quizService.readQuizzes(anyLong(), any(ReadAllQuizRequest.class), any(Pageable.class))).willReturn(response); + given(quizServiceFacade.readQuizzes(anyLong(), any(ReadAllQuizRequest.class), any(Pageable.class))).willReturn(response); // when & then ResultActions resultActions = mockMvc.perform( diff --git a/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/TodayQuizControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/TodayQuizControllerTest.java index 447502f6..2206beb8 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/TodayQuizControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/quiz/presentation/TodayQuizControllerTest.java @@ -24,7 +24,7 @@ import com.dnd.spaced.config.common.CommonControllerSliceTest; import com.dnd.spaced.config.docs.link.DocumentLinkGenerator.DocsUrl; -import com.dnd.spaced.core.quiz.application.TodayQuizService; +import com.dnd.spaced.core.quiz.application.TodayQuizServiceFacade; import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.ReadTodayQuizGradedAnswerSearchRequest; import com.dnd.spaced.core.quiz.application.dto.response.SimpleTodayQuizResponse; @@ -38,7 +38,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -50,7 +49,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { @Autowired - TodayQuizService todayQuizService; + TodayQuizServiceFacade todayQuizServiceFacade; @Test void 최신_오늘의_퀴즈_요청_성공_테스트() throws Exception { @@ -66,7 +65,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { LocalDateTime.now() ); - given(todayQuizService.readLatestTodayQuiz()).willReturn(todayQuizResponse); + given(todayQuizServiceFacade.readLatestTodayQuiz()).willReturn(todayQuizResponse); // when & then ResultActions resultActions = mockMvc.perform( @@ -81,7 +80,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { "인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘") ); - verify(todayQuizService).readLatestTodayQuiz(); + verify(todayQuizServiceFacade).readLatestTodayQuiz(); 최신_오늘의_퀴즈_요청_문서화(resultActions); } @@ -128,7 +127,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { TodayQuizStatus.NOT_SOLVED ); - given(todayQuizService.readTodayQuiz(anyLong(), anyLong())).willReturn(todayQuizResponse); + given(todayQuizServiceFacade.readTodayQuiz(anyLong(), anyLong())).willReturn(todayQuizResponse); // when & then ResultActions resultActions = mockMvc.perform( @@ -146,7 +145,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { jsonPath("todayQuizQuestion.todayQuizOptions[*].content").exists() ); - verify(todayQuizService).readTodayQuiz(anyLong(), anyLong()); + verify(todayQuizServiceFacade).readTodayQuiz(anyLong(), anyLong()); 오늘의_퀴즈_조회_요청_문서화(resultActions); } @@ -195,7 +194,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { @WithMockUser("1") void 오늘의_퀴즈_채점_요청_성공_테스트() throws Exception { // given - willDoNothing().given(todayQuizService).grade(anyLong(), anyLong(), any(GradeTodayQuizRequest.class)); + willDoNothing().given(todayQuizServiceFacade).gradeTodayQuiz(anyLong(), anyLong(), any(GradeTodayQuizRequest.class)); // when & then GradeTodayQuizRequest request = new GradeTodayQuizRequest(1L, "Authorization"); @@ -211,7 +210,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { header().string("Location", "/today-quizzes/1/graded-answers") ); - verify(todayQuizService).grade(anyLong(), anyLong(), any(GradeTodayQuizRequest.class)); + verify(todayQuizServiceFacade).gradeTodayQuiz(anyLong(), anyLong(), any(GradeTodayQuizRequest.class)); 오늘의_퀴즈_채점_요청_문서화(resultActions); } @@ -258,7 +257,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { ); given( - todayQuizService.readTodayQuizGradedAnswers( + todayQuizServiceFacade.readTodayQuizGradedAnswers( anyLong(), any(ReadTodayQuizGradedAnswerSearchRequest.class), any(Pageable.class) @@ -289,7 +288,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { jsonPath("answers[0].answerQuizOptionContent").value("Authorization") ); - verify(todayQuizService).readTodayQuizGradedAnswers( + verify(todayQuizServiceFacade).readTodayQuizGradedAnswers( anyLong(), any(ReadTodayQuizGradedAnswerSearchRequest.class), any(Pageable.class) @@ -353,7 +352,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { true ); - given(todayQuizService.readTargetTodayQuizGradedAnswers(anyLong(), anyLong())).willReturn( + given(todayQuizServiceFacade.readTargetTodayQuizGradedAnswers(anyLong(), anyLong())).willReturn( todayQuizGradedAnswerResponse); // when & then @@ -374,7 +373,7 @@ class TodayQuizControllerTest extends CommonControllerSliceTest { jsonPath("answerQuizOptionContent").value("Authorization") ); - verify(todayQuizService).readTargetTodayQuizGradedAnswers(anyLong(), anyLong()); + verify(todayQuizServiceFacade).readTargetTodayQuizGradedAnswers(anyLong(), anyLong()); 특정_오늘의_퀴즈에_대한_채점_결과_조회_요청_문서화(resultActions); } diff --git a/space-d/src/test/java/com/dnd/spaced/core/report/domain/enums/ReportStatusTest.java b/space-d/src/test/java/com/dnd/spaced/core/report/domain/enums/ReportStatusTest.java index 102ea828..f5b152ec 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/report/domain/enums/ReportStatusTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/report/domain/enums/ReportStatusTest.java @@ -50,7 +50,7 @@ private static Stream findByTestArguments() { ReportStatus reportStatus = ReportStatus.PROCESSED; // when - boolean actual = reportStatus.isProcess(); + boolean actual = reportStatus.isProcessed(); // then assertThat(actual).isTrue(); diff --git a/space-d/src/test/java/com/dnd/spaced/core/skill/application/event/listener/GradedQuizEventListenerTest.java b/space-d/src/test/java/com/dnd/spaced/core/skill/application/event/listener/GradedQuizEventListenerTest.java index c5cea710..52aba2d2 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/skill/application/event/listener/GradedQuizEventListenerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/skill/application/event/listener/GradedQuizEventListenerTest.java @@ -10,8 +10,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import com.dnd.spaced.core.quiz.application.QuizService; -import com.dnd.spaced.core.quiz.application.TodayQuizService; +import com.dnd.spaced.core.quiz.application.QuizServiceFacade; +import com.dnd.spaced.core.quiz.application.TodayQuizServiceFacade; import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest; import com.dnd.spaced.core.quiz.application.dto.request.GradeQuizRequest.SubmitAnswerRequest; import com.dnd.spaced.core.quiz.application.dto.request.GradeTodayQuizRequest; @@ -44,10 +44,10 @@ class GradedQuizEventListenerTest { ApplicationEvents events; @Autowired - QuizService quizService; + QuizServiceFacade quizServiceFacade; @Autowired - TodayQuizService todayQuizService; + TodayQuizServiceFacade todayQuizServiceFacade; @Autowired SkillRepository skillRepository; @@ -76,7 +76,7 @@ class GradedQuizEventListenerTest { GradeQuizRequest request = new GradeQuizRequest(submitAnswers); // when - quizService.grade(1L, 1L, request); + quizServiceFacade.grade(1L, 1L, request); // then assertAll( @@ -109,7 +109,7 @@ class GradedQuizEventListenerTest { GradeQuizRequest request = new GradeQuizRequest(submitAnswers); // when - quizService.grade(1L, 1L, request); + quizServiceFacade.grade(1L, 1L, request); // then assertAll( @@ -140,7 +140,7 @@ class GradedQuizEventListenerTest { GradeQuizRequest request = new GradeQuizRequest(submitAnswers); // when - quizService.grade(1L, 1L, request); + quizServiceFacade.grade(1L, 1L, request); // then assertAll( @@ -162,7 +162,7 @@ class GradedQuizEventListenerTest { GradeTodayQuizRequest request = new GradeTodayQuizRequest(2L, "YAML"); // when - todayQuizService.grade(1L, 1L, request); + todayQuizServiceFacade.gradeTodayQuiz(1L, 1L, request); // then assertAll( @@ -188,7 +188,7 @@ class GradedQuizEventListenerTest { GradeTodayQuizRequest request = new GradeTodayQuizRequest(2L, "YAML"); // when - todayQuizService.grade(1L, 1L, request); + todayQuizServiceFacade.gradeTodayQuiz(1L, 1L, request); // then assertAll( @@ -212,7 +212,7 @@ class GradedQuizEventListenerTest { GradeTodayQuizRequest request = new GradeTodayQuizRequest(2L, "YAML"); // when - todayQuizService.grade(1L, 1L, request); + todayQuizServiceFacade.gradeTodayQuiz(1L, 1L, request); // then assertAll( diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/application/ReadPopularWordServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/application/ReadPopularWordServiceTest.java new file mode 100644 index 00000000..2b5b07d6 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/application/ReadPopularWordServiceTest.java @@ -0,0 +1,47 @@ +package com.dnd.spaced.core.word.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.domain.dto.PopularWord; +import com.dnd.spaced.core.word.domain.repository.PopularWordRepository; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReadPopularWordServiceTest { + + private static final Long WORD_ID = 1L; + + @Autowired + ReadPopularWordService readPopularWordService; + + @Autowired + PopularWordRepository popularWordRepository; + + @Test + void 많이_찾아본_용어_목록을_조회한다() { + // given + PopularWord popularWord = new PopularWord(1, WORD_ID, "Authorization"); + popularWordRepository.saveAll(List.of(popularWord), LocalDateTime.now()); + + // when + List actual = readPopularWordService.readPopularWords(); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).rank()).isEqualTo(popularWord.rank()), + () -> assertThat(actual.get(0).name()).isEqualTo(popularWord.name()), + () -> assertThat(actual.get(0).wordId()).isEqualTo(popularWord.wordId()) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/application/ReadWordViewServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/application/ReadWordViewServiceTest.java new file mode 100644 index 00000000..9a5716eb --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/application/ReadWordViewServiceTest.java @@ -0,0 +1,73 @@ +package com.dnd.spaced.core.word.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.application.dto.request.ReadAllWordRequest; +import com.dnd.spaced.core.word.application.exception.WordNotFoundException; +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.enums.Category; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReadWordViewServiceTest { + + private static final Long WORD_ID = 1L; + private static final Long NOT_FOUND_WORD_ID = -999L; + + @Autowired + ReadWordViewService readWordViewService; + + @Test + @Sql("classpath:sql/word/word.sql") + void 용어를_조회한다() { + // when + WordView actual = readWordViewService.readWord(WORD_ID); + + // then + assertAll( + () -> assertThat(actual.name()).isEqualTo("Authorization"), + () -> assertThat(actual.category()).isEqualTo(Category.DEVELOP), + () -> assertThat(actual.wordMeaning().getMeaning()).isEqualTo("인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘") + ); + } + + @Test + void 용어_ID로_용어를_찾지_못하면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> readWordViewService.readWord(NOT_FOUND_WORD_ID)) + .isInstanceOf(WordNotFoundException.class) + .hasMessage("지정한 ID에 해당하는 용어를 찾을 수 없습니다."); + } + + @Test + @Sql("classpath:sql/word/word.sql") + void 용어_목록을_조회한다() { + // given + ReadAllWordRequest request = new ReadAllWordRequest( + null, + null, + null + ); + + // when + List actual = readWordViewService.readWords(request, Pageable.ofSize(10)); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).name()).isEqualTo("Authorization") + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/application/SearchWordViewServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/application/SearchWordViewServiceTest.java new file mode 100644 index 00000000..037aa6b5 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/application/SearchWordViewServiceTest.java @@ -0,0 +1,47 @@ +package com.dnd.spaced.core.word.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.application.dto.request.SearchWordRequest; +import com.dnd.spaced.core.word.domain.dto.WordView; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SearchWordViewServiceTest { + + @Autowired + SearchWordViewService wordService; + + @Test + @Sql("classpath:sql/word/word.sql") + void 용어를_검색한다() { + // given + SearchWordRequest request = new SearchWordRequest( + "Authorization", + null, + null, + null, + null + ); + + // when + List actual = wordService.searchWord(request, Pageable.ofSize(10)); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).name()).isEqualTo("Authorization") + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/application/WordServiceTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/application/WordServiceFacadeTest.java similarity index 86% rename from space-d/src/test/java/com/dnd/spaced/core/word/application/WordServiceTest.java rename to space-d/src/test/java/com/dnd/spaced/core/word/application/WordServiceFacadeTest.java index ef13028f..d838caae 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/word/application/WordServiceTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/word/application/WordServiceFacadeTest.java @@ -31,10 +31,13 @@ @RecordApplicationEvents @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -class WordServiceTest { +class WordServiceFacadeTest { + + private static final long WORD_ID = 1L; + private static final long NOT_FOUND_WORD = -999L; @Autowired - WordService wordService; + WordServiceFacade wordServiceFacade; @Autowired PopularWordRepository popularWordRepository; @@ -46,7 +49,7 @@ class WordServiceTest { @Sql("classpath:sql/word/word.sql") void 용어를_조회한다() { // when - WordResponse actual = wordService.readWord(1L); + WordResponse actual = wordServiceFacade.readWord(WORD_ID); // then assertAll( @@ -61,7 +64,7 @@ class WordServiceTest { @Test void 용어_식별자로_용어를_찾지_못하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> wordService.readWord(-999L)) + assertThatThrownBy(() -> wordServiceFacade.readWord(NOT_FOUND_WORD)) .isInstanceOf(WordNotFoundException.class) .hasMessage("지정한 ID에 해당하는 용어를 찾을 수 없습니다."); } @@ -77,7 +80,7 @@ class WordServiceTest { ); // when - WordCollectionResponse actual = wordService.readWords(request, Pageable.ofSize(10)); + WordCollectionResponse actual = wordServiceFacade.readWords(request, Pageable.ofSize(10)); // then assertAll( @@ -99,7 +102,7 @@ class WordServiceTest { ); // when - WordCollectionResponse actual = wordService.searchWord(request, Pageable.ofSize(10)); + WordCollectionResponse actual = wordServiceFacade.searchWord(request, Pageable.ofSize(10)); // then assertAll( @@ -111,11 +114,11 @@ class WordServiceTest { @Test void 많이_찾아본_용어_목록을_조회한다() { // given - PopularWord popularWord = new PopularWord(1, 1L, "Authorization"); + PopularWord popularWord = new PopularWord(1, WORD_ID, "Authorization"); popularWordRepository.saveAll(List.of(popularWord), LocalDateTime.now()); // when - PopularWordCollectionResponse actual = wordService.readPopularWords(); + PopularWordCollectionResponse actual = wordServiceFacade.readPopularWords(); // then assertAll( diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/application/event/listener/WordPersistEventListenerTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/application/event/listener/WordPersistEventListenerTest.java index 1d9190b7..cc40515e 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/word/application/event/listener/WordPersistEventListenerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/word/application/event/listener/WordPersistEventListenerTest.java @@ -12,7 +12,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import com.dnd.spaced.core.admin.application.AdminWordService; +import com.dnd.spaced.core.admin.application.AdminWordServiceFacade; import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest; import com.dnd.spaced.core.admin.application.dto.request.CreateWordRequest.CreatePronunciationRequest; import com.dnd.spaced.core.word.application.event.dto.FailedWordPersistedEvent; @@ -52,7 +52,7 @@ class WordPersistEventListenerTest { PronunciationRepository pronunciationRepository; @Autowired - AdminWordService adminWordService; + AdminWordServiceFacade adminWordServiceFacade; @Autowired WordRandomRepository wordRandomRepository; @@ -82,7 +82,7 @@ class WordPersistEventListenerTest { List.of("게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.") ); - adminWordService.createWord(request); + adminWordServiceFacade.createWord(request); assertAll( () -> assertThat(events.stream(PersistedWordEvent.class).count()).isOne(), @@ -114,7 +114,7 @@ class WordPersistEventListenerTest { List.of("게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.") ); - adminWordService.createWord(request); + adminWordServiceFacade.createWord(request); assertAll( () -> assertThat(events.stream(PersistedWordEvent.class).count()).isOne(), @@ -140,7 +140,7 @@ class WordPersistEventListenerTest { List.of("게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.") ); - adminWordService.createWord(request); + adminWordServiceFacade.createWord(request); assertAll( () -> assertThat(events.stream(PersistedWordEvent.class).count()).isOne(), diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/domain/WordExampleTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/domain/WordExampleTest.java index 463454fc..1c622cdb 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/word/domain/WordExampleTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/word/domain/WordExampleTest.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.springframework.test.util.ReflectionTestUtils; @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @@ -64,19 +63,6 @@ class WordExampleTest { assertThat(wordExample.getContent()).isEqualTo(changedExample); } - @Test - void 용어_예문의_식별자_여부를_판단한다() { - // given - WordExample wordExample = WordExample.from("시스템 관리자는 신입 직원들에게 회사 내부 네트워크에 대한 Authorization을 부여했다."); - ReflectionTestUtils.setField(wordExample, "id", 1L); - - // when - boolean actual = wordExample.isEqualTo(1L); - - // then - assertThat(actual).isTrue(); - } - @Test void 용어_예문을_삭제한다() { // given diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepositoryTest.java new file mode 100644 index 00000000..e9db9200 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/PronunciationGatewayRepositoryTest.java @@ -0,0 +1,115 @@ +package com.dnd.spaced.core.word.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.domain.Pronunciation; +import com.dnd.spaced.core.word.domain.Word; +import com.dnd.spaced.core.word.domain.repository.WordRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class PronunciationGatewayRepositoryTest { + + private static final long PRONUNCIATION_ID = 1L; + private static final long DELETED_PRONUNCIATION_ID = 2L; + private static final long WORD_ID = 1L; + + @Autowired + PronunciationGatewayRepository pronunciationRepository; + + @Autowired + WordRepository wordRepository; + + @Test + @Sql("classpath:sql/word/pronunciation.sql") + void 삭제하지_않은_용어_발음을_id로_조회한다() { + // when + Optional actual = pronunciationRepository.findBy(PRONUNCIATION_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getId()).isEqualTo(PRONUNCIATION_ID) + ); + } + + @Test + @Sql("classpath:sql/word/pronunciation.sql") + void 삭제한_용어_발음은_id로_조회할_수_없다() { + // when + Optional actual = pronunciationRepository.findBy(DELETED_PRONUNCIATION_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + void 용어_발음_다수를_영속화_한다() { + // given + Word word = Word.builder() + .name("Authorization") + .meaning("인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘") + .categoryName("개발") + .build(); + + wordRepository.save(word); + + Pronunciation pronunciation1 = Pronunciation.of("어써라이제이션", "한글 발음"); + Pronunciation pronunciation2 = Pronunciation.of("어썰라이제이션", "한글 발음"); + Pronunciation pronunciation3 = Pronunciation.of("오써러제이션", "한글 발음"); + pronunciation1.initWord(word); + pronunciation2.initWord(word); + pronunciation3.initWord(word); + + List pronunciations = List.of(pronunciation1, pronunciation2, pronunciation3); + + // when + pronunciationRepository.saveAll(pronunciations); + + // then + Optional actual1 = pronunciationRepository.findBy(1L); + Optional actual2 = pronunciationRepository.findBy(2L); + Optional actual3 = pronunciationRepository.findBy(3L); + + assertAll( + () -> assertThat(actual1.get().getContent()).isEqualTo("어써라이제이션"), + () -> assertThat(actual2.get().getContent()).isEqualTo("어썰라이제이션"), + () -> assertThat(actual3.get().getContent()).isEqualTo("오써러제이션") + ); + } + + @Test + @Sql("classpath:sql/word/pronunciation.sql") + void 삭제되지_않은_발음_개수를_조회한다() { + // when + long actual = pronunciationRepository.countBy(WORD_ID); + + // then + assertThat(actual).isEqualTo(1L); + } + + @Test + @Sql("classpath:sql/word/pronunciation.sql") + @Transactional + void 특정_용어의_모든_용어_발음을_삭제한다() { + // when + pronunciationRepository.deleteAllBy(1L); + + // then + long actual = pronunciationRepository.countBy(WORD_ID); + + assertThat(actual).isZero(); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepositoryTest.java new file mode 100644 index 00000000..6067d8f7 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordExampleGatewayRepositoryTest.java @@ -0,0 +1,128 @@ +package com.dnd.spaced.core.word.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.domain.Word; +import com.dnd.spaced.core.word.domain.WordExample; +import com.dnd.spaced.core.word.domain.repository.WordRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class WordExampleGatewayRepositoryTest { + + private static final long WORD_EXAMPLE_ID = 1L; + private static final long DELETED_WORD_EXAMPLE_ID = 2L; + private static final long WORD_ID = 1L; + + @Autowired + WordExampleGatewayRepository wordExampleRepository; + + @Autowired + WordRepository wordRepository; + + @Test + @Sql("classpath:sql/word/word_example.sql") + void 삭제하지_않은_용어_예문을_id로_조회한다() { + // when + Optional actual = wordExampleRepository.findBy(WORD_EXAMPLE_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getId()).isEqualTo(WORD_EXAMPLE_ID) + ); + } + + @Test + @Sql("classpath:sql/word/word_example.sql") + void 삭제한_용어_예문은_id로_조회할_수_없다() { + // when + Optional actual = wordExampleRepository.findBy(DELETED_WORD_EXAMPLE_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql("classpath:sql/word/word_example.sql") + void 삭제하지_않은_용어_예문_개수를_조회한다() { + // when + long actual = wordExampleRepository.countBy(WORD_ID); + + // then + assertThat(actual).isEqualTo(1L); + } + + @Test + @Sql("classpath:sql/word/word_example.sql") + @Transactional + void 삭제하지_않은_용어_예문을_수정한다() { + // when + wordExampleRepository.update(WORD_EXAMPLE_ID, "웹 API 요청 시 사용자 인증을 위해서는 HTTP 헤더에 Authorization 토큰을 포함해야 합니다. 서버는 이 토큰을 검증하여 접근 권한을 확인한 후 요청된 데이터를 반환합니다."); + + // then + WordExample actual = wordExampleRepository.findBy(WORD_EXAMPLE_ID).get(); + + assertThat(actual.getContent()).isEqualTo("웹 API 요청 시 사용자 인증을 위해서는 HTTP 헤더에 Authorization 토큰을 포함해야 합니다. 서버는 이 토큰을 검증하여 접근 권한을 확인한 후 요청된 데이터를 반환합니다."); + } + + @Test + @Sql("classpath:sql/word/word_example.sql") + @Transactional + void 특정_용어의_모든_용어_예문을_삭제한다() { + // when + wordExampleRepository.deleteAllBy(WORD_ID); + + // then + long actual = wordExampleRepository.countBy(WORD_ID); + + assertThat(actual).isZero(); + } + + @Test + void 용어_예문_다수를_영속화한다() { + // given + Word word = Word.builder() + .name("Authorization") + .meaning("인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘") + .categoryName("개발") + .build(); + + wordRepository.save(word); + + WordExample wordExample1 = WordExample.from("게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다."); + WordExample wordExample2 = WordExample.from("해당 에러는 Authorization 과정이 실패해서 발생했습니다"); + WordExample wordExample3 = WordExample.from("웹 API 요청 시 사용자 인증을 위해서는 HTTP 헤더에 Authorization 토큰을 포함해야 합니다. 서버는 이 토큰을 검증하여 접근 권한을 확인한 후 요청된 데이터를 반환합니다."); + wordExample1.initWord(word); + wordExample2.initWord(word); + wordExample3.initWord(word); + + List wordExamples = List.of(wordExample1, wordExample2, wordExample3); + + // when + wordExampleRepository.saveAll(wordExamples); + + // then + WordExample actual1 = wordExampleRepository.findBy(1L).get(); + WordExample actual2 = wordExampleRepository.findBy(2L).get(); + WordExample actual3 = wordExampleRepository.findBy(3L).get(); + + assertAll( + () -> assertThat(actual1.getContent()).isEqualTo(wordExample1.getContent()), + () -> assertThat(actual2.getContent()).isEqualTo(wordExample2.getContent()), + () -> assertThat(actual3.getContent()).isEqualTo(wordExample3.getContent()) + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepositoryTest.java new file mode 100644 index 00000000..0c88ab2d --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordGatewayRepositoryTest.java @@ -0,0 +1,229 @@ +package com.dnd.spaced.core.word.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.domain.Word; +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class WordGatewayRepositoryTest { + + private static final long WORD_ID = 1L; + private static final long DELETED_WORD_ID = 2L; + + @Autowired + WordGatewayRepository wordGatewayRepository; + + @Autowired + WordCrudRepository wordCrudRepository; + + @Autowired + EntityManager em; + + @Test + void 용어를_영속화_한다() { + // given + Word word = Word.builder() + .name("Authorization") + .meaning("인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘") + .categoryName("개발") + .build(); + + // when + Word actual = wordGatewayRepository.save(word); + + // then + assertThat(actual.getId()).isPositive(); + } + + @Test + @Sql("classpath:sql/word/word.sql") + void 삭제하지_않은_용어의_영속화_여부를_확인한다() { + // when + boolean actual = wordGatewayRepository.existsBy(WORD_ID); + + // then + assertThat(actual).isTrue(); + } + + @Test + @Sql("classpath:sql/word/word.sql") + void 삭제한_용어의_영속화_여부를_확인한다() { + // when + boolean actual = wordGatewayRepository.existsBy(DELETED_WORD_ID); + + // then + assertThat(actual).isFalse(); + } + + @Test + @Sql("classpath:sql/word/word.sql") + void 삭제하지_않은_용어를_조회한다() { + // when + Optional actual = wordGatewayRepository.findBy(WORD_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().getId()).isEqualTo(WORD_ID) + ); + } + + @Test + @Sql("classpath:sql/word/word.sql") + void 삭제한_용어는_조회할_수_없다() { + // when + Optional actual = wordGatewayRepository.findBy(DELETED_WORD_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql("classpath:sql/word/word.sql") + @Transactional + void 삭제하지_않은_용어의_조회_수를_1_증가시킨다() { + // given + Word word = wordGatewayRepository.findBy(WORD_ID).get(); + + assertThat(word.getViewCount()).isZero(); + + em.clear(); + + // when + wordGatewayRepository.addViewCount(WORD_ID); + + // then + Word actual = wordGatewayRepository.findBy(WORD_ID).get(); + + assertThat(actual.getViewCount()).isEqualTo(1L); + } + + @Test + @Sql("classpath:sql/word/word.sql") + @Transactional + void 삭제한_용어의_조회_수는_증가되지_않는다() { + // given + Word deletedWord = wordCrudRepository.findById(DELETED_WORD_ID).get(); + + assertThat(deletedWord.getViewCount()).isZero(); + + em.clear(); + + // when + wordGatewayRepository.addViewCount(DELETED_WORD_ID); + + // then + Word actual = wordCrudRepository.findById(DELETED_WORD_ID).get(); + + assertThat(actual.getViewCount()).isZero(); + } + + @Test + @Sql("classpath:sql/word/word.sql") + @Transactional + void 삭제하지_않은_용어의_북마크_수를_1_증가시킨다() { + // given + Word word = wordGatewayRepository.findBy(WORD_ID).get(); + + assertThat(word.getBookmarkCount()).isZero(); + + em.clear(); + + // when + wordGatewayRepository.addBookmarkCount(WORD_ID); + + // then + Word actual = wordGatewayRepository.findBy(WORD_ID).get(); + + assertThat(actual.getBookmarkCount()).isEqualTo(1L); + } + + @Test + @Sql("classpath:sql/word/word.sql") + @Transactional + void 삭제한_용어의_북마크_수는_증가되지_않는다() { + // given + Word deletedWord = wordCrudRepository.findById(DELETED_WORD_ID).get(); + + assertThat(deletedWord.getBookmarkCount()).isEqualTo(1L); + + em.clear(); + + // when + wordGatewayRepository.addBookmarkCount(DELETED_WORD_ID); + + // then + Word actual = wordCrudRepository.findById(DELETED_WORD_ID).get(); + + assertThat(actual.getBookmarkCount()).isEqualTo(1L); + } + + @Test + @Sql("classpath:sql/word/word.sql") + @Transactional + void 삭제하지_않은_용어의_북마크_수를_1_감소시킨다() { + // given + wordGatewayRepository.addBookmarkCount(WORD_ID); + + Word word = wordGatewayRepository.findBy(WORD_ID).get(); + + assertThat(word.getBookmarkCount()).isEqualTo(1L); + + em.clear(); + + // when + wordGatewayRepository.subtractBookmarkCount(WORD_ID); + + // then + Word actual = wordGatewayRepository.findBy(WORD_ID).get(); + + assertThat(actual.getBookmarkCount()).isZero(); + } + + @Test + @Sql("classpath:sql/word/word.sql") + @Transactional + void 삭제한_용어의_북마크_수는_감소되지_않는다() { + // given + Word deletedWord = wordCrudRepository.findById(DELETED_WORD_ID).get(); + + assertThat(deletedWord.getBookmarkCount()).isEqualTo(1L); + + em.clear(); + + // when + wordGatewayRepository.subtractBookmarkCount(DELETED_WORD_ID); + + // then + Word actual = wordCrudRepository.findById(DELETED_WORD_ID).get(); + + assertThat(actual.getBookmarkCount()).isEqualTo(1L); + } + + @Test + @Sql("classpath:sql/word/word.sql") + void 삭제하지_않은_모든_용어의_이름을_조회한다() { + // when + List actual = wordGatewayRepository.findNameAllBy(new Long[]{WORD_ID, DELETED_WORD_ID}); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0)).isEqualTo("Authorization") + ); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepositoryTest.java index c4f6d02a..4e2e424a 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepositoryTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordRandomGatewayRepositoryTest.java @@ -5,7 +5,7 @@ import com.dnd.spaced.core.quiz.domain.enums.QuizCategory; import com.dnd.spaced.core.word.domain.Word; -import com.dnd.spaced.core.word.domain.dto.SimpleWordInfo; +import com.dnd.spaced.core.word.domain.dto.SimpleWord; import com.dnd.spaced.core.word.domain.enums.Category; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -54,7 +54,7 @@ void beforeEach() { wordRandomGatewayRepository.saveWith(savedWord, Category.DEVELOP); // then - List actual = wordRandomGatewayRepository.findRandomAllBy(QuizCategory.DEVELOP, 1L); + List actual = wordRandomGatewayRepository.findRandomAllBy(QuizCategory.DEVELOP, 1L); assertAll( () -> assertThat(actual).hasSize(1), @@ -89,7 +89,7 @@ void beforeEach() { wordRandomGatewayRepository.saveWith(tomlWord, Category.DEVELOP); // when - List actual = wordRandomGatewayRepository.findRandomAllBy(QuizCategory.DEVELOP, 2L); + List actual = wordRandomGatewayRepository.findRandomAllBy(QuizCategory.DEVELOP, 2L); // then assertThat(actual).hasSize(2); @@ -120,7 +120,7 @@ void beforeEach() { wordRandomGatewayRepository.saveWith(kpiWord, Category.BUSINESS); // when - List actual = wordRandomGatewayRepository.findRandomAllBy(QuizCategory.TOTAL, 3L); + List actual = wordRandomGatewayRepository.findRandomAllBy(QuizCategory.TOTAL, 3L); // then assertThat(actual).hasSize(3); diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordViewGatewayRepositoryTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordViewGatewayRepositoryTest.java new file mode 100644 index 00000000..461aafa5 --- /dev/null +++ b/space-d/src/test/java/com/dnd/spaced/core/word/infrastructure/persistence/WordViewGatewayRepositoryTest.java @@ -0,0 +1,318 @@ +package com.dnd.spaced.core.word.infrastructure.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.dnd.spaced.core.word.domain.dto.WordView; +import com.dnd.spaced.core.word.domain.enums.Category; +import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchCondition; +import com.dnd.spaced.core.word.domain.repository.dto.request.WordSearchPageRequest; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class WordViewGatewayRepositoryTest { + + private static final Long WORD_ID = 1L; + private static final Long DELETED_WORD_ID = 2L; + + @Autowired + WordViewGatewayRepository wordViewGatewayRepository; + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 삭제되지_않은_용어를_용어_id로_조회한다() { + // when + Optional actual = wordViewGatewayRepository.findBy(WORD_ID); + + // then + assertAll( + () -> assertThat(actual).isPresent(), + () -> assertThat(actual.get().id()).isEqualTo(WORD_ID) + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 삭제된_용어의_정보는_용어_id로_조회할_수_없다() { + // when + Optional actual = wordViewGatewayRepository.findBy(DELETED_WORD_ID); + + // then + assertThat(actual).isEmpty(); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 삭제되지_않은_용어_목록을_조회한다() { + // when + List actual = wordViewGatewayRepository.findAllBy( + null, + null, + null, + Pageable.ofSize(10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(10), + () -> assertThat(convertWordId(actual)).doesNotContain(DELETED_WORD_ID) + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 삭제되지_않고_사전_순서로_debounce_이후_용어_목록을_조회한다() { + // when + List actual = wordViewGatewayRepository.findAllBy( + null, + "debounce", + Category.DEVELOP, + Pageable.ofSize(10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(10), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("debounce") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_삭제되지_않은_첫_번째_용어_목록을_조회한다() { + // when + List actual = wordViewGatewayRepository.findAllBy( + Category.DEVELOP, + null, + null, + Pageable.ofSize(10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(10), + () -> assertThat(convertWordId(actual)).doesNotContain(DELETED_WORD_ID) + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_사전_순서로_debounce_이후이면서_삭제되지_않은_용어_목록을_조회한다() { + // when + List actual = wordViewGatewayRepository.findAllBy( + Category.DEVELOP, + "debounce", + Category.DEVELOP, + Pageable.ofSize(10) + ); + + // then + assertAll( + () -> assertThat(actual).hasSize(10), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("debounce") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition(null, Category.DEVELOP, null); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(10), null, null); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertAll( + () -> assertThat(actual).hasSize(10), + () -> assertThat(convertWordId(actual)).doesNotContain(DELETED_WORD_ID) + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_사전_순서로_용어_이름이_debounce_이후_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition(null, Category.DEVELOP, null); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(10), "debounce", Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertAll( + () -> assertThat(actual).hasSize(10), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("debounce") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_발음이_디로_시작하면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition(null, Category.DEVELOP, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), null, Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + assertThat(actual).hasSize(5); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_발음이_디로_시작하면서_사전_순으로_용어_이름이_default_이후이면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition(null, Category.DEVELOP, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), "default", Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + assertAll( + () -> assertThat(actual).hasSize(5), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("default") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_발음이_디로_시작하면서_용어_이름이_de로_시작하면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition("de", Category.DEVELOP, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), null, Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertThat(actual).hasSize(5); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 카테고리가_DEVELOP이면서_발음이_디로_시작하면서_용어_이름이_de로_시작하면서_사전_순으로_용어_이름이_design_이후이면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition("de", Category.DEVELOP, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), "design", Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("design") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 발음이_디로_시작하면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition(null, null, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), null, null); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertThat(actual).hasSize(5); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 발음이_디로_시작하면서_사전_순으로_용어_이름이_default_이후이면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition(null, null, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), "default", Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + assertAll( + () -> assertThat(actual).hasSize(5), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("default") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 발음이_디로_시작하면서_용어_이름이_de로_시작하면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition("de", null, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), null, Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertThat(actual).hasSize(5); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 발음이_디로_시작하면서_용어_이름이_de로_시작하면서_사전_순으로_용어_이름이_design_이후이면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition("de", null, "디"); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), "design", Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("design") + ); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 용어_이름이_de로_시작하면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition("de", null, null); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), null, Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertThat(actual).hasSize(5); + } + + @Test + @Sql("classpath:sql/word/word_view.sql") + void 용어_이름이_de로_시작하면서_사전_순으로_용어_이름이_design_이후이면서_삭제되지_않은_용어_목록을_검색한다() { + // given + WordSearchCondition condition = new WordSearchCondition("de", null, null); + WordSearchPageRequest pageRequest = new WordSearchPageRequest(Pageable.ofSize(5), "design", Category.DEVELOP); + + // when + List actual = wordViewGatewayRepository.search(condition, pageRequest); + + // then + assertAll( + () -> assertThat(actual).hasSize(1), + () -> assertThat(actual.get(0).name()).usingDefaultComparator().isGreaterThan("design") + ); + } + + private List convertWordId(List wordViews) { + return wordViews.stream() + .map(WordView::id) + .toList(); + } +} diff --git a/space-d/src/test/java/com/dnd/spaced/core/word/presentation/WordControllerTest.java b/space-d/src/test/java/com/dnd/spaced/core/word/presentation/WordControllerTest.java index dc4e5985..b9d60bc9 100644 --- a/space-d/src/test/java/com/dnd/spaced/core/word/presentation/WordControllerTest.java +++ b/space-d/src/test/java/com/dnd/spaced/core/word/presentation/WordControllerTest.java @@ -18,7 +18,7 @@ import com.dnd.spaced.config.common.CommonControllerSliceTest; import com.dnd.spaced.config.docs.link.DocumentLinkGenerator.DocsUrl; -import com.dnd.spaced.core.word.application.WordService; +import com.dnd.spaced.core.word.application.WordServiceFacade; import com.dnd.spaced.core.word.application.dto.request.SearchWordRequest; import com.dnd.spaced.core.word.application.dto.response.PopularWordCollectionResponse; import com.dnd.spaced.core.word.application.dto.response.WordCollectionResponse; @@ -27,7 +27,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; @@ -37,7 +36,7 @@ class WordControllerTest extends CommonControllerSliceTest { @Autowired - WordService wordService; + WordServiceFacade wordServiceFacade; @Test void 용어_조회_요청_성공_테스트() throws Exception { @@ -53,7 +52,7 @@ class WordControllerTest extends CommonControllerSliceTest { 1L ); - given(wordService.readWord(anyLong())).willReturn(wordResponse); + given(wordServiceFacade.readWord(anyLong())).willReturn(wordResponse); // when & then ResultActions resultActions = mockMvc.perform( @@ -112,7 +111,7 @@ class WordControllerTest extends CommonControllerSliceTest { 1L ); WordCollectionResponse wordCollectionResponse = new WordCollectionResponse(List.of(wordResponse), wordResponse.name()); - given(wordService.searchWord(any(SearchWordRequest.class), any(Pageable.class))).willReturn(wordCollectionResponse); + given(wordServiceFacade.searchWord(any(SearchWordRequest.class), any(Pageable.class))).willReturn(wordCollectionResponse); // when & then ResultActions resultActions = mockMvc.perform( @@ -183,7 +182,7 @@ class WordControllerTest extends CommonControllerSliceTest { 1L ); WordCollectionResponse wordCollectionResponse = new WordCollectionResponse(List.of(wordResponse), wordResponse.name()); - given(wordService.readWords(any(), any())).willReturn(wordCollectionResponse); + given(wordServiceFacade.readWords(any(), any())).willReturn(wordCollectionResponse); // when & then ResultActions resultActions = mockMvc.perform( @@ -240,7 +239,7 @@ class WordControllerTest extends CommonControllerSliceTest { // given PopularWordResponse popularWordResponse = new PopularWordResponse(1, 3L, "Authorization"); PopularWordCollectionResponse popularWordCollectionResponse = new PopularWordCollectionResponse(List.of(popularWordResponse)); - given(wordService.readPopularWords()).willReturn(popularWordCollectionResponse); + given(wordServiceFacade.readPopularWords()).willReturn(popularWordCollectionResponse); // when & then ResultActions resultActions = mockMvc.perform( diff --git a/space-d/src/test/resources/sql/account/account.sql b/space-d/src/test/resources/sql/account/account.sql index bcd093f5..23c140f6 100644 --- a/space-d/src/test/resources/sql/account/account.sql +++ b/space-d/src/test/resources/sql/account/account.sql @@ -1,2 +1,11 @@ -INSERT INTO accounts(id, created_at, updated_at, registration_id, social_identifier, company, experience, job_group, deleted, nickname, profile_image) +INSERT INTO accounts(id, created_at, updated_at, registration_id, social_id, company, experience, job_group, deleted, nickname, profile_image_name) VALUES (1, now(), now(), 'KAKAO', '12345', 'STARTUP', 'UNDER_FIRST', 'ETC', false, '재빠른지구001', 'earth.png'); + +INSERT INTO accounts(id, created_at, updated_at, registration_id, social_id, company, experience, job_group, deleted, nickname, profile_image_name) +VALUES (2, now(), now(), 'KAKAO', '54321', 'STARTUP', 'UNDER_FIRST', 'ETC', true, '재빠른지구002', 'earth.png'); + +INSERT INTO accounts(id, created_at, updated_at, registration_id, social_id, company, experience, job_group, deleted, nickname, profile_image_name) +VALUES (3, now(), now(), 'KAKAO', '13245', null, null, null, false, '재빠른지구002', 'earth.png'); + +INSERT INTO accounts(id, created_at, updated_at, registration_id, social_id, company, experience, job_group, deleted, nickname, profile_image_name) +VALUES (4, now(), now(), 'KAKAO', '32145', null, null, null, true, '재빠른지구002', 'earth.png'); diff --git a/space-d/src/test/resources/sql/auth/account.sql b/space-d/src/test/resources/sql/auth/account.sql index 00f51fd6..1d632ab5 100644 --- a/space-d/src/test/resources/sql/auth/account.sql +++ b/space-d/src/test/resources/sql/auth/account.sql @@ -1,2 +1,2 @@ -INSERT INTO accounts(id, created_at, updated_at, deleted, role, registration_id, social_identifier) +INSERT INTO accounts(id, created_at, updated_at, deleted, role, registration_id, social_id) VALUES (1, now(), now(), false, 'ROLE_USER', 'KAKAO', '12345'); diff --git a/space-d/src/test/resources/sql/bookmark/bookmark.sql b/space-d/src/test/resources/sql/bookmark/bookmark.sql index 7615d6d0..fc14a66c 100644 --- a/space-d/src/test/resources/sql/bookmark/bookmark.sql +++ b/space-d/src/test/resources/sql/bookmark/bookmark.sql @@ -1 +1,5 @@ INSERT INTO bookmarks(id, created_at, account_id, word_id) VALUES(1, now(), 1, 1); + +INSERT INTO bookmarks(id, created_at, account_id, word_id) VALUES(2, now(), 1, 2); + +INSERT INTO bookmarks(id, created_at, account_id, word_id) VALUES(3, now(), 1, 3); diff --git a/space-d/src/test/resources/sql/bookmark/word.sql b/space-d/src/test/resources/sql/bookmark/word.sql index 0c46de6e..d3eb6977 100644 --- a/space-d/src/test/resources/sql/bookmark/word.sql +++ b/space-d/src/test/resources/sql/bookmark/word.sql @@ -6,3 +6,21 @@ VALUES(1, now(), now(), '어써라이제이션', 'KOREAN', 1, false); INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) VALUES(1, now(), now(), '게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.', 1, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(2, now(), now(), 1, 'DEVELOP', 'YAML', 0, '사람이 읽기 쉬운 데이터 형식으로, 주로 설정 파일에 사용됩니다.', true); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(2, now(), now(), '야믈', 'KOREAN', 2, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(2, now(), now(), 'YAML은 설정 파일이나 데이터 교환 포맷으로 자주 사용됩니다.', 2, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(3, now(), now(), 0, 'DEVELOP', 'HTTP', 0, 'HyperText Transfer Protocol의 약자.', true); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(3, now(), now(), '에이치티티피', 'KOREAN', 2, true); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(3, now(), now(), 'HTTP 통신이 정상적으로 수행되지 않는 것 같습니다.', 2, true); diff --git a/space-d/src/test/resources/sql/comment/comment.sql b/space-d/src/test/resources/sql/comment/comment.sql index b57f7dab..51b9d095 100644 --- a/space-d/src/test/resources/sql/comment/comment.sql +++ b/space-d/src/test/resources/sql/comment/comment.sql @@ -1,2 +1,5 @@ INSERT INTO comments(id, created_at, updated_at, content, deleted, like_count, word_id, writer_id) VALUES (1, now(), now(), '이 용어는 언제 쓰는건가요?', false, 0, 1, 1); + +INSERT INTO comments(id, created_at, updated_at, content, deleted, like_count, word_id, writer_id) +VALUES (2, now(), now(), '이 용어 쓰는걸 본 적이 없는거 같아요', true, 1, 1, 1); diff --git a/space-d/src/test/resources/sql/comment/word.sql b/space-d/src/test/resources/sql/comment/word.sql index 0c46de6e..1adc2269 100644 --- a/space-d/src/test/resources/sql/comment/word.sql +++ b/space-d/src/test/resources/sql/comment/word.sql @@ -6,3 +6,13 @@ VALUES(1, now(), now(), '어써라이제이션', 'KOREAN', 1, false); INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) VALUES(1, now(), now(), '게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.', 1, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(2, now(), now(), 0, 'DEVELOP', 'HTTP', 0, 'HyperText Transfer Protocol의 약자.', true); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(2, now(), now(), '에이치티티피', 'KOREAN', 2, true); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(2, now(), now(), 'HTTP 통신이 정상적으로 수행되지 않는 것 같습니다.', 2, true); + diff --git a/space-d/src/test/resources/sql/like/account.sql b/space-d/src/test/resources/sql/like/account.sql index bcd093f5..61d7dbff 100644 --- a/space-d/src/test/resources/sql/like/account.sql +++ b/space-d/src/test/resources/sql/like/account.sql @@ -1,2 +1,2 @@ -INSERT INTO accounts(id, created_at, updated_at, registration_id, social_identifier, company, experience, job_group, deleted, nickname, profile_image) +INSERT INTO accounts(id, created_at, updated_at, registration_id, social_id, company, experience, job_group, deleted, nickname, profile_image_name) VALUES (1, now(), now(), 'KAKAO', '12345', 'STARTUP', 'UNDER_FIRST', 'ETC', false, '재빠른지구001', 'earth.png'); diff --git a/space-d/src/test/resources/sql/quiz/quiz_graded_answer.sql b/space-d/src/test/resources/sql/quiz/quiz_graded_answer.sql index cf5d0c35..a3803a62 100644 --- a/space-d/src/test/resources/sql/quiz/quiz_graded_answer.sql +++ b/space-d/src/test/resources/sql/quiz/quiz_graded_answer.sql @@ -1,5 +1,5 @@ -INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(1, now(), 1, 1, 1, 1, 'Authorization'); -INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(2, now(), 1, 1, 2, 2, 'YAML'); -INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(3, now(), 1, 1, 3, 3, 'TOML'); -INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(4, now(), 1, 1, 4, 4, 'deprecated'); -INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(5, now(), 1, 1, 5, 5, 'execute'); +INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(1, now(), 1, 2, 1, 1, 'Authorization'); +INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(2, now(), 1, 2, 2, 2, 'YAML'); +INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(3, now(), 1, 2, 3, 3, 'TOML'); +INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(4, now(), 1, 2, 4, 4, 'deprecated'); +INSERT INTO quiz_graded_answers(id, created_at, account_id, quiz_id, quiz_question_id, selected_word_id, selected_content) VALUES(5, now(), 1, 2, 5, 5, 'execute'); diff --git a/space-d/src/test/resources/sql/quiz/solved_quiz.sql b/space-d/src/test/resources/sql/quiz/solved_quiz.sql index 32469b0b..fb4e7ae7 100644 --- a/space-d/src/test/resources/sql/quiz/solved_quiz.sql +++ b/space-d/src/test/resources/sql/quiz/solved_quiz.sql @@ -1,12 +1,12 @@ SET REFERENTIAL_INTEGRITY FALSE; -INSERT INTO quizzes(id, created_at, account_id, solved) VALUES(1, now(), 1, true); +INSERT INTO quizzes(id, created_at, account_id, solved) VALUES(2, now(), 1, true); -INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(1, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘', 'Authorization', 1, 'DEVELOP', 1); -INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(2, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '사람이 읽기 쉬운 데이터 형식으로, 주로 설정 파일에 사용', 'YAML', 2, 'DEVELOP', 1); -INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(3, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '간단하고 가독성이 높은 설정 파일 형식으로, 키-값 쌍을 이용해 데이터를 표현', 'TOML', 3, 'DEVELOP', 1); -INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(4, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '더 이상 사용되지 않거나, 지원되지 않는다는 뜻', 'deprecated', 4, 'DEVELOP', 1); -INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(5, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', ' 주로 프로그램이나 코드, 명령을 실행할 때 사용', 'execute', 5, 'DEVELOP', 1); +INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(1, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘', 'Authorization', 1, 'DEVELOP', 2); +INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(2, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '사람이 읽기 쉬운 데이터 형식으로, 주로 설정 파일에 사용', 'YAML', 2, 'DEVELOP', 2); +INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(3, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '간단하고 가독성이 높은 설정 파일 형식으로, 키-값 쌍을 이용해 데이터를 표현', 'TOML', 3, 'DEVELOP', 2); +INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(4, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', '더 이상 사용되지 않거나, 지원되지 않는다는 뜻', 'deprecated', 4, 'DEVELOP', 2); +INSERT INTO quiz_questions(id, question, passage, answer_content, answer_word_id, quiz_category, quiz_id) VALUES(5, '다음 예문을 보고 예문에 맞는 용어를 선택해주세요.', ' 주로 프로그램이나 코드, 명령을 실행할 때 사용', 'execute', 5, 'DEVELOP', 2); INSERT INTO quiz_options(id, content, option_order, word_id, quiz_question_id) VALUES(1, 'Authorization', 1, 1, 1); INSERT INTO quiz_options(id, content, option_order, word_id, quiz_question_id) VALUES(2, 'COALESCE', 2, 6, 1); diff --git a/space-d/src/test/resources/sql/word/pronunciation.sql b/space-d/src/test/resources/sql/word/pronunciation.sql new file mode 100644 index 00000000..3825f62f --- /dev/null +++ b/space-d/src/test/resources/sql/word/pronunciation.sql @@ -0,0 +1,8 @@ +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(1, now(), now(), 0, 'DEVELOP', 'Authorization', 0, '인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(1, now(), now(), '어써라이제이션', 'KOREAN', 1, false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(2, now(), now(), '오써러제이션', 'KOREAN', 1, true); diff --git a/space-d/src/test/resources/sql/word/word.sql b/space-d/src/test/resources/sql/word/word.sql index 0c46de6e..6e2159f4 100644 --- a/space-d/src/test/resources/sql/word/word.sql +++ b/space-d/src/test/resources/sql/word/word.sql @@ -6,3 +6,12 @@ VALUES(1, now(), now(), '어써라이제이션', 'KOREAN', 1, false); INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) VALUES(1, now(), now(), '게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.', 1, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(2, now(), now(), 1, 'DEVELOP', 'YAML', 0, '사람이 읽기 쉬운 데이터 형식으로, 주로 설정 파일에 사용됩니다.', true); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(2, now(), now(), '야믈', 'KOREAN', 2, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(2, now(), now(), 'YAML은 설정 파일이나 데이터 교환 포맷으로 자주 사용됩니다.', 2, false); diff --git a/space-d/src/test/resources/sql/bookmark/deleted_word.sql b/space-d/src/test/resources/sql/word/word_example.sql similarity index 70% rename from space-d/src/test/resources/sql/bookmark/deleted_word.sql rename to space-d/src/test/resources/sql/word/word_example.sql index 465caf54..dc05c88d 100644 --- a/space-d/src/test/resources/sql/bookmark/deleted_word.sql +++ b/space-d/src/test/resources/sql/word/word_example.sql @@ -1,8 +1,8 @@ INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) -VALUES(1, now(), now(), 0, 'DEVELOP', 'Authorization', 0, '인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘', true); - -INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) -VALUES(1, now(), now(), '어써라이제이션', 'KOREAN', 1, false); +VALUES(1, now(), now(), 0, 'DEVELOP', 'Authorization', 0, '인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘', false); INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) VALUES(1, now(), now(), '게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.', 1, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(2, now(), now(), '해당 에러는 Authorization 과정이 실패해서 발생했습니다', 1, true); diff --git a/space-d/src/test/resources/sql/word/word_view.sql b/space-d/src/test/resources/sql/word/word_view.sql new file mode 100644 index 00000000..c7ba8565 --- /dev/null +++ b/space-d/src/test/resources/sql/word/word_view.sql @@ -0,0 +1,269 @@ +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(1, now(), now(), 0, 'DEVELOP', 'Authorization', 0, '인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하고 제어하는 보안 메커니즘', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(1, now(), now(), '어써라이제이션', 'KOREAN', 1, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(1, now(), now(), '게시글 삭제는 작성자와 관리자만 Authorization이 있도록 구현했습니다.', 1, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(2, now(), now(), 0, 'DEVELOP', 'annotation', 0, '소스 코드에 추가되는 주석이나 설명을 의미하며, 코드의 이해를 돕기 위해 사용됩니다.', true); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(2, now(), now(), '어노테이션', 'KOREAN', 2, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(2, now(), now(), '어노테이션은 소스 코드에 주석을 추가하여 코드의 의미를 명확히 하는 데 사용됩니다.', 2, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(3, now(), now(), 0, 'DEVELOP', 'TOML', 0, '간단하고 가독성이 높은 설정 파일 형식으로, 키-값 쌍을 이용해 데이터를 표현합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(3, now(), now(), '톰엘', 'KOREAN', 3, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(3, now(), now(), 'TOML은 구성 파일에 사용하기 쉬운 데이터 직렬화 언어입니다.', 3, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(4, now(), now(), 0, 'DEVELOP', 'deprecated', 0, '더 이상 사용되지 않거나, 지원되지 않는다는 뜻입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(4, now(), now(), '데프리케이티드', 'KOREAN', 4, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(4, now(), now(), '이 함수는 더 이상 사용되지 않으므로 deprecated되었습니다.', 4, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(5, now(), now(), 0, 'DEVELOP', 'execute', 0, '주로 프로그램이나 코드, 명령을 실행할 때 사용됩니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(5, now(), now(), '엑시큐트', 'KOREAN', 5, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(5, now(), now(), '코드를 실행하려면 Run 버튼을 눌러서 execute시킵니다.', 5, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(6, now(), now(), 0, 'DEVELOP', 'COALESCE', 0, 'SQL에서 인자로 주어진 컬럼들 중에서 NULL이 아닌 첫 번째 값을 반환하는 함수입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(6, now(), now(), '코얼레스', 'KOREAN', 6, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(6, now(), now(), '데이터베이스 쿼리에서 COALESCE 함수를 사용해 NULL 값을 빈 문자열로 대체합니다.', 6, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(7, now(), now(), 0, 'DEVELOP', 'Queue', 0, '대기열을 의미하며, 데이터 구조에서 먼저 들어온 데이터가 먼저 나가는(FIFO) 방식의 대기열을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(7, now(), now(), '큐', 'KOREAN', 7, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(7, now(), now(), '비동기 작업을 처리하기 위해 작업 Queue를 사용하여 작업을 순차적으로 실행합니다.', 7, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(8, now(), now(), 0, 'DEVELOP', 'carousel', 0, '회전목마를 의미하며, UI 중 이미지를 순환하며 보여주는 슬라이더를 지칭합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(8, now(), now(), '캐러셀', 'KOREAN', 8, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(8, now(), now(), '사용자는 이미지 캐러셀을 통해 다양한 사진을 볼 수 있습니다.', 8, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(9, now(), now(), 0, 'DEVELOP', 'Dequeue', 0, 'Queue의 반대 동작으로, 큐에 저장된 데이터 중 첫 번째 요소를 제거하고 반환하는 것을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(9, now(), now(), '디큐', 'KOREAN', 9, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(9, now(), now(), '사용자 요청을 Queue에 저장하고, 순서대로 Dequeue하여 처리합니다.', 9, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(10, now(), now(), 0, 'DEVELOP', 'JWT', 0, 'JWT는 JSON Web Token의 약자로, JSON 형식의 웹 토큰을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(10, now(), now(), '제이더블유티', 'KOREAN', 10, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(10, now(), now(), '사용자가 로그인하면 서버는 JWT를 생성하여 클라이언트에게 반환합니다.', 10, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(11, now(), now(), 0, 'DEVELOP', 'GUI', 0, '그래픽 사용자 인터페이스를 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(11, now(), now(), '구이', 'KOREAN', 11, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(11, now(), now(), '새로 출시된 운영 체제는 직관적인 GUI를 제공하여 사용자가 쉽게 파일을 관리하고 프로그램을 실행할 수 있습니다.', 11, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(12, now(), now(), 0, 'DEVELOP', 'usage', 0, '사용, 용법을 뜻합니다. CPU, 메모리, 네트워크 등 시스템이나 소프트웨어 자원의 사용량을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(12, now(), now(), '유씨지', 'KOREAN', 12, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(12, now(), now(), '시스템의 CPU usage가 80%를 초과했습니다.', 12, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(13, now(), now(), 0, 'DEVELOP', 'SaaS', 0, 'Software as a Service의 약자로, 서비스형 소프트웨어를 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(13, now(), now(), '사스', 'KOREAN', 13, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(13, now(), now(), '회사에 클라우드 기반 SaaS 솔루션을 도입했습니다.', 13, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(14, now(), now(), 0, 'DEVELOP', 'directory', 0, '파일 시스템에서 파일과 폴더를 계층적으로 구성하는 데 사용되는 구조를 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(14, now(), now(), '디렉터리', 'KOREAN', 14, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(14, now(), now(), 'directory 권한 설정을 통해 특정 사용자만 접근할 수 있도록 했습니다.', 14, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(15, now(), now(), 0, 'DEVELOP', 'empty', 0, '비어 있는, 아무것도 없는 상태를 의미하며, 변수나 데이터 구조가 값을 포함하지 않은 상태를 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(15, now(), now(), '엠티', 'KOREAN', 15, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(15, now(), now(), '배열이 empty인지 확인한 후에 데이터 추가 작업을 수행합니다.', 15, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(16, now(), now(), 0, 'DEVELOP', 'redirect', 0, '웹 서버나 애플리케이션에서 사용자가 요청한 URL을 다른 URL로 자동으로 보내는 행위를 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(16, now(), now(), '리다이렉트', 'KOREAN', 16, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(16, now(), now(), 'HTTP 상태 코드 중 301은 영구적으로 다른 URL로 redirect하며, 302는 일시적으로 다른 URL로 redirect했다는 뜻입니다.', 16, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(17, now(), now(), 0, 'DEVELOP', 'jar', 0, '자바 애플리케이션을 패키징하여 배포하는 데 사용되는 파일 형식으로, 여러 클래스 파일과 관련 메타데이터를 포함합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(17, now(), now(), '자르', 'KOREAN', 17, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(17, now(), now(), 'JAR 파일을 실행하여 애플리케이션을 시작합니다', 17, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(18, now(), now(), 0, 'DEVELOP', 'locale', 0, '소프트웨어나 시스템에서 특정 언어와 문화권에 맞춘 설정을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(18, now(), now(), '로캘', 'KOREAN', 18, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(18, now(), now(), '시스템의 locale을 한국어로 설정합니다.', 18, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(19, now(), now(), 0, 'DEVELOP', 'gradient', 0, 'CSS에서 색상이나 밝기가 점진적으로 변하는 효과로, 웹 페이지의 배경이나 요소에 주로 적용합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(19, now(), now(), '그레이디언트', 'KOREAN', 19, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(19, now(), now(), 'CSS에서 linear-gradient와 radial-gradient 속성을 사용하여 다양한 형태의 gradient를 만들 수 있습니다.', 19, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(20, now(), now(), 0, 'DEVELOP', 'status', 0, '시스템, 프로세스, 작업, 또는 소프트웨어의 현재 상태나 진행 상황을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(20, now(), now(), '스테이터스', 'KOREAN', 20, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(20, now(), now(), '클라이언트의 요청이 성공적으로 처리되었음을 나타내기 위해 서버는 200 OK HTTP status 코드를 반환합니다.', 20, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(21, now(), now(), 0, 'DEVELOP', 'digital', 0, '디지털 기술과 관련된 용어로, 아날로그가 아닌 이진 데이터를 사용하는 방식을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(21, now(), now(), '디지털', 'KOREAN', 21, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(21, now(), now(), 'digital 신호 처리는 아날로그 신호를 디지털로 변환하는 과정입니다.', 21, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(22, now(), now(), 0, 'DEVELOP', 'debugging', 0, '프로그램의 오류를 찾아 수정하는 과정입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(22, now(), now(), '디버깅', 'KOREAN', 22, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(22, now(), now(), '코드를 debugging하여 문제를 해결했습니다.', 22, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(23, now(), now(), 0, 'DEVELOP', 'disk', 0, '컴퓨터 저장 장치 중 하나로, 데이터를 저장하는 물리적 매체입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(23, now(), now(), '디스크', 'KOREAN', 23, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(23, now(), now(), 'disk 용량이 부족하여 파일을 삭제했습니다.', 23, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(24, now(), now(), 0, 'DEVELOP', 'directory', 0, '파일 시스템에서 파일과 폴더를 계층적으로 구성하는 구조입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(24, now(), now(), '디렉토리', 'KOREAN', 24, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(24, now(), now(), 'directory 권한을 설정하여 접근을 제한했습니다.', 24, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(25, now(), now(), 0, 'DEVELOP', 'device', 0, '하드웨어 장치를 의미하며, 컴퓨터 주변기기를 포함합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(25, now(), now(), '디바이스', 'KOREAN', 25, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(25, now(), now(), '새로운 device를 연결하여 테스트했습니다.', 25, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(26, now(), now(), 0, 'DEVELOP', 'design', 0, '제품이나 서비스의 외관 및 기능을 계획하고 설계하는 과정입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(26, now(), now(), '디자인', 'KOREAN', 26, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(26, now(), now(), '웹사이트 design을 새롭게 변경했습니다.', 26, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(27, now(), now(), 0, 'DEVELOP', 'default', 0, '기본 설정이나 초기값을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(27, now(), now(), '디폴트', 'KOREAN', 27, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(27, now(), now(), 'default 설정으로 프로그램을 실행했습니다.', 27, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(28, now(), now(), 0, 'DEVELOP', 'decode', 0, '암호화된 데이터를 해독하는 과정을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(28, now(), now(), '디코딩', 'KOREAN', 28, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(28, now(), now(), '비디오 스트림을 decode하여 재생했습니다.', 28, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(29, now(), now(), 0, 'DEVELOP', 'debounce', 0, '입력 신호의 잡음을 제거하는 기술입니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(29, now(), now(), '디바운스', 'KOREAN', 29, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(29, now(), now(), '버튼 클릭 시 debounce 처리를 적용했습니다.', 29, false); + +INSERT INTO words(id, created_at, updated_at, bookmark_count, category, name, view_count, meaning, deleted) +VALUES(30, now(), now(), 0, 'DEVELOP', 'display', 0, '화면에 정보를 출력하는 장치나 기능을 의미합니다.', false); + +INSERT INTO pronunciations(id, created_at, updated_at, content, pronunciation_type, word_id, deleted) +VALUES(30, now(), now(), '디스플레이', 'KOREAN', 30, false); + +INSERT INTO word_examples(id, created_at, updated_at, content, word_id, deleted) +VALUES(30, now(), now(), '고해상도 display를 사용하여 화면을 선명하게 표현했습니다.', 30, false);