diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/Course.java index cf27ce471..21a4fe0f0 100644 --- a/src/main/java/nextstep/courses/domain/Course.java +++ b/src/main/java/nextstep/courses/domain/Course.java @@ -61,4 +61,8 @@ public String toString() { ", updatedAt=" + updatedAt + '}'; } + + public Long getId() { + return this.id; + } } diff --git a/src/main/java/nextstep/courses/domain/CourseRepository.java b/src/main/java/nextstep/courses/domain/CourseRepository.java index 6aaeb638d..ef3e5c46f 100644 --- a/src/main/java/nextstep/courses/domain/CourseRepository.java +++ b/src/main/java/nextstep/courses/domain/CourseRepository.java @@ -1,7 +1,7 @@ package nextstep.courses.domain; public interface CourseRepository { - int save(Course course); + long save(Course course); Course findById(Long id); } diff --git a/src/main/java/nextstep/courses/domain/RegisteredUsers.java b/src/main/java/nextstep/courses/domain/RegisteredUsers.java index 630ac7d1b..5a19c9087 100644 --- a/src/main/java/nextstep/courses/domain/RegisteredUsers.java +++ b/src/main/java/nextstep/courses/domain/RegisteredUsers.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.stream.Stream; /** * 수강신청한 사용자들 목록 @@ -24,4 +25,12 @@ public void add(NsUser user) { public int theNumberOfUsers() { return this.users.size(); } -} + + public Stream stream() { + return this.users.stream(); + } + + public List toList() { + return List.copyOf(this.users); + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index c513d1468..dd52b9a08 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -1,6 +1,6 @@ package nextstep.courses.domain; -import nextstep.courses.type.InfinitablePositiveInteger; +import nextstep.courses.type.MaxRegister; import nextstep.courses.type.SessionDuration; import nextstep.courses.type.SessionState; import nextstep.payments.domain.Payment; @@ -24,48 +24,49 @@ public class Session { private SessionImage coverImage; private SessionDuration duration; - private InfinitablePositiveInteger maxUserCount; + private MaxRegister maxUserCount; private int fee; + private Session() { } - public static Session createFreeSession(Long id, Course course, SessionImage coverImage, SessionDuration duration) { + public static Session create(Long id, Course course, SessionState state, RegisteredUsers registeredUsers, SessionImage coverImage, SessionDuration duration, MaxRegister maxUserCount, int fee) { Session session = new Session(); session.id = id; session.course = course; - session.state = READY; - session.registeredUsers = new RegisteredUsers(); + session.state = state; + session.registeredUsers = registeredUsers; session.coverImage = coverImage; session.duration = duration; - session.maxUserCount = InfinitablePositiveInteger.infinite(); - session.fee = 0; + session.maxUserCount = maxUserCount; + session.fee = fee; validateSession(session); return session; } - public static Session createPaidSession(Long id, Course course, SessionImage coverImage, SessionDuration duration, InfinitablePositiveInteger maxUserCount, int fee) { - Session session = new Session(); - - session.id = id; - session.course = course; - session.state = READY; - session.registeredUsers = new RegisteredUsers(); - session.coverImage = coverImage; - session.duration = duration; - session.maxUserCount = maxUserCount; - session.fee = fee; + public static Session createFreeSession(Long id, Course course, SessionImage coverImage, SessionDuration duration) { + return Session.create(id, course, READY, new RegisteredUsers(), coverImage, duration, MaxRegister.infinite(), 0); + } - validateSession(session); - return session; + public static Session createPaidSession(Long id, Course course, SessionImage coverImage, SessionDuration duration, MaxRegister maxUserCount, int fee) { + return Session.create(id, course, READY, new RegisteredUsers(), coverImage, duration, maxUserCount, fee); } private static void validateSession(Session session) { validateFee(session.fee); + validateMaxUserCount(session.maxUserCount, session.fee); } + + private static void validateMaxUserCount(MaxRegister maxUserCount, int fee) { + if (fee == 0 && maxUserCount.isFinite()) { + throw new IllegalArgumentException("무료 강의는 수강 제한 인원을 둘 수 없습니다."); + } + } + private static void validateFee(int fee) { if (fee < 0) { throw new IllegalArgumentException("강의료가 음수일 수 없습니다."); @@ -88,15 +89,15 @@ public void registerUser(NsUser user) { } public void registerUser(NsUser user, Payment payment) { - if (payment.isSameUser(user) == false) { + if (!payment.isSameUser(user)) { throw new IllegalArgumentException("지불 정보와 등록하는 유저가 일치하지 않습니다."); } - if (payment.isSameAmountWith(this.fee) == false) { + if (!payment.isSameAmountWith(this.fee)) { throw new IllegalArgumentException("수강료와 지불 금액이 동일하지 않습니다."); } - if (this.maxUserCount.isLargerThan(this.registeredUsers.theNumberOfUsers()) == false) { + if (!this.maxUserCount.isLargerThan(this.registeredUsers.theNumberOfUsers())) { throw new IllegalStateException("이 강의의 최대 등록 가능 인원에 도달했습니다. 더 이상 사용자를 추가할 수 없습니다."); } @@ -116,6 +117,31 @@ public Long getId() { return this.id; } + + public Course getCourse() { + return course; + } + + public SessionState getState() { + return state; + } + + public SessionImage getCoverImage() { + return coverImage; + } + + public SessionDuration getDuration() { + return duration; + } + + public MaxRegister getMaxUserCount() { + return maxUserCount; + } + + public int getFee() { + return fee; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -141,4 +167,8 @@ public String toString() { ", fee=" + fee + '}'; } + + public RegisteredUsers getRegisteredUsers() { + return this.registeredUsers; + } } \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/SessionImage.java b/src/main/java/nextstep/courses/domain/SessionImage.java index 52c3f86bc..b45b83e88 100644 --- a/src/main/java/nextstep/courses/domain/SessionImage.java +++ b/src/main/java/nextstep/courses/domain/SessionImage.java @@ -3,11 +3,13 @@ import nextstep.courses.type.*; public class SessionImage { - private static final Capacity MAX_CAPACITY = new Capacity(1024, CapacityUnit.KB); - private static final Rectangle MIN_SIZE = new Rectangle(300, 200, LengthUnit.PIXEL); + private static final Capacity MAX_CAPACITY = new Capacity(1024); + private static final Rectangle MIN_SIZE = new Rectangle(300, 200); private static final int WIDTH_RATIO = 3; private static final int HEIGHT_RATIO = 2; + private String filePath; + /** * 이미지 용량 */ @@ -18,22 +20,32 @@ public class SessionImage { */ private Rectangle size; - private ImageExtension type; - public SessionImage(Capacity capacity, Rectangle size, ImageExtension type) { + public SessionImage(String filePath, Capacity capacity, Rectangle size, ImageExtension type) { validateCapacity(capacity); validateSize(size); + this.filePath = filePath; this.capacity = capacity; this.size = size; this.type = type; } + public SessionImage(String filePath, int capacity, int width, int height, ImageExtension type) { + this( + filePath, + new Capacity(capacity), + new Rectangle(width, height), + type + ); + } + public SessionImage(int capacity, int width, int height, ImageExtension type) { this( - new Capacity(capacity, CapacityUnit.KB), - new Rectangle(width, height, LengthUnit.PIXEL), + "", + new Capacity(capacity), + new Rectangle(width, height), type ); } @@ -47,4 +59,19 @@ private static void validateSize(Rectangle size) { size.throwIfRatioIsNotTheSameWith(WIDTH_RATIO, HEIGHT_RATIO); } -} + public String getFilePath() { + return filePath; + } + + public Capacity getCapacity() { + return capacity; + } + + public Rectangle getSize() { + return size; + } + + public ImageExtension getType() { + return type; + } +} \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/SessionRepository.java b/src/main/java/nextstep/courses/domain/SessionRepository.java new file mode 100644 index 000000000..f9d086f44 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/SessionRepository.java @@ -0,0 +1,7 @@ +package nextstep.courses.domain; + +public interface SessionRepository { + long save(Session session); + + Session findById(Long id); +} diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java index f9122cbe3..0b0ad8e42 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java @@ -2,10 +2,16 @@ import nextstep.courses.domain.Course; import nextstep.courses.domain.CourseRepository; +import nextstep.courses.infrastructure.exception.DbInsertFailException; +import nextstep.qna.CannotDeleteException; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import java.sql.PreparedStatement; +import java.sql.SQLException; import java.sql.Timestamp; import java.time.LocalDateTime; @@ -18,9 +24,22 @@ public JdbcCourseRepository(JdbcOperations jdbcTemplate) { } @Override - public int save(Course course) { + public long save(Course course) { String sql = "insert into course (title, creator_id, created_at) values(?, ?, ?)"; - return jdbcTemplate.update(sql, course.getTitle(), course.getCreatorId(), course.getCreatedAt()); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, course.getTitle()); + ps.setLong(2, course.getCreatorId()); + ps.setTimestamp(3, Timestamp.valueOf(course.getCreatedAt())); + return ps; + }, keyHolder); + + if (keyHolder.getKey() == null) { + throw new DbInsertFailException("Course"); + } + return keyHolder.getKey().longValue(); } @Override diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java new file mode 100644 index 000000000..ece2dc894 --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java @@ -0,0 +1,215 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.*; +import nextstep.courses.infrastructure.exception.DbInsertFailException; +import nextstep.courses.type.ImageExtension; +import nextstep.courses.type.MaxRegister; +import nextstep.courses.type.SessionDuration; +import nextstep.courses.type.SessionState; +import nextstep.users.domain.NsUser; +import nextstep.users.domain.UserRepository; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.stereotype.Repository; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Repository("sessionRepository") +public class JdbcSessionRepository implements SessionRepository { + private final JdbcOperations jdbcTemplate; + + private final CourseRepository courseRepository; + private final UserRepository userRepository; + + public JdbcSessionRepository(JdbcOperations jdbcTemplate, CourseRepository courseRepository, UserRepository userRepository) { + this.jdbcTemplate = jdbcTemplate; + this.courseRepository = courseRepository; + this.userRepository = userRepository; + } + + @Override + public long save(Session session) { + String sql = "insert into session(\n" + + " state,\n" + + " course_id,\n" + + " cover_image_file_path, image_type, image_capacity, image_width, image_height,\n" + + " start_time, end_time,\n" + + " max_user_count,\n" + + " fee\n" + + ")\n" + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + RawSession db = new RawSession(session); + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, db.state()); + ps.setLong(2, db.courseId()); + ps.setString(3, db.coverImageFilePath()); + ps.setString(4, db.imageType()); + ps.setInt(5, db.imageCapacity()); + ps.setInt(6, db.imageWidth()); + ps.setInt(7, db.imageHeight()); + ps.setTimestamp(8, Timestamp.valueOf(db.startTime())); + ps.setTimestamp(9, Timestamp.valueOf(db.endTime())); + ps.setInt(10, db.maxUserCount()); + ps.setInt(11, db.fee()); + return ps; + }, keyHolder); + + if (keyHolder.getKey() == null) { + throw new DbInsertFailException("Session"); + } + long pk = keyHolder.getKey().longValue(); + + session.getRegisteredUsers().stream().forEach(user -> { + saveRegisteredUserRelation(pk, user); + }); + + return pk; + } + + private long saveRegisteredUserRelation(long sessionPk, NsUser user) { + String sql = "insert into user_session_registration(user_id, session_id) values (?, ?);"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setLong(1, user.getId()); + ps.setLong(2, sessionPk); + return ps; + }, keyHolder); + + if (keyHolder.getKey() == null) { + throw new DbInsertFailException("user_session_registration"); + } + return keyHolder.getKey().longValue(); + } + + private List findRegisterUsers(long sessionPk) { + String sql = "select user_id from user_session_registration where session_id = ?"; + + return jdbcTemplate.query(sql, rs -> { + List idList = new ArrayList<>(); + while (rs.next()) { + idList.add(rs.getLong(1)); + } + return idList; + }, sessionPk); + } + + @Override + public Session findById(Long id) { + String sql = "select id, course_id, state, cover_image_file_path, image_type, image_capacity, image_width, image_height, start_time, end_time, fee\n" + + "from session\n" + + "where id = ?"; + + RowMapper rowMapper = (rs, rowNum) -> { + long pk = rs.getLong(1); + + ImageExtension extension = ImageExtension.valueOf(rs.getString(5)); + MaxRegister maxUserCount = MaxRegister.of(rs.getInt(11)); + if(rs.wasNull()) { + maxUserCount = MaxRegister.infinite(); + } + + RegisteredUsers registeredUsers = new RegisteredUsers(); + for (long userPk : findRegisterUsers(pk)) { + NsUser user = userRepository.findById(userPk); + registeredUsers.add(user); + } + + return Session.create( + pk, + courseRepository.findById(rs.getLong(2)), + SessionState.valueOf(rs.getString(3)), + registeredUsers, + new SessionImage(rs.getString(4), rs.getInt(6), rs.getInt(7), rs.getInt(8), extension), + new SessionDuration(toLocalDateTime(rs.getTimestamp(9)), toLocalDateTime(rs.getTimestamp(10))), + maxUserCount, + rs.getInt(11) + ); + }; + return jdbcTemplate.queryForObject(sql, rowMapper, id); + } + + private LocalDateTime toLocalDateTime(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime(); + } +} + +/** + * 세션 객체를 DB 레코드로 매핑하는 책임을 가진 객체 + * 객체를 탐색하며 DB에 매핑될 필드를 찾아냅니다. + */ +class RawSession { + private final Session original; + + public RawSession(Session original) { + this.original = original; + } + + public Long id() { + return original.getId(); + } + + public Long courseId() { + return original.getCourse().getId(); + } + + public String state() { + return original.getState().toString(); + } + + public String coverImageFilePath() { + return original.getCoverImage().getFilePath(); + } + + public String imageType() { + return original.getCoverImage().getType().toString(); + } + + public int imageCapacity() { + return original.getCoverImage().getCapacity().toInt(); + } + + public int imageWidth() { + return original.getCoverImage().getSize().getWidth(); + } + + public int imageHeight() { + return original.getCoverImage().getSize().getHeight(); + } + + public LocalDateTime startTime() { + return original.getDuration().getStartTime(); + } + + public LocalDateTime endTime() { + return original.getDuration().getEndTime(); + + } + + public Integer maxUserCount() { + if (original.getMaxUserCount().isInfinite()) { + return null; + } + + return original.getMaxUserCount().toInt(); + } + + public int fee() { + return original.getFee(); + } + +} diff --git a/src/main/java/nextstep/courses/infrastructure/exception/DbInsertFailException.java b/src/main/java/nextstep/courses/infrastructure/exception/DbInsertFailException.java new file mode 100644 index 000000000..619af3106 --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/exception/DbInsertFailException.java @@ -0,0 +1,7 @@ +package nextstep.courses.infrastructure.exception; + +public class DbInsertFailException extends RuntimeException { + public DbInsertFailException(String entityName) { + super(entityName + "의 DB 삽입이 실패했습니다."); + } +} diff --git a/src/main/java/nextstep/courses/type/Capacity.java b/src/main/java/nextstep/courses/type/Capacity.java index ae25da65b..7784ab599 100644 --- a/src/main/java/nextstep/courses/type/Capacity.java +++ b/src/main/java/nextstep/courses/type/Capacity.java @@ -4,17 +4,16 @@ /** * 파일의 용량을 나타냅니다. + * 단위는 KB입니다. * 불변객체입니다. */ public class Capacity { private final int capacity; - private final CapacityUnit unit; - public Capacity(int capacity, CapacityUnit unit) { + public Capacity(int capacity) { validateCapacity(capacity); this.capacity = capacity; - this.unit = unit; } private static void validateCapacity(int capacity) { @@ -24,12 +23,8 @@ private static void validateCapacity(int capacity) { } public void throwIfCapacityIsLagerThan(Capacity capacity) { - if (this.unit != capacity.unit) { - throw new IllegalArgumentException("단위가 불일치하여 비교할 수 없습니다."); - } - if (this.capacity > capacity.capacity) { - throw new IllegalArgumentException("파일 용량이 " + capacity + unit.toSymbol() + "보다 작습니다."); + throw new IllegalArgumentException("파일 용량이 " + capacity + "KB보다 작습니다."); } } @@ -42,19 +37,22 @@ public boolean equals(Object o) { return false; } Capacity capacity1 = (Capacity) o; - return capacity == capacity1.capacity && unit == capacity1.unit; + return capacity == capacity1.capacity; } @Override public int hashCode() { - return Objects.hash(capacity, unit); + return Objects.hash(capacity); } @Override public String toString() { return "Capacity{" + - "capacity=" + capacity + - ", unit=" + unit.toSymbol() + + "capacity=" + capacity + "KB" + '}'; } + + public int toInt() { + return this.capacity; + } } diff --git a/src/main/java/nextstep/courses/type/CapacityUnit.java b/src/main/java/nextstep/courses/type/CapacityUnit.java deleted file mode 100644 index 019a70f19..000000000 --- a/src/main/java/nextstep/courses/type/CapacityUnit.java +++ /dev/null @@ -1,15 +0,0 @@ -package nextstep.courses.type; - -public enum CapacityUnit { - KB("KB"); - - private final String unitSymbol; - - CapacityUnit(String unitSymbol) { - this.unitSymbol = unitSymbol; - } - - public String toSymbol() { - return this.unitSymbol; - } -} diff --git a/src/main/java/nextstep/courses/type/LengthUnit.java b/src/main/java/nextstep/courses/type/LengthUnit.java deleted file mode 100644 index 567e1e8d8..000000000 --- a/src/main/java/nextstep/courses/type/LengthUnit.java +++ /dev/null @@ -1,15 +0,0 @@ -package nextstep.courses.type; - -public enum LengthUnit { - PIXEL("px"); - - private final String unitSymbol; - - LengthUnit(String unitSymbol) { - this.unitSymbol = unitSymbol; - } - - public String toSymbol() { - return this.unitSymbol; - } -} diff --git a/src/main/java/nextstep/courses/type/InfinitablePositiveInteger.java b/src/main/java/nextstep/courses/type/MaxRegister.java similarity index 63% rename from src/main/java/nextstep/courses/type/InfinitablePositiveInteger.java rename to src/main/java/nextstep/courses/type/MaxRegister.java index 320cf1817..bc59672a9 100644 --- a/src/main/java/nextstep/courses/type/InfinitablePositiveInteger.java +++ b/src/main/java/nextstep/courses/type/MaxRegister.java @@ -3,15 +3,15 @@ import java.util.Objects; /** - * 무한대로 값을 설정할 수 있는 0 이상의 int형입니다. + * 수강 가능 최대 인원을 나타내는 클래스로 int형을 warping합니다. * 이 클래스는 값이 무한대로 설정되었을 때의 값 비교 처리를 위해 도입되었습니다 * 불변 객체입니다. */ -public class InfinitablePositiveInteger { +public class MaxRegister { private final int value; private final boolean isInfinite; - private InfinitablePositiveInteger(int value, boolean isInfinite) { + private MaxRegister(int value, boolean isInfinite) { if (value < 0) { throw new IllegalArgumentException("0 미만의 값은 불가능합니다."); } @@ -20,12 +20,12 @@ private InfinitablePositiveInteger(int value, boolean isInfinite) { this.isInfinite = isInfinite; } - public static InfinitablePositiveInteger of(int value) { - return new InfinitablePositiveInteger(value, false); + public static MaxRegister of(int value) { + return new MaxRegister(value, false); } - public static InfinitablePositiveInteger infinite() { - return new InfinitablePositiveInteger(0, true); + public static MaxRegister infinite() { + return new MaxRegister(0, true); } public boolean isLessThan(int value) { @@ -52,13 +52,13 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - InfinitablePositiveInteger that = (InfinitablePositiveInteger) o; + MaxRegister that = (MaxRegister) o; if (this.isInfinite != that.isInfinite) { return false; } - if (this.isInfinite == true) { + if (this.isInfinite) { return true; } @@ -73,4 +73,21 @@ public int hashCode() { return Objects.hash(value); } + + public boolean isInfinite() { + return this.isInfinite; + } + + public boolean isFinite() { + return !this.isInfinite; + } + + public int toInt() { + if (this.isInfinite) { + throw new IllegalStateException("무한 상태에서는 정수값으로 바꿀 수 없습니다."); + } + + return this.value; + } + } diff --git a/src/main/java/nextstep/courses/type/Rectangle.java b/src/main/java/nextstep/courses/type/Rectangle.java index ce42e3f52..a78578a06 100644 --- a/src/main/java/nextstep/courses/type/Rectangle.java +++ b/src/main/java/nextstep/courses/type/Rectangle.java @@ -4,20 +4,19 @@ /** * 직사각형 물체의 크기를 정의합니다. + * 단위는 픽셀입니다. * 불변 객체입니다. */ public class Rectangle { private final int width; private final int height; - private final LengthUnit unit; - public Rectangle(int width, int height, LengthUnit unit) { + public Rectangle(int width, int height) { validateValue(width); validateValue(height); this.width = width; this.height = height; - this.unit = unit; } private static void validateValue(int value) { @@ -30,10 +29,6 @@ private static void validateValue(int value) { * 가로나 세로 둘 중 하나라도 주어진 크기보다 작다면 예외를 던집니다. */ public void throwIfSizeIsSmallerThan(Rectangle size) { - if (unit != size.unit) { - throw new IllegalArgumentException("비교하려는 두 대상의 단위가 다릅니다."); - } - if (width < size.width) { throw new IllegalArgumentException("이미지 가로 크기가 " + size.width + "보다 작습니다."); } @@ -66,21 +61,27 @@ public boolean equals(Object o) { return false; } Rectangle rectangle = (Rectangle) o; - return width == rectangle.width && height == rectangle.height && unit == rectangle.unit; + return width == rectangle.width && height == rectangle.height; } @Override public int hashCode() { - return Objects.hash(width, height, unit); + return Objects.hash(width, height); } @Override public String toString() { return "Rectangle{" + - "width=" + width + unit.toString() + - ", height=" + height + unit.toSymbol() + + "width=" + width + "px" + + ", height=" + height + "px" + '}'; } + public int getWidth() { + return width; + } + public int getHeight() { + return height; + } } \ No newline at end of file diff --git a/src/main/java/nextstep/courses/type/SessionDuration.java b/src/main/java/nextstep/courses/type/SessionDuration.java index d3498b6ab..8da7e48a6 100644 --- a/src/main/java/nextstep/courses/type/SessionDuration.java +++ b/src/main/java/nextstep/courses/type/SessionDuration.java @@ -10,7 +10,7 @@ public class SessionDuration { private LocalDateTime start; private LocalDateTime end; - private SessionDuration(LocalDateTime start, LocalDateTime end) { + public SessionDuration(LocalDateTime start, LocalDateTime end) { validateDuration(start, end); this.start = start; this.end = end; @@ -31,4 +31,12 @@ public static SessionDuration fromIso8601(String start, String end) { return new SessionDuration(startTime, endTime); } + + public LocalDateTime getStartTime() { + return start; + } + + public LocalDateTime getEndTime() { + return end; + } } \ No newline at end of file diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java index f56899232..85e7a09d2 100755 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -19,6 +19,7 @@ public class NsUser { private String email; + private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -80,6 +81,14 @@ public NsUser setEmail(String email) { return this; } + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + public void update(NsUser loginUser, NsUser target) { if (!matchUserId(loginUser.getUserId())) { throw new UnAuthorizedException(); @@ -154,4 +163,5 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(id); } + } diff --git a/src/main/java/nextstep/users/domain/UserRepository.java b/src/main/java/nextstep/users/domain/UserRepository.java index 745a85dc1..a0b144c17 100755 --- a/src/main/java/nextstep/users/domain/UserRepository.java +++ b/src/main/java/nextstep/users/domain/UserRepository.java @@ -3,5 +3,7 @@ import java.util.Optional; public interface UserRepository { + long save(NsUser user); + NsUser findById(long id); Optional findByUserId(String userId); } diff --git a/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java b/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java index 005f04fc5..921967ed8 100644 --- a/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java +++ b/src/main/java/nextstep/users/infrastructure/JdbcUserRepository.java @@ -1,11 +1,15 @@ package nextstep.users.infrastructure; +import nextstep.courses.infrastructure.exception.DbInsertFailException; import nextstep.users.domain.NsUser; import nextstep.users.domain.UserRepository; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import java.sql.PreparedStatement; import java.sql.Timestamp; import java.time.LocalDateTime; import java.util.Optional; @@ -32,6 +36,43 @@ public Optional findByUserId(String userId) { return Optional.of(jdbcTemplate.queryForObject(sql, rowMapper, userId)); } + @Override + public long save(NsUser user) { + String sql = "insert into ns_user(user_id, password, name, email, created_at, updated_at) values (?, ?, ?, ?, ?, ?);"; + + KeyHolder keyHolder = new GeneratedKeyHolder(); + jdbcTemplate.update(con -> { + PreparedStatement ps = con.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, user.getUserId()); + ps.setString(2, user.getPassword()); + ps.setString(3, user.getName()); + ps.setString(4, user.getEmail()); + ps.setTimestamp(5,Timestamp.valueOf(user.getCreatedAt())); + ps.setTimestamp(6, user.getUpdatedAt() == null ? null : Timestamp.valueOf(user.getUpdatedAt())); + return ps; + }, keyHolder); + + if (keyHolder.getKey() == null) { + throw new DbInsertFailException("user"); + } + + return keyHolder.getKey().longValue(); + } + + @Override + public NsUser findById(long id) { + String sql = "select id, user_id, password, name, email, created_at, updated_at from ns_user where id = ?"; + RowMapper rowMapper = (rs, rowNum) -> new NsUser( + rs.getLong(1), + rs.getString(2), + rs.getString(3), + rs.getString(4), + rs.getString(5), + toLocalDateTime(rs.getTimestamp(6)), + toLocalDateTime(rs.getTimestamp(7))); + return jdbcTemplate.queryForObject(sql, rowMapper, id); + } + private LocalDateTime toLocalDateTime(Timestamp timestamp) { if (timestamp == null) { return null; diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index 4a6bc5a66..2b5a4bbee 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,5 +1,5 @@ -INSERT INTO ns_user (id, user_id, password, name, email, created_at) values (1, 'javajigi', 'test', '자바지기', 'javajigi@slipp.net', CURRENT_TIMESTAMP()); -INSERT INTO ns_user (id, user_id, password, name, email, created_at) values (2, 'sanjigi', 'test', '산지기', 'sanjigi@slipp.net', CURRENT_TIMESTAMP()); +INSERT INTO ns_user (user_id, password, name, email, created_at) values ('javajigi', 'test', '자바지기', 'javajigi@slipp.net', CURRENT_TIMESTAMP()); +INSERT INTO ns_user (user_id, password, name, email, created_at) values ('sanjigi', 'test', '산지기', 'sanjigi@slipp.net', CURRENT_TIMESTAMP()); INSERT INTO question (id, writer_id, title, contents, created_at, deleted) VALUES (1, 1, '국내에서 Ruby on Rails와 Play가 활성화되기 힘든 이유는 뭘까?', 'Ruby on Rails(이하 RoR)는 2006년 즈음에 정말 뜨겁게 달아올랐다가 금방 가라 앉았다. Play 프레임워크는 정말 한 순간 잠시 눈에 뜨이다가 사라져 버렸다. RoR과 Play 기반으로 개발을 해보면 정말 생산성이 높으며, 웹 프로그래밍이 재미있기까지 하다. Spring MVC + JPA(Hibernate) 기반으로 진행하면 설정할 부분도 많고, 기본으로 지원하지 않는 기능도 많아 RoR과 Play에서 기본적으로 지원하는 기능을 서비스하려면 추가적인 개발이 필요하다.', CURRENT_TIMESTAMP(), false); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8d5a988c8..8387939a8 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -4,9 +4,28 @@ create table course ( creator_id bigint not null, created_at timestamp not null, updated_at timestamp, + season int, primary key (id) ); +create table session ( + id bigint generated by default as identity, + course_id bigint not null, + state varchar not null, + cover_image_file_path varchar, + image_type varchar, + image_capacity int, + image_width int, + image_height int, + start_time timestamp, + end_time timestamp, + max_user_count int, + fee int not null, + primary key (id), + foreign key (course_id) references course (id) +); + + create table ns_user ( id bigint generated by default as identity, user_id varchar(20) not null, @@ -18,6 +37,16 @@ create table ns_user ( primary key (id) ); +create table user_session_registration ( + id bigint generated by default as identity, + user_id bigint not null, + session_id bigint not null, + primary key (id), + foreign key (user_id) references ns_user (id), + foreign key (session_id) references session (id), + constraint unique_registration unique (user_id, session_id) +); + create table question ( id bigint generated by default as identity, created_at timestamp not null, @@ -47,4 +76,4 @@ create table delete_history ( created_date timestamp, deleted_by_id bigint, primary key (id) -); +); \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/SessionTest.java b/src/test/java/nextstep/courses/domain/SessionTest.java index f273c6f85..06f9cfe68 100644 --- a/src/test/java/nextstep/courses/domain/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -1,7 +1,7 @@ package nextstep.courses.domain; import nextstep.courses.type.ImageExtension; -import nextstep.courses.type.InfinitablePositiveInteger; +import nextstep.courses.type.MaxRegister; import nextstep.courses.type.SessionDuration; import nextstep.payments.domain.Payment; import nextstep.users.domain.NsUserTest; @@ -27,7 +27,7 @@ public void sampleDataSetUp() { SessionDuration duration = SessionDuration.fromIso8601("2023-12-06T10:23:10.000", "2023-12-07T10:00:00.000"); session = Session.createFreeSession(1L, sampleCourse, image, duration); - paidSession = Session.createPaidSession(2L, sampleCourse, image, duration, InfinitablePositiveInteger.of(1), 100); + paidSession = Session.createPaidSession(2L, sampleCourse, image, duration, MaxRegister.of(1), 100); } @Test diff --git a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java index f087fc0ad..4177eac0e 100644 --- a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java +++ b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java @@ -29,9 +29,9 @@ void setUp() { @Test void crud() { Course course = new Course("TDD, 클린 코드 with Java", 1L); - int count = courseRepository.save(course); - assertThat(count).isEqualTo(1); - Course savedCourse = courseRepository.findById(1L); + long pk = courseRepository.save(course); + assertThat(pk).isNotEqualTo(0); + Course savedCourse = courseRepository.findById(pk); assertThat(course.getTitle()).isEqualTo(savedCourse.getTitle()); LOGGER.debug("Course: {}", savedCourse); } diff --git a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java new file mode 100644 index 000000000..ee51dbe77 --- /dev/null +++ b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java @@ -0,0 +1,102 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.*; +import nextstep.courses.type.ImageExtension; +import nextstep.courses.type.MaxRegister; +import nextstep.courses.type.SessionDuration; +import nextstep.payments.domain.Payment; +import nextstep.users.domain.NsUser; +import nextstep.users.domain.UserRepository; +import nextstep.users.infrastructure.JdbcUserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +public class SessionRepositoryTest { + private static final Logger LOGGER = LoggerFactory.getLogger(CourseRepositoryTest.class); + + @Autowired + private JdbcTemplate jdbcTemplate; + + private CourseRepository courseRepository; + private SessionRepository sessionRepository; + private UserRepository userRepository; + + @BeforeEach + public void beforeEach() { + courseRepository = new JdbcCourseRepository(jdbcTemplate); + userRepository = new JdbcUserRepository(jdbcTemplate); + sessionRepository = new JdbcSessionRepository(jdbcTemplate, courseRepository, userRepository); + } + + @Test + void crud() { + Course course = new Course("TDD, 클린 코드 with Java", 1L); + long courseId = courseRepository.save(course); + course = courseRepository.findById(courseId); + + SessionImage coverImage = new SessionImage("/a", 500, 300, 200, ImageExtension.JPG); + SessionDuration duration = SessionDuration.fromIso8601("2023-12-11T16:56:00", "2023-12-13T12:00:00"); + Session session = Session.createPaidSession(1L, course, coverImage, duration, MaxRegister.of(10), 100); + + long pk = sessionRepository.save(session); + assertThat(pk).isNotEqualTo(0L); + + Session savedSession = sessionRepository.findById(pk); + assertThat(session).isEqualTo(savedSession); + + LOGGER.debug("Session: {}", savedSession); + } + + @Test + void embeddableTest() { + Course course = new Course("TDD, 클린 코드 with Java", 1L); + long coursePk = courseRepository.save(course); + course = courseRepository.findById(coursePk); + + SessionImage coverImage = new SessionImage("/a", 500, 300, 200, ImageExtension.JPG); + SessionDuration duration = SessionDuration.fromIso8601("2023-12-11T16:56:00", "2023-12-13T12:00:00"); + Session session = Session.createPaidSession(1L, course, coverImage, duration, MaxRegister.of(10), 100); + + long sessionKey = sessionRepository.save(session); + + Session foundedSession = sessionRepository.findById(sessionKey); + + assertThat(foundedSession.getCoverImage().getSize().getWidth()).isEqualTo(300); + } + + @Test + void registeredUsersTest() { + NsUser myUser1 = new NsUser(null, "mymy", "mypass", "myname", "mymail"); + NsUser myUser2 = new NsUser(null, "meme", "mepass", "mename", "memail"); + long user1Pk = userRepository.save(myUser1); + long user2Pk = userRepository.save(myUser2); + myUser1 = userRepository.findById(user1Pk); + myUser2 = userRepository.findById(user2Pk); + + Course course = new Course("TDD, 클린 코드 with Java", 1L); + long coursePk = courseRepository.save(course); + course = courseRepository.findById(coursePk); + + SessionImage coverImage = new SessionImage("/a", 500, 300, 200, ImageExtension.JPG); + SessionDuration duration = SessionDuration.fromIso8601("2023-12-11T16:56:00", "2023-12-13T12:00:00"); + Session session = Session.createPaidSession(1L, course, coverImage, duration, MaxRegister.of(10), 100); + session.registerUser(myUser1, new Payment(null, session, myUser1, 100L)); + session.registerUser(myUser2, new Payment(null, session, myUser2, 100L)); + long sessionPk = sessionRepository.save(session); + + Session savedSession = sessionRepository.findById(sessionPk); + + assertThat(savedSession.getRegisteredUsers().toList()) + .hasSameElementsAs(List.of(myUser1, myUser2)); + } +} diff --git a/src/test/java/nextstep/courses/type/CapacityTest.java b/src/test/java/nextstep/courses/type/CapacityTest.java new file mode 100644 index 000000000..2264d62ef --- /dev/null +++ b/src/test/java/nextstep/courses/type/CapacityTest.java @@ -0,0 +1,33 @@ +package nextstep.courses.type; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +public class CapacityTest { + @Test + @DisplayName("[Capacity.new] 파일 용량은 음수일 수 없음") + public void negativeTest() { + assertThatIllegalArgumentException() + .isThrownBy(() -> { + new Capacity(-1); + }); + } + + @Test + @DisplayName("[Capacity.throwIfCapacityIsLagerThan] 주어진 숫자보다 용량이 크면 -> 예외 던짐") + public void throwIfCapacityIsLagerThanTest() { + Capacity capacity = new Capacity(50); + assertThatIllegalArgumentException() + .isThrownBy(() -> { + capacity.throwIfCapacityIsLagerThan(new Capacity(49)); + }); + + assertThatCode(() -> { + capacity.throwIfCapacityIsLagerThan(new Capacity(50)); + }) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/type/MaxRegisterTest.java b/src/test/java/nextstep/courses/type/MaxRegisterTest.java new file mode 100644 index 000000000..3b358f8b1 --- /dev/null +++ b/src/test/java/nextstep/courses/type/MaxRegisterTest.java @@ -0,0 +1,45 @@ +package nextstep.courses.type; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +public class MaxRegisterTest { + @Test + @DisplayName("[MaxRegister.of] 음수 불가능") + public void negativeTest() { + assertThatIllegalArgumentException() + .isThrownBy(() -> { + MaxRegister.of(-1); + }); + } + + @Test + @DisplayName("[MaxRegister.isLessThan] 유한 lessThan 테스트") + public void finiteLessThan() { + assertThat(MaxRegister.of(10).isLessThan(11)).isTrue(); + assertThat(MaxRegister.of(10).isLessThan(10)).isFalse(); + } + + @Test + @DisplayName("[MaxRegister.isLessThan] 무한 lessThan 테스트. 무한은 그 어떤 수보다도 크기 때문에 false 반환") + public void infiniteLessThan() { + assertThat(MaxRegister.infinite().isLessThan(Integer.MAX_VALUE)).isFalse(); + } + + @Test + @DisplayName("[MaxRegister.isLargerThan] 유한 largerThan 테스트") + public void finiteLargerThan() { + assertThat(MaxRegister.of(10).isLargerThan(9)).isTrue(); + assertThat(MaxRegister.of(10).isLargerThan(10)).isFalse(); + } + + @Test + @DisplayName("[MaxRegister.isLargerThan] 무한 largerThan 테스트. 무한은 그 어떤 수보다도 크기 때문에 true 반환") + public void infiniteLargerThan() { + assertThat(MaxRegister.infinite().isLargerThan(Integer.MAX_VALUE)).isTrue(); + } +}