diff --git a/build.gradle b/build.gradle index 0bb89395..0943c572 100644 --- a/build.gradle +++ b/build.gradle @@ -76,17 +76,19 @@ jacocoTestReport { afterEvaluate { classDirectories.setFrom( - files(classDirectories.files.collect { - fileTree(dir: it, excludes: [ - "**/*Application*", - "**/*Config*", - "**/*Request*", - "**/*Response*", - "**/*Exception*", - "**/*Mapper*", - "**/*ErrorMessage*", - ] + Qdomains) - }) + files(classDirectories.files.collect { + fileTree(dir: it, excludes: [ + "**/*Application*", + "**/*Config*", + "**/*Request*", + "**/*Response*", + "**/*Exception*", + "**/*Mapper*", + "**/*ErrorMessage*", + "**/*DynamicQuery*", + "**/*BaseTimeEntity*", + ] + Qdomains) + }) ) } } @@ -115,8 +117,8 @@ sonar { property "sonar.host.url", "https://sonarcloud.io" property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml' property 'sonar.coverage.exclusions', '**/test/**, **/Q*.java, **/*Doc*.java, **/resources/** ' + - ',**/*Application*.java , **/*Config*.java, **/*Request*.java, **/*Response*.java ,**/*Exception*.java ' + - ',**/*ErrorMessage*.java, **/*Mapper*.java' + ',**/*Application*.java , **/*Config*.java, **/*Request*.java, **/*Response*.java ,**/*Exception*.java ' + + ',**/*ErrorMessage*.java, **/*Mapper*.java' property 'sonar.java.checkstyle.reportPaths', 'build/reports/checkstyle/main.xml' } } diff --git a/src/main/java/com/moabam/api/application/AuthenticationService.java b/src/main/java/com/moabam/api/application/AuthenticationService.java index bd383e95..7c407afc 100644 --- a/src/main/java/com/moabam/api/application/AuthenticationService.java +++ b/src/main/java/com/moabam/api/application/AuthenticationService.java @@ -9,7 +9,7 @@ import org.springframework.web.util.UriComponentsBuilder; import com.moabam.api.dto.AuthorizationCodeRequest; -import com.moabam.api.mapper.OAuthMapper; +import com.moabam.api.dto.OAuthMapper; import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.config.OAuthConfig; import com.moabam.global.error.exception.BadRequestException; diff --git a/src/main/java/com/moabam/api/application/RoomService.java b/src/main/java/com/moabam/api/application/RoomService.java new file mode 100644 index 00000000..b16e6a06 --- /dev/null +++ b/src/main/java/com/moabam/api/application/RoomService.java @@ -0,0 +1,65 @@ +package com.moabam.api.application; + +import static com.moabam.global.error.model.ErrorMessage.*; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.moabam.api.domain.entity.Participant; +import com.moabam.api.domain.entity.Room; +import com.moabam.api.domain.entity.Routine; +import com.moabam.api.domain.repository.ParticipantRepository; +import com.moabam.api.domain.repository.ParticipantSearchRepository; +import com.moabam.api.domain.repository.RoomRepository; +import com.moabam.api.domain.repository.RoutineRepository; +import com.moabam.api.dto.CreateRoomRequest; +import com.moabam.api.dto.ModifyRoomRequest; +import com.moabam.api.dto.RoomMapper; +import com.moabam.global.error.exception.ForbiddenException; +import com.moabam.global.error.exception.NotFoundException; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class RoomService { + + private final RoomRepository roomRepository; + private final RoutineRepository routineRepository; + private final ParticipantRepository participantRepository; + private final ParticipantSearchRepository participantSearchRepository; + + @Transactional + public void createRoom(Long memberId, CreateRoomRequest createRoomRequest) { + Room room = RoomMapper.toRoomEntity(createRoomRequest); + List routines = RoomMapper.toRoutineEntity(room, createRoomRequest.routines()); + Participant participant = Participant.builder() + .room(room) + .memberId(memberId) + .build(); + participant.enableManager(); + roomRepository.save(room); + routineRepository.saveAll(routines); + participantRepository.save(participant); + } + + @Transactional + public void modifyRoom(Long memberId, Long roomId, ModifyRoomRequest modifyRoomRequest) { + // TODO: 추후에 별도 메서드로 뺄듯 + Participant participant = participantSearchRepository.findParticipant(roomId, memberId) + .orElseThrow(() -> new NotFoundException(PARTICIPANT_NOT_FOUND)); + + if (!participant.isManager()) { + throw new ForbiddenException(ROOM_MODIFY_UNAUTHORIZED_REQUEST); + } + + Room room = roomRepository.findById(roomId).orElseThrow(() -> new NotFoundException(ROOM_NOT_FOUND)); + room.changeTitle(modifyRoomRequest.title()); + room.changePassword(modifyRoomRequest.password()); + room.changeCertifyTime(modifyRoomRequest.certifyTime()); + room.changeMaxCount(modifyRoomRequest.maxUserCount()); + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Certification.java b/src/main/java/com/moabam/api/domain/entity/Certification.java new file mode 100644 index 00000000..eeff890e --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/Certification.java @@ -0,0 +1,53 @@ +package com.moabam.api.domain.entity; + +import static java.util.Objects.*; + +import com.moabam.global.common.entity.BaseTimeEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "certification") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Certification extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "routine_id", nullable = false, updatable = false) + private Routine routine; + + @Column(name = "member_id", nullable = false, updatable = false) + private Long memberId; + + @Column(name = "image", nullable = false) + private String image; + + @Builder + private Certification(Long id, Routine routine, Long memberId, String image) { + this.id = id; + this.routine = requireNonNull(routine); + this.memberId = requireNonNull(memberId); + this.image = requireNonNull(image); + } + + public void changeImage(String image) { + this.image = image; + } +} diff --git a/src/main/java/com/moabam/api/domain/Member.java b/src/main/java/com/moabam/api/domain/entity/Member.java similarity index 96% rename from src/main/java/com/moabam/api/domain/Member.java rename to src/main/java/com/moabam/api/domain/entity/Member.java index c2e3701e..a0a9e749 100644 --- a/src/main/java/com/moabam/api/domain/Member.java +++ b/src/main/java/com/moabam/api/domain/entity/Member.java @@ -1,4 +1,4 @@ -package com.moabam.api.domain; +package com.moabam.api.domain.entity; import static java.util.Objects.*; @@ -8,6 +8,7 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; +import com.moabam.api.domain.entity.enums.Role; import com.moabam.global.common.entity.BaseTimeEntity; import com.moabam.global.common.util.BaseImageUrl; diff --git a/src/main/java/com/moabam/api/domain/entity/Participant.java b/src/main/java/com/moabam/api/domain/entity/Participant.java new file mode 100644 index 00000000..733585cd --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/Participant.java @@ -0,0 +1,71 @@ +package com.moabam.api.domain.entity; + +import static java.util.Objects.*; + +import java.time.LocalDateTime; + +import org.hibernate.annotations.SQLDelete; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "participant") +@SQLDelete(sql = "UPDATE participant SET deleted_at = CURRENT_TIMESTAMP where id = ?") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Participant { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", updatable = false, nullable = false) + private Room room; + + @Column(name = "member_id", updatable = false, nullable = false) + private Long memberId; + + @Column(name = "is_manager") + private boolean isManager; + + @Column(name = "certify_count") + private int certifyCount; + + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + @Builder + private Participant(Long id, Room room, Long memberId) { + this.id = id; + this.room = requireNonNull(room); + this.memberId = requireNonNull(memberId); + this.isManager = false; + this.certifyCount = 0; + } + + public void disableManager() { + this.isManager = false; + } + + public void enableManager() { + this.isManager = true; + } + + public void updateCertifyCount() { + this.certifyCount += 1; + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Room.java b/src/main/java/com/moabam/api/domain/entity/Room.java new file mode 100644 index 00000000..d4ad6c6b --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/Room.java @@ -0,0 +1,134 @@ +package com.moabam.api.domain.entity; + +import static com.moabam.api.domain.entity.enums.RoomType.*; +import static com.moabam.global.error.model.ErrorMessage.*; +import static java.util.Objects.*; + +import org.hibernate.annotations.ColumnDefault; + +import com.moabam.api.domain.entity.enums.RoomType; +import com.moabam.global.common.entity.BaseTimeEntity; +import com.moabam.global.error.exception.BadRequestException; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "room") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Room extends BaseTimeEntity { + + private static final String ROOM_LEVEL_0_IMAGE = "'temptemp'"; + private static final String ROOM_LEVEL_10_IMAGE = "'temp'"; + private static final String ROOM_LEVEL_20_IMAGE = "'tempp'"; + private static final int MORNING_START_TIME = 4; + private static final int MORNING_END_TIME = 10; + private static final int NIGHT_START_TIME = 20; + private static final int NIGHT_END_TIME = 2; + private static final int CLOCK_ZERO = 0; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + // TODO: 한글 10자도 맞나? + @Column(name = "title", nullable = false, length = 30) + private String title; + + @Column(name = "password", length = 8) + private String password; + + @Column(name = "level", nullable = false) + private int level; + + @Enumerated(value = EnumType.STRING) + @Column(name = "room_type") + private RoomType roomType; + + @Column(name = "certify_time", nullable = false) + private int certifyTime; + + @Column(name = "current_user_count", nullable = false) + private int currentUserCount; + + @Column(name = "max_user_count", nullable = false) + private int maxUserCount; + + // TODO: 한글 길이 고려 + @Column(name = "announcement", length = 255) + private String announcement; + + @ColumnDefault(ROOM_LEVEL_0_IMAGE) + @Column(name = "room_image", length = 500) + private String roomImage; + + @Builder + private Room(Long id, String title, String password, RoomType roomType, int certifyTime, int maxUserCount) { + this.id = id; + this.title = requireNonNull(title); + this.password = password; + this.level = 0; + this.roomType = requireNonNull(roomType); + this.certifyTime = validateCertifyTime(roomType, certifyTime); + this.currentUserCount = 1; + this.maxUserCount = maxUserCount; + this.roomImage = ROOM_LEVEL_0_IMAGE; + } + + public void levelUp() { + this.level += 1; + } + + public void changeAnnouncement(String announcement) { + this.announcement = announcement; + } + + public void changeTitle(String title) { + this.title = title; + } + + public void changePassword(String password) { + this.password = password; + } + + public void changeMaxCount(int maxUserCount) { + if (maxUserCount < this.currentUserCount) { + throw new BadRequestException(ROOM_MAX_USER_COUNT_MODIFY_FAIL); + } + + this.maxUserCount = maxUserCount; + } + + public void upgradeRoomImage(String roomImage) { + this.roomImage = roomImage; + } + + public void changeCertifyTime(int certifyTime) { + this.certifyTime = validateCertifyTime(this.roomType, certifyTime); + } + + private int validateCertifyTime(RoomType roomType, int certifyTime) { + if (roomType.equals(MORNING) && (certifyTime < MORNING_START_TIME || certifyTime > MORNING_END_TIME)) { + throw new BadRequestException(INVALID_REQUEST_FIELD); + } + + if (roomType.equals(NIGHT) + && ((certifyTime < NIGHT_START_TIME && certifyTime > NIGHT_END_TIME) || certifyTime < CLOCK_ZERO)) { + throw new BadRequestException(INVALID_REQUEST_FIELD); + } + + return certifyTime; + } +} diff --git a/src/main/java/com/moabam/api/domain/entity/Routine.java b/src/main/java/com/moabam/api/domain/entity/Routine.java new file mode 100644 index 00000000..26015bb2 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/Routine.java @@ -0,0 +1,49 @@ +package com.moabam.api.domain.entity; + +import static java.util.Objects.*; + +import com.moabam.global.common.entity.BaseTimeEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "routine") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Routine extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false, updatable = false) + private Room room; + + @Column(name = "content", nullable = false, length = 60) + private String content; + + @Builder + private Routine(Long id, Room room, String content) { + this.id = id; + this.room = requireNonNull(room); + this.content = requireNonNull(content); + } + + public void changeContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/moabam/api/domain/Role.java b/src/main/java/com/moabam/api/domain/entity/enums/Role.java similarity index 50% rename from src/main/java/com/moabam/api/domain/Role.java rename to src/main/java/com/moabam/api/domain/entity/enums/Role.java index 65cb1a49..b90adc44 100644 --- a/src/main/java/com/moabam/api/domain/Role.java +++ b/src/main/java/com/moabam/api/domain/entity/enums/Role.java @@ -1,4 +1,4 @@ -package com.moabam.api.domain; +package com.moabam.api.domain.entity.enums; public enum Role { diff --git a/src/main/java/com/moabam/api/domain/entity/enums/RoomType.java b/src/main/java/com/moabam/api/domain/entity/enums/RoomType.java new file mode 100644 index 00000000..e9f8342a --- /dev/null +++ b/src/main/java/com/moabam/api/domain/entity/enums/RoomType.java @@ -0,0 +1,7 @@ +package com.moabam.api.domain.entity.enums; + +public enum RoomType { + + MORNING, + NIGHT +} diff --git a/src/main/java/com/moabam/api/domain/repository/CertificationRepository.java b/src/main/java/com/moabam/api/domain/repository/CertificationRepository.java new file mode 100644 index 00000000..4d794056 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/CertificationRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.Certification; + +public interface CertificationRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/domain/repository/ParticipantRepository.java b/src/main/java/com/moabam/api/domain/repository/ParticipantRepository.java new file mode 100644 index 00000000..3f0a7bfc --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/ParticipantRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.Participant; + +public interface ParticipantRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java b/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java new file mode 100644 index 00000000..e80e81b9 --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/ParticipantSearchRepository.java @@ -0,0 +1,30 @@ +package com.moabam.api.domain.repository; + +import static com.moabam.api.domain.entity.QParticipant.*; + +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import com.moabam.api.domain.entity.Participant; +import com.moabam.global.common.util.DynamicQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ParticipantSearchRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public Optional findParticipant(Long roomId, Long memberId) { + return Optional.ofNullable( + jpaQueryFactory.selectFrom(participant) + .where( + DynamicQuery.generateEq(roomId, participant.room.id::eq), + DynamicQuery.generateEq(memberId, participant.memberId::eq) + ).fetchOne() + ); + } +} diff --git a/src/main/java/com/moabam/api/domain/repository/RoomRepository.java b/src/main/java/com/moabam/api/domain/repository/RoomRepository.java new file mode 100644 index 00000000..96c8d99f --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/RoomRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.Room; + +public interface RoomRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/domain/repository/RoutineRepository.java b/src/main/java/com/moabam/api/domain/repository/RoutineRepository.java new file mode 100644 index 00000000..099e82da --- /dev/null +++ b/src/main/java/com/moabam/api/domain/repository/RoutineRepository.java @@ -0,0 +1,9 @@ +package com.moabam.api.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.moabam.api.domain.entity.Routine; + +public interface RoutineRepository extends JpaRepository { + +} diff --git a/src/main/java/com/moabam/api/dto/CreateRoomRequest.java b/src/main/java/com/moabam/api/dto/CreateRoomRequest.java new file mode 100644 index 00000000..516564ea --- /dev/null +++ b/src/main/java/com/moabam/api/dto/CreateRoomRequest.java @@ -0,0 +1,23 @@ +package com.moabam.api.dto; + +import java.util.List; + +import org.hibernate.validator.constraints.Range; + +import com.moabam.api.domain.entity.enums.RoomType; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record CreateRoomRequest( + @NotBlank String title, + @Pattern(regexp = "^(|[0-9]{4,8})$") String password, + @NotNull @Size(min = 1, max = 4) List routines, + @NotNull RoomType roomType, + @Range(min = 0, max = 23) int certifyTime, + @Range(min = 0, max = 10) int maxUserCount +) { + +} diff --git a/src/main/java/com/moabam/api/dto/ModifyRoomRequest.java b/src/main/java/com/moabam/api/dto/ModifyRoomRequest.java new file mode 100644 index 00000000..4e24341a --- /dev/null +++ b/src/main/java/com/moabam/api/dto/ModifyRoomRequest.java @@ -0,0 +1,15 @@ +package com.moabam.api.dto; + +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record ModifyRoomRequest( + @NotBlank String title, + @Pattern(regexp = "^(|[0-9]{4,8})$") String password, + @Range(min = 0, max = 23) int certifyTime, + @Range(min = 0, max = 10) int maxUserCount +) { + +} diff --git a/src/main/java/com/moabam/api/mapper/OAuthMapper.java b/src/main/java/com/moabam/api/dto/OAuthMapper.java similarity index 94% rename from src/main/java/com/moabam/api/mapper/OAuthMapper.java rename to src/main/java/com/moabam/api/dto/OAuthMapper.java index 7d16b8ef..dac14930 100644 --- a/src/main/java/com/moabam/api/mapper/OAuthMapper.java +++ b/src/main/java/com/moabam/api/dto/OAuthMapper.java @@ -1,4 +1,4 @@ -package com.moabam.api.mapper; +package com.moabam.api.dto; import com.moabam.api.dto.AuthorizationCodeRequest; import com.moabam.global.config.OAuthConfig; diff --git a/src/main/java/com/moabam/api/dto/RoomMapper.java b/src/main/java/com/moabam/api/dto/RoomMapper.java new file mode 100644 index 00000000..9ac332c2 --- /dev/null +++ b/src/main/java/com/moabam/api/dto/RoomMapper.java @@ -0,0 +1,32 @@ +package com.moabam.api.dto; + +import java.util.List; + +import com.moabam.api.domain.entity.Room; +import com.moabam.api.domain.entity.Routine; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public abstract class RoomMapper { + + public static Room toRoomEntity(CreateRoomRequest createRoomRequest) { + return Room.builder() + .title(createRoomRequest.title()) + .password(createRoomRequest.password()) + .roomType(createRoomRequest.roomType()) + .certifyTime(createRoomRequest.certifyTime()) + .maxUserCount(createRoomRequest.maxUserCount()) + .build(); + } + + public static List toRoutineEntity(Room room, List routinesRequest) { + return routinesRequest.stream() + .map(routine -> Routine.builder() + .room(room) + .content(routine) + .build()) + .toList(); + } +} diff --git a/src/main/java/com/moabam/api/presentation/RoomController.java b/src/main/java/com/moabam/api/presentation/RoomController.java new file mode 100644 index 00000000..c0edc651 --- /dev/null +++ b/src/main/java/com/moabam/api/presentation/RoomController.java @@ -0,0 +1,38 @@ +package com.moabam.api.presentation; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import com.moabam.api.application.RoomService; +import com.moabam.api.dto.CreateRoomRequest; +import com.moabam.api.dto.ModifyRoomRequest; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/rooms") +public class RoomController { + + private final RoomService roomService; + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public void createRoom(@Valid @RequestBody CreateRoomRequest createRoomRequest) { + roomService.createRoom(1L, createRoomRequest); + } + + @PutMapping("/{roomId}") + @ResponseStatus(HttpStatus.OK) + public void modifyRoom(@Valid @RequestBody ModifyRoomRequest modifyRoomRequest, + @PathVariable("roomId") Long roomId) { + roomService.modifyRoom(1L, roomId, modifyRoomRequest); + } +} diff --git a/src/main/java/com/moabam/global/error/model/ErrorMessage.java b/src/main/java/com/moabam/global/error/model/ErrorMessage.java index 26d157ea..82942270 100644 --- a/src/main/java/com/moabam/global/error/model/ErrorMessage.java +++ b/src/main/java/com/moabam/global/error/model/ErrorMessage.java @@ -8,6 +8,10 @@ public enum ErrorMessage { INVALID_REQUEST_FIELD("올바른 요청 정보가 아닙니다."), + ROOM_NOT_FOUND("존재하지 않는 방 입니다."), + ROOM_MAX_USER_COUNT_MODIFY_FAIL("잘못된 최대 인원수 설정입니다."), + ROOM_MODIFY_UNAUTHORIZED_REQUEST("방장이 아닌 사용자는 방을 수정할 수 없습니다."), + PARTICIPANT_NOT_FOUND("방에 대한 참여자의 정보가 없습니다."), LOGIN_FAILED("로그인에 실패했습니다."), REQUEST_FAILD("네트우크 접근 실패입니다."); diff --git a/src/main/resources/config b/src/main/resources/config index 90404393..8bc59e64 160000 --- a/src/main/resources/config +++ b/src/main/resources/config @@ -1 +1 @@ -Subproject commit 90404393aafb50e5650b81f6b67b69adc825e938 +Subproject commit 8bc59e6455ce1220e00acf676849951cbd935373 diff --git a/src/test/java/com/moabam/api/application/AuthenticationServiceTest.java b/src/test/java/com/moabam/api/application/AuthenticationServiceTest.java index cfebb663..20fea0af 100644 --- a/src/test/java/com/moabam/api/application/AuthenticationServiceTest.java +++ b/src/test/java/com/moabam/api/application/AuthenticationServiceTest.java @@ -21,7 +21,7 @@ import org.springframework.test.util.ReflectionTestUtils; import com.moabam.api.dto.AuthorizationCodeRequest; -import com.moabam.api.mapper.OAuthMapper; +import com.moabam.api.dto.OAuthMapper; import com.moabam.global.common.util.GlobalConstant; import com.moabam.global.config.OAuthConfig; import com.moabam.global.error.exception.BadRequestException; diff --git a/src/test/java/com/moabam/api/application/RoomServiceTest.java b/src/test/java/com/moabam/api/application/RoomServiceTest.java new file mode 100644 index 00000000..400d5cbb --- /dev/null +++ b/src/test/java/com/moabam/api/application/RoomServiceTest.java @@ -0,0 +1,83 @@ +package com.moabam.api.application; + +import static com.moabam.api.domain.entity.enums.RoomType.*; +import static org.mockito.BDDMockito.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.moabam.api.domain.entity.Participant; +import com.moabam.api.domain.entity.Room; +import com.moabam.api.domain.entity.Routine; +import com.moabam.api.domain.repository.CertificationRepository; +import com.moabam.api.domain.repository.ParticipantRepository; +import com.moabam.api.domain.repository.RoomRepository; +import com.moabam.api.domain.repository.RoutineRepository; +import com.moabam.api.dto.CreateRoomRequest; + +@ExtendWith(MockitoExtension.class) +class RoomServiceTest { + + @InjectMocks + private RoomService roomService; + + @Mock + private RoomRepository roomRepository; + + @Mock + private RoutineRepository routineRepository; + + @Mock + private CertificationRepository certificationRepository; + + @Mock + private ParticipantRepository participantRepository; + + @DisplayName("비밀번호 없는 방 생성 성공") + @Test + void create_room_no_password_success() { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "재윤과 앵맹이의 방임", null, routines, MORNING, 10, 4); + + // when + roomService.createRoom(1L, createRoomRequest); + + // then + verify(roomRepository).save(any(Room.class)); + verify(routineRepository).saveAll(ArgumentMatchers.anyList()); + verify(participantRepository).save(any(Participant.class)); + } + + @DisplayName("비밀번호 있는 방 생성 성공") + @Test + void create_room_with_password_success() { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "재윤과 앵맹이의 방임", "1234", routines, MORNING, 10, 4); + + // when + roomService.createRoom(1L, createRoomRequest); + + // then + verify(roomRepository).save(any(Room.class)); + verify(routineRepository).saveAll(ArgumentMatchers.anyList()); + verify(participantRepository).save(any(Participant.class)); + } +} diff --git a/src/test/java/com/moabam/api/domain/entity/CertificationTest.java b/src/test/java/com/moabam/api/domain/entity/CertificationTest.java new file mode 100644 index 00000000..1e14bf29 --- /dev/null +++ b/src/test/java/com/moabam/api/domain/entity/CertificationTest.java @@ -0,0 +1,41 @@ +package com.moabam.api.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.moabam.api.domain.entity.enums.RoomType; + +class CertificationTest { + + String content = "물 마시기"; + String image = "https://s3.testtest"; + + @DisplayName("Certification 생성 성공") + @Test + void create_certification_success() { + Room room = Room.builder() + .title("앵윤이의 방") + .roomType(RoomType.MORNING) + .certifyTime(10) + .maxUserCount(9) + .build(); + + Routine routine = Routine.builder() + .room(room) + .content(content) + .build(); + + assertThatNoException().isThrownBy(() -> { + Certification certification = Certification.builder() + .routine(routine) + .memberId(1L) + .image(image).build(); + + assertThat(certification.getImage()).isEqualTo(image); + assertThat(certification.getMemberId()).isEqualTo(1L); + assertThat(certification.getRoutine()).isEqualTo(routine); + }); + } +} diff --git a/src/test/java/com/moabam/api/domain/MemberTest.java b/src/test/java/com/moabam/api/domain/entity/MemberTest.java similarity index 95% rename from src/test/java/com/moabam/api/domain/MemberTest.java rename to src/test/java/com/moabam/api/domain/entity/MemberTest.java index b08dde3f..dbc695fc 100644 --- a/src/test/java/com/moabam/api/domain/MemberTest.java +++ b/src/test/java/com/moabam/api/domain/entity/MemberTest.java @@ -1,4 +1,4 @@ -package com.moabam.api.domain; +package com.moabam.api.domain.entity; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; @@ -6,6 +6,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import com.moabam.api.domain.entity.enums.Role; import com.moabam.global.common.util.BaseImageUrl; class MemberTest { diff --git a/src/test/java/com/moabam/api/domain/entity/RoomTest.java b/src/test/java/com/moabam/api/domain/entity/RoomTest.java new file mode 100644 index 00000000..d4516b62 --- /dev/null +++ b/src/test/java/com/moabam/api/domain/entity/RoomTest.java @@ -0,0 +1,92 @@ +package com.moabam.api.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import com.moabam.api.domain.entity.enums.RoomType; +import com.moabam.global.error.exception.BadRequestException; +import com.moabam.global.error.model.ErrorMessage; + +class RoomTest { + + @DisplayName("비밀번호 없이 방 생성 성공") + @Test + void create_room_without_password_success() { + // given, when + Room room = Room.builder() + .title("앵윤이의 방") + .roomType(RoomType.MORNING) + .certifyTime(10) + .maxUserCount(9) + .build(); + + // then + assertThat(room.getPassword()).isNull(); + assertThat(room.getRoomImage()).isEqualTo("'temptemp'"); + assertThat(room.getRoomType()).isEqualTo(RoomType.MORNING); + assertThat(room.getCertifyTime()).isEqualTo(10); + assertThat(room.getMaxUserCount()).isEqualTo(9); + assertThat(room.getLevel()).isZero(); + assertThat(room.getCurrentUserCount()).isEqualTo(1); + assertThat(room.getAnnouncement()).isNull(); + } + + @DisplayName("비밀번호 설정 후 방 생성 성공") + @Test + void create_room_with_password_success() { + // given, when + Room room = Room.builder() + .title("앵윤이의 방") + .password("12345") + .roomType(RoomType.MORNING) + .certifyTime(10) + .maxUserCount(9) + .build(); + + // then + assertThat(room.getPassword()).isEqualTo("12345"); + } + + @DisplayName("아침 방 설정 시, 저녁 시간이 들어오는 예외 발생") + @ParameterizedTest + @CsvSource({ + "13", "19", "3", "11", "0" + }) + void morning_time_validate_exception(int certifyTime) { + Room room = Room.builder() + .title("모아밤 짱") + .password("1234") + .roomType(RoomType.MORNING) + .certifyTime(9) + .maxUserCount(5) + .build(); + + // given, when, then + assertThatThrownBy(() -> room.changeCertifyTime(certifyTime)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_REQUEST_FIELD.getMessage()); + } + + @DisplayName("저녁 방 설정 시, 아침 시간이 들어오는 경우 예외 발생") + @ParameterizedTest + @CsvSource({ + "3", "5", "-1", "15", "8", "19" + }) + void night_time_validate_exception(int certifyTime) { + Room room = Room.builder() + .title("모아밤 짱") + .roomType(RoomType.NIGHT) + .certifyTime(21) + .maxUserCount(5) + .build(); + + // given, when, then + assertThatThrownBy(() -> room.changeCertifyTime(certifyTime)) + .isInstanceOf(BadRequestException.class) + .hasMessage(ErrorMessage.INVALID_REQUEST_FIELD.getMessage()); + } +} diff --git a/src/test/java/com/moabam/api/presentation/RoomControllerTest.java b/src/test/java/com/moabam/api/presentation/RoomControllerTest.java new file mode 100644 index 00000000..55668783 --- /dev/null +++ b/src/test/java/com/moabam/api/presentation/RoomControllerTest.java @@ -0,0 +1,287 @@ +package com.moabam.api.presentation; + +import static com.moabam.api.domain.entity.enums.RoomType.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.http.MediaType.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.moabam.api.domain.entity.Participant; +import com.moabam.api.domain.entity.Room; +import com.moabam.api.domain.repository.ParticipantRepository; +import com.moabam.api.domain.repository.RoomRepository; +import com.moabam.api.domain.repository.RoutineRepository; +import com.moabam.api.dto.CreateRoomRequest; +import com.moabam.api.dto.ModifyRoomRequest; + +@Transactional +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class RoomControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private RoomRepository roomRepository; + + @Autowired + private RoutineRepository routineRepository; + + @Autowired + private ParticipantRepository participantRepository; + + @DisplayName("비밀번호 없는 방 생성 성공") + @Test + void create_room_no_password_success() throws Exception { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "재윤과 앵맹이의 방임", null, routines, MORNING, 10, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andDo(print()); + + assertThat(roomRepository.findAll()).hasSize(1); + assertThat(roomRepository.findAll().get(0).getTitle()).isEqualTo("재윤과 앵맹이의 방임"); + assertThat(roomRepository.findAll().get(0).getPassword()).isNull(); + } + + @DisplayName("비밀번호 있는 방 생성 성공") + @ParameterizedTest + @CsvSource({ + "1234", "12345678", "98765" + }) + void create_room_with_password_success(String password) throws Exception { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "비번 있는 재윤과 앵맹이의 방임", password, routines, MORNING, 10, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isCreated()) + .andDo(print()); + + assertThat(roomRepository.findAll()).hasSize(1); + assertThat(roomRepository.findAll().get(0).getTitle()).isEqualTo("비번 있는 재윤과 앵맹이의 방임"); + assertThat(roomRepository.findAll().get(0).getPassword()).isEqualTo(password); + } + + @DisplayName("올바르지 않은 비밀번호 방 생성시 예외 발생") + @ParameterizedTest + @CsvSource({ + "1", "12", "123", "123456789", "abc" + }) + void create_room_with_wrong_password_fail(String password) throws Exception { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "비번 있는 재윤과 앵맹이의 방임", password, routines, MORNING, 10, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("Routine 갯수를 초과한 방 생성시 예외 발생") + @Test + void create_room_with_too_many_routine_fail() throws Exception { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + routines.add("밥 먹기"); + routines.add("코드 리뷰 달기"); + routines.add("책 읽기"); + routines.add("산책 하기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "비번 없는 재윤과 앵맹이의 방임", null, routines, MORNING, 10, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("Routine 없는 방 생성시 예외 발생") + @Test + void create_room_with_no_routine_fail() throws Exception { + // given + List routines = new ArrayList<>(); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "비번 없는 재윤과 앵맹이의 방임", null, routines, MORNING, 10, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("올바르지 못한 시간으로 아침 방 생성") + @ParameterizedTest + @CsvSource({ + "1", "3", "11", "12", "20" + }) + void create_morning_room_wrong_certify_time_fail(int certifyTime) throws Exception { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "비번 없는 재윤과 앵맹이의 방임", null, routines, MORNING, certifyTime, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("올바르지 못한 시간으로 저녁 방 생성시 에외 발생") + @ParameterizedTest + @CsvSource({ + "19", "3", "6", "9" + }) + void create_night_room_wrong_certify_time_fail(int certifyTime) throws Exception { + // given + List routines = new ArrayList<>(); + routines.add("물 마시기"); + routines.add("코테 풀기"); + + CreateRoomRequest createRoomRequest = new CreateRoomRequest( + "비번 없는 재윤과 앵맹이의 방임", null, routines, NIGHT, certifyTime, 4); + + String json = objectMapper.writeValueAsString(createRoomRequest); + + // expected + mockMvc.perform(post("/rooms") + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isBadRequest()) + .andDo(print()); + } + + @DisplayName("방 수정 성공 - 방장일 경우") + @Test + void modify_room_success() throws Exception { + // given + Room room = Room.builder() + .title("처음 제목") + .password("1234") + .roomType(MORNING) + .certifyTime(9) + .maxUserCount(5) + .build(); + + Participant participant = Participant.builder() + .room(room) + .memberId(1L) + .build(); + participant.enableManager(); + + Room savedRoom = roomRepository.save(room); + participantRepository.save(participant); + + ModifyRoomRequest modifyRoomRequest = new ModifyRoomRequest("수정할 방임!", "1234", 10, 7); + + String json = objectMapper.writeValueAsString(modifyRoomRequest); + + // expected + mockMvc.perform(put("/rooms/" + savedRoom.getId()) + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andDo(print()); + } + + @DisplayName("방 수정 실패 - 방장 아닐 경우") + @Test + void unauthorized_modify_room_fail() throws Exception { + // given + Room room = Room.builder() + .title("처음 제목") + .password("1234") + .roomType(MORNING) + .certifyTime(9) + .maxUserCount(5) + .build(); + + Participant participant = Participant.builder() + .room(room) + .memberId(1L) + .build(); + + Room savedRoom = roomRepository.save(room); + participantRepository.save(participant); + + ModifyRoomRequest modifyRoomRequest = new ModifyRoomRequest("수정할 방임!", "1234", 10, 7); + + String json = objectMapper.writeValueAsString(modifyRoomRequest); + + // expected + mockMvc.perform(put("/rooms/" + savedRoom.getId()) + .contentType(APPLICATION_JSON) + .content(json)) + .andExpect(status().isNotFound()) + .andDo(print()); + } +}