From a5e70745ea5bb3eccdafc9953281f560f880c59b Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Sun, 20 Apr 2025 18:45:13 +0900 Subject: [PATCH 1/9] docs: set up requirement --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 00ce3d2e4..1041e5199 100644 --- a/README.md +++ b/README.md @@ -68,4 +68,10 @@ - boolean validateFileSize() - boolean validateFileType() - boolean validateRatio() - \ No newline at end of file + + +## ๐ 4๋จ๊ณ - ์๊ฐ์ ์ฒญ(์๊ตฌ์ฌํญ ๋ณ๊ฒฝ) +- [] SessionStatus ์งํ์ํ์ ๋ชจ์ง์ํ๋ก ๋ถ๋ฆฌ +- [] coverImage ์ฌ๋ฌ๊ฐ ๊ฐ๋ฅํ๋๋ก ๊ธฐ๋ฅ ์ถ๊ฐ +- [] ์ ๋ฐ ์ฌ๋ถ ํ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ +- [] ์๊ฐ ์น์ธ ๋ฐ ์๊ฐ ์ทจ์ ๊ธฐ๋ฅ ์ถ๊ฐ \ No newline at end of file From 65672481e4169445b8f138fc22d2a75b1151408f Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Sun, 20 Apr 2025 19:00:25 +0900 Subject: [PATCH 2/9] feat: add RecruitmentStatus to `Session` --- README.md | 2 +- .../courses/domain/RecruitmentStatus.java | 6 ++ .../java/nextstep/courses/domain/Session.java | 17 +++-- .../courses/domain/SessionStatus.java | 2 +- .../infrastructure/JdbcSessionRepository.java | 15 ++-- src/main/resources/schema.sql | 1 + .../nextstep/courses/domain/SessionTest.java | 74 ++++++++++++++++++- .../nextstep/courses/domain/SessionsTest.java | 3 +- .../infrastructure/SessionRepositoryTest.java | 8 +- 9 files changed, 102 insertions(+), 26 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/RecruitmentStatus.java diff --git a/README.md b/README.md index 1041e5199..1555bb1a8 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ ## ๐ 4๋จ๊ณ - ์๊ฐ์ ์ฒญ(์๊ตฌ์ฌํญ ๋ณ๊ฒฝ) -- [] SessionStatus ์งํ์ํ์ ๋ชจ์ง์ํ๋ก ๋ถ๋ฆฌ +- [x] SessionStatus ์งํ์ํ์ ๋ชจ์ง์ํ๋ก ๋ถ๋ฆฌ - [] coverImage ์ฌ๋ฌ๊ฐ ๊ฐ๋ฅํ๋๋ก ๊ธฐ๋ฅ ์ถ๊ฐ - [] ์ ๋ฐ ์ฌ๋ถ ํ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ - [] ์๊ฐ ์น์ธ ๋ฐ ์๊ฐ ์ทจ์ ๊ธฐ๋ฅ ์ถ๊ฐ \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/RecruitmentStatus.java b/src/main/java/nextstep/courses/domain/RecruitmentStatus.java new file mode 100644 index 000000000..77e1512c2 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/RecruitmentStatus.java @@ -0,0 +1,6 @@ +package nextstep.courses.domain; + +public enum RecruitmentStatus { + NOT_RECRUITING, + RECRUITING +} diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index e7f925551..085b6c062 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -3,7 +3,6 @@ import nextstep.payments.domain.Payment; import java.time.LocalDateTime; -import java.util.regex.Pattern; public class Session { private CapacityInfo capacityInfo; @@ -12,15 +11,16 @@ public class Session { private int id; private Long tuition; private Image coverImage; + private RecruitmentStatus recruitmentStatus; private SessionStatus sessionStatus; private JoinStrategy joinStrategy; - public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Image coverImage, SessionStatus sessionStatus) { - this(title, id, startDate, endDate, tuition, currentCount, capacity, coverImage, sessionStatus, tuition == 0 ? new FreeJoinStrategy() : new PaidJoinStrategy()); + public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Image coverImage, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus) { + this(title, id, startDate, endDate, tuition, currentCount, capacity, coverImage, sessionStatus, recruitmentStatus, tuition == 0 ? new FreeJoinStrategy() : new PaidJoinStrategy()); } - public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Image coverImage, SessionStatus sessionStatus, JoinStrategy joinStrategy) { + public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Image coverImage, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus, JoinStrategy joinStrategy) { this.title = title; this.id = id; this.sessionPeriod = new SessionPeriod(startDate, endDate); @@ -28,6 +28,7 @@ public Session(String title, int id, LocalDateTime startDate, LocalDateTime endD this.capacityInfo = new CapacityInfo(currentCount, capacity); this.coverImage = coverImage; this.sessionStatus = sessionStatus; + this.recruitmentStatus = recruitmentStatus; this.joinStrategy = joinStrategy; } @@ -36,7 +37,7 @@ boolean joinable(Payment pay) { } public boolean recruiting() { - return sessionStatus == SessionStatus.RECRUITING; + return sessionStatus == SessionStatus.ONGOING && recruitmentStatus == RecruitmentStatus.RECRUITING; } public boolean underCapacity() { @@ -91,7 +92,11 @@ public Image getCoverImage() { return coverImage; } - public SessionStatus getStatus() { + public SessionStatus getSessionStatus() { return sessionStatus; } + + public RecruitmentStatus getRecruitmentStatus() { + return recruitmentStatus; + } } diff --git a/src/main/java/nextstep/courses/domain/SessionStatus.java b/src/main/java/nextstep/courses/domain/SessionStatus.java index d4ce621fa..43cdd62a8 100644 --- a/src/main/java/nextstep/courses/domain/SessionStatus.java +++ b/src/main/java/nextstep/courses/domain/SessionStatus.java @@ -2,6 +2,6 @@ public enum SessionStatus { PREPARING, - RECRUITING, + ONGOING, CLOSED } diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java index 1b692b2ac..e04fb39b0 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java @@ -1,9 +1,6 @@ package nextstep.courses.infrastructure; -import nextstep.courses.domain.Image; -import nextstep.courses.domain.Session; -import nextstep.courses.domain.SessionRepository; -import nextstep.courses.domain.SessionStatus; +import nextstep.courses.domain.*; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; @@ -22,8 +19,8 @@ public int save(Session session, Long courseId) { String sql = "insert into session " + "(title, start_date, end_date, tuition, current_count, capacity, " + "image_file_size, image_file_type, image_url, image_width, image_height, " + - "status, course_id) " + - "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + "status, recruitment_status, course_id) " + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; return jdbcTemplate.update(sql, session.getTitle(), @@ -37,7 +34,8 @@ public int save(Session session, Long courseId) { session.getCoverImage().getImageUrl(), session.getCoverImage().getWidth(), session.getCoverImage().getHeight(), - session.getStatus().name(), + session.getSessionStatus().name(), + session.getRecruitmentStatus().name(), courseId ); } @@ -65,7 +63,8 @@ private RowMapper<Session> sessionRowMapper() { rs.getInt("image_width"), rs.getInt("image_height") ), - SessionStatus.valueOf(rs.getString("status")) + SessionStatus.valueOf(rs.getString("status")), + RecruitmentStatus.valueOf(rs.getString("recruitment_status")) ); } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 04723cb30..a78c7b01d 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -27,6 +27,7 @@ CREATE TABLE session image_height INT, status VARCHAR(50) NOT NULL, + recruitment_status VARCHAR(50) NOT NULL, -- FK to course course_id BIGINT NOT NULL, diff --git a/src/test/java/nextstep/courses/domain/SessionTest.java b/src/test/java/nextstep/courses/domain/SessionTest.java index 75a9e4de7..eba2a0e63 100644 --- a/src/test/java/nextstep/courses/domain/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -24,7 +24,8 @@ void freeSession_joinable_whenRecruiting() { 0, // currentCount 0, // capacity (๋ฌด์ ํ์ด์ง๋ง ๊ทธ๋ฅ 0์ผ๋ก ๋ ) validImage, - SessionStatus.RECRUITING, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, new FreeJoinStrategy() ); @@ -44,6 +45,7 @@ void freeSession_notJoinable_whenNotRecruiting() { 0, validImage, SessionStatus.PREPARING, + RecruitmentStatus.RECRUITING, new FreeJoinStrategy() ); @@ -62,7 +64,8 @@ void paidSession_joinable_whenRecruiting_underCapacity_andPaidCorrectly() { 29, // currentCount 30, // capacity validImage, - SessionStatus.RECRUITING, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, new PaidJoinStrategy() ); @@ -81,7 +84,8 @@ void paidSession_notJoinable_whenWrongAmount() { 10, 30, validImage, - SessionStatus.RECRUITING, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, new PaidJoinStrategy() ); @@ -100,10 +104,72 @@ void paidSession_notJoinable_whenOverCapacity() { 30, 30, validImage, - SessionStatus.RECRUITING, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, new PaidJoinStrategy() ); assertThat(session.joinable(new Payment(10000L))).isFalse(); } + + @Test + @DisplayName("๊ฐ์ ์ํ๊ฐ PREPARING์ด๋ฉด ๋ชจ์ง์ค์ด์ด๋ ์๊ฐ ์ ์ฒญ ๋ถ๊ฐ") + void notJoinable_whenLecturePreparing_evenIfRecruiting() { + Session session = new Session( + "๊ฐ์ ์ค๋น ์ค", + 1, + LocalDateTime.now(), + LocalDateTime.now().plusDays(7), + 0L, + 0, + 0, + validImage, + SessionStatus.PREPARING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + assertThat(session.joinable(new Payment())).isFalse(); + } + + @Test + @DisplayName("๊ฐ์ ์ํ๊ฐ CLOSED์ด๋ฉด ๋ชจ์ง์ค์ด์ด๋ ์๊ฐ ์ ์ฒญ ๋ถ๊ฐ") + void notJoinable_whenLectureClosed_evenIfRecruiting() { + Session session = new Session( + "์ข ๋ฃ๋ ๊ฐ์", + 1, + LocalDateTime.now().minusDays(10), + LocalDateTime.now().minusDays(3), + 0L, + 0, + 0, + validImage, + SessionStatus.CLOSED, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + assertThat(session.joinable(new Payment())).isFalse(); + } + + @Test + @DisplayName("๋ชจ์ง ์ํ๊ฐ NOT_RECRUITING์ด๋ฉด ๊ฐ์๊ฐ Ongoing์ด์ด๋ ์๊ฐ ์ ์ฒญ ๋ถ๊ฐ") + void notJoinable_whenNotRecruiting() { + Session session = new Session( + "๋ชจ์ง ๋นํ์ฑ ๊ฐ์", + 1, + LocalDateTime.now(), + LocalDateTime.now().plusDays(7), + 0L, + 0, + 0, + validImage, + SessionStatus.ONGOING, + RecruitmentStatus.NOT_RECRUITING, + new FreeJoinStrategy() + ); + + assertThat(session.joinable(new Payment())).isFalse(); + } + } diff --git a/src/test/java/nextstep/courses/domain/SessionsTest.java b/src/test/java/nextstep/courses/domain/SessionsTest.java index 7b2ea5065..4a9ff3a25 100644 --- a/src/test/java/nextstep/courses/domain/SessionsTest.java +++ b/src/test/java/nextstep/courses/domain/SessionsTest.java @@ -22,7 +22,8 @@ private Session createSessionWithId(int id) { 0, 0, image, - SessionStatus.RECRUITING, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, new FreeJoinStrategy() ); } diff --git a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java index 94523c758..a2219b00e 100644 --- a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java +++ b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java @@ -1,9 +1,6 @@ package nextstep.courses.infrastructure; -import nextstep.courses.domain.Image; -import nextstep.courses.domain.Session; -import nextstep.courses.domain.SessionRepository; -import nextstep.courses.domain.SessionStatus; +import nextstep.courses.domain.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -43,7 +40,8 @@ void crud() { 0, 20, image, - SessionStatus.RECRUITING + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING ); // when From 4cc11b9542d0a7c23403cb60995804ac3f854e13 Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Mon, 21 Apr 2025 19:43:48 +0900 Subject: [PATCH 3/9] feat: add support for multiple cover images --- README.md | 2 +- .../java/nextstep/courses/domain/Image.java | 8 ++ .../java/nextstep/courses/domain/Images.java | 16 +++ .../java/nextstep/courses/domain/Session.java | 18 +-- .../courses/domain/SessionRepository.java | 2 +- .../infrastructure/JdbcSessionRepository.java | 114 ++++++++++++------ src/main/resources/schema.sql | 40 +++--- .../nextstep/courses/domain/ImagesTest.java | 44 +++++++ .../nextstep/courses/domain/SessionTest.java | 19 +-- .../nextstep/courses/domain/SessionsTest.java | 4 +- .../infrastructure/SessionRepositoryTest.java | 24 ++-- 11 files changed, 210 insertions(+), 81 deletions(-) create mode 100644 src/main/java/nextstep/courses/domain/Images.java create mode 100644 src/test/java/nextstep/courses/domain/ImagesTest.java diff --git a/README.md b/README.md index 1555bb1a8..3477a100e 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,6 @@ ## ๐ 4๋จ๊ณ - ์๊ฐ์ ์ฒญ(์๊ตฌ์ฌํญ ๋ณ๊ฒฝ) - [x] SessionStatus ์งํ์ํ์ ๋ชจ์ง์ํ๋ก ๋ถ๋ฆฌ -- [] coverImage ์ฌ๋ฌ๊ฐ ๊ฐ๋ฅํ๋๋ก ๊ธฐ๋ฅ ์ถ๊ฐ +- [x] coverImage ์ฌ๋ฌ๊ฐ ๊ฐ๋ฅํ๋๋ก ๊ธฐ๋ฅ ์ถ๊ฐ - [] ์ ๋ฐ ์ฌ๋ถ ํ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ - [] ์๊ฐ ์น์ธ ๋ฐ ์๊ฐ ์ทจ์ ๊ธฐ๋ฅ ์ถ๊ฐ \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/Image.java b/src/main/java/nextstep/courses/domain/Image.java index 4454a011f..c981bdf7c 100644 --- a/src/main/java/nextstep/courses/domain/Image.java +++ b/src/main/java/nextstep/courses/domain/Image.java @@ -6,11 +6,18 @@ public class Image { private static final Set<String> ALLOWED_FILE_FORMAT = new HashSet<>(List.of("gif", "jpg", "jpeg", "png", "svg")); + private Long id; private final File file; private String imageUrl; private int width; private int height; + // DB์ฉ ์์ฑ์ (id ํฌํจ, ๊ฒ์ฆ ์๋ต ๊ฐ๋ฅ) + public Image(Long id, float fileSize, String fileType, String imageUrl, int width, int height) { + this(fileSize, fileType, imageUrl, width, height); + this.id = id; + } + public Image(float fileSize, String fileType, String imageUrl, int width, int height) { this.file = new File(ALLOWED_FILE_FORMAT, fileSize, fileType); this.imageUrl = imageUrl; @@ -19,6 +26,7 @@ public Image(float fileSize, String fileType, String imageUrl, int width, int he validate(); } + private void validate() { validateFileSize(); validateFileType(); diff --git a/src/main/java/nextstep/courses/domain/Images.java b/src/main/java/nextstep/courses/domain/Images.java new file mode 100644 index 000000000..ef8daae52 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Images.java @@ -0,0 +1,16 @@ +package nextstep.courses.domain; + +import java.util.Collections; +import java.util.List; + +public class Images { + private List<Image> images; + + public Images(List<Image> images) { + this.images = List.copyOf(images); + } + + public List<Image> getImages() { + return Collections.unmodifiableList(images); + } +} diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index 085b6c062..84d7886de 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -10,23 +10,23 @@ public class Session { private String title; private int id; private Long tuition; - private Image coverImage; + private Images coverImages; private RecruitmentStatus recruitmentStatus; private SessionStatus sessionStatus; private JoinStrategy joinStrategy; - public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Image coverImage, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus) { - this(title, id, startDate, endDate, tuition, currentCount, capacity, coverImage, sessionStatus, recruitmentStatus, tuition == 0 ? new FreeJoinStrategy() : new PaidJoinStrategy()); + public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Images coverImages, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus) { + this(title, id, startDate, endDate, tuition, currentCount, capacity, coverImages, sessionStatus, recruitmentStatus, tuition == 0 ? new FreeJoinStrategy() : new PaidJoinStrategy()); } - public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Image coverImage, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus, JoinStrategy joinStrategy) { + public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Images coverImages, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus, JoinStrategy joinStrategy) { this.title = title; this.id = id; this.sessionPeriod = new SessionPeriod(startDate, endDate); this.tuition = tuition; this.capacityInfo = new CapacityInfo(currentCount, capacity); - this.coverImage = coverImage; + this.coverImages = coverImages; this.sessionStatus = sessionStatus; this.recruitmentStatus = recruitmentStatus; this.joinStrategy = joinStrategy; @@ -88,8 +88,12 @@ public int getCapacity() { return capacityInfo.getCapacity(); } - public Image getCoverImage() { - return coverImage; + public Images getCoverImages() { + return coverImages; + } + + public Image getMainCoverImage() { + return coverImages.getImages().get(0); } public SessionStatus getSessionStatus() { diff --git a/src/main/java/nextstep/courses/domain/SessionRepository.java b/src/main/java/nextstep/courses/domain/SessionRepository.java index f1d74ae1d..d84e70ff8 100644 --- a/src/main/java/nextstep/courses/domain/SessionRepository.java +++ b/src/main/java/nextstep/courses/domain/SessionRepository.java @@ -2,6 +2,6 @@ public interface SessionRepository { int save(Session session, Long courseId); - + void saveImage(int sessionId, Image image); Session findById(Long id); } diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java index e04fb39b0..6b854afc7 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java @@ -5,6 +5,8 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public class JdbcSessionRepository implements SessionRepository { @@ -16,30 +18,57 @@ public JdbcSessionRepository(JdbcOperations jdbcTemplate) { @Override public int save(Session session, Long courseId) { - String sql = "insert into session " + + String sql = "INSERT INTO session " + "(title, start_date, end_date, tuition, current_count, capacity, " + - "image_file_size, image_file_type, image_url, image_width, image_height, " + "status, recruitment_status, course_id) " + - "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; - - return jdbcTemplate.update(sql, - session.getTitle(), - session.getStartDate(), - session.getEndDate(), - session.getTuition(), - session.getCurrentCount(), - session.getCapacity(), - session.getCoverImage().getSize(), - session.getCoverImage().getType(), - session.getCoverImage().getImageUrl(), - session.getCoverImage().getWidth(), - session.getCoverImage().getHeight(), - session.getSessionStatus().name(), - session.getRecruitmentStatus().name(), - courseId + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + var keyHolder = new org.springframework.jdbc.support.GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + var ps = connection.prepareStatement(sql, new String[]{"id"}); + ps.setString(1, session.getTitle()); + ps.setTimestamp(2, java.sql.Timestamp.valueOf(session.getStartDate())); + ps.setTimestamp(3, java.sql.Timestamp.valueOf(session.getEndDate())); + ps.setLong(4, session.getTuition()); + ps.setInt(5, session.getCurrentCount()); + ps.setInt(6, session.getCapacity()); + ps.setString(7, session.getSessionStatus().name()); + ps.setString(8, session.getRecruitmentStatus().name()); + ps.setLong(9, courseId); + return ps; + }, keyHolder); + + var generatedId = keyHolder.getKey(); + if (generatedId == null) { + throw new IllegalStateException("Session ์ ์ฅ ์คํจ - id ์์ฑ ์คํจ"); + } + + int sessionId = generatedId.intValue(); + + // ์ด๋ฏธ์ง ์ ์ฅ + for (Image image : session.getCoverImages().getImages()) { + saveImage(sessionId, image); + } + + return sessionId; + } + + + @Override + public void saveImage(int sessionId, Image image){ + String sql = "INSERT INTO image (session_id, url, file_type, file_size, width, height) VALUES (?, ?, ?, ?, ?, ?)"; + jdbcTemplate.update(sql, + sessionId, + image.getImageUrl(), + image.getType(), + image.getSize(), + image.getWidth(), + image.getHeight() ); } + @Override public Session findById(Long id) { String sql = "select * from session where id = ?"; @@ -48,23 +77,34 @@ public Session findById(Long id) { } private RowMapper<Session> sessionRowMapper() { - return (rs, rowNum) -> new Session( - rs.getString("title"), - rs.getInt("id"), - rs.getTimestamp("start_date").toLocalDateTime(), - rs.getTimestamp("end_date").toLocalDateTime(), - rs.getLong("tuition"), - rs.getInt("current_count"), - rs.getInt("capacity"), - new Image( - rs.getFloat("image_file_size"), - rs.getString("image_file_type"), - rs.getString("image_url"), - rs.getInt("image_width"), - rs.getInt("image_height") - ), - SessionStatus.valueOf(rs.getString("status")), - RecruitmentStatus.valueOf(rs.getString("recruitment_status")) - ); + return (rs, rowNum) -> { + int sessionId = rs.getInt("id"); + + List<Image> images = jdbcTemplate.query( + "SELECT * FROM image WHERE session_id = ?", + (irs, irow) -> new Image( + irs.getLong("id"), + irs.getFloat("file_size"), + irs.getString("file_type"), + irs.getString("url"), + irs.getInt("width"), + irs.getInt("height") + ), + sessionId + ); + + return new Session( + rs.getString("title"), + sessionId, + rs.getTimestamp("start_date").toLocalDateTime(), + rs.getTimestamp("end_date").toLocalDateTime(), + rs.getLong("tuition"), + rs.getInt("current_count"), + rs.getInt("capacity"), + new Images(images), + SessionStatus.valueOf(rs.getString("status")), + RecruitmentStatus.valueOf(rs.getString("recruitment_status")) + ); + }; } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index a78c7b01d..d54a6733b 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -11,29 +11,35 @@ create table course CREATE TABLE session ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - title VARCHAR(255) NOT NULL, - start_date DATETIME NOT NULL, - end_date DATETIME NOT NULL, - tuition BIGINT NOT NULL, - current_count INT NOT NULL, - capacity INT NOT NULL, + id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + start_date DATETIME NOT NULL, + end_date DATETIME NOT NULL, + tuition BIGINT NOT NULL, + current_count INT NOT NULL, + capacity INT NOT NULL, - -- Embedded Image - image_file_size FLOAT, - image_file_type VARCHAR(50), - image_url VARCHAR(500), - image_width INT, - image_height INT, - - status VARCHAR(50) NOT NULL, - recruitment_status VARCHAR(50) NOT NULL, + status VARCHAR(50) NOT NULL, + recruitment_status VARCHAR(50) NOT NULL, -- FK to course - course_id BIGINT NOT NULL, + course_id BIGINT NOT NULL, FOREIGN KEY (course_id) REFERENCES course (id) ); +CREATE TABLE image +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + session_id BIGINT NOT NULL, + url VARCHAR(500) NOT NULL, + file_type VARCHAR(50), + file_size FLOAT, + width INT, + height INT, + FOREIGN KEY (session_id) REFERENCES session (id) +); + + create table ns_user ( id bigint generated by default as identity, diff --git a/src/test/java/nextstep/courses/domain/ImagesTest.java b/src/test/java/nextstep/courses/domain/ImagesTest.java new file mode 100644 index 000000000..f06660988 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/ImagesTest.java @@ -0,0 +1,44 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +class ImagesTest { + + @Test + void ์์ฑ_๋ฐ_์กฐํ() { + Image image1 = new Image(100, "jpg", "url1", 300, 200); + Image image2 = new Image(100, "png", "url2", 300, 200); + + Images images = new Images(List.of(image1, image2)); + + assertThat(images.getImages()).hasSize(2) + .containsExactly(image1, image2); + } + + @Test + void ์์ฑ์์์_๋ณต์ฌ๋_๋ฆฌ์คํธ๋_์ธ๋ถ_๋ฆฌ์คํธ_๋ณ๊ฒฝ_์ํฅ์_๋ฐ์ง_์๋๋ค() { + List<Image> original = new ArrayList<>(); + original.add(new Image(100, "jpg", "url1", 300, 200)); + + Images images = new Images(original); + + original.add(new Image(100, "png", "url2", 300, 200)); + + assertThat(images.getImages()).hasSize(1); + } + + @Test + void getImages๋ก_๊ฐ์ ธ์จ_๋ฆฌ์คํธ๋_์์ ํ _์_์๋ค() { + Images images = new Images(List.of(new Image(100, "jpg", "url1", 300, 200))); + + List<Image> retrieved = images.getImages(); + + assertThatThrownBy(() -> retrieved.add(new Image(100, "png", "url2", 300, 200))) + .isInstanceOf(UnsupportedOperationException.class); + } +} \ 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 eba2a0e63..ad5c8020c 100644 --- a/src/test/java/nextstep/courses/domain/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -5,12 +5,13 @@ import org.junit.jupiter.api.Test; import java.time.LocalDateTime; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class SessionTest { - private final Image validImage = new Image(500f, "png", "cdn.com", 600, 400); + private final Images validImages = new Images(List.of(validImage)); @Test @DisplayName("๋ชจ์ง์ค ์ํ์ ๋ฌด๋ฃ ๊ฐ์๋ ์๊ฐ ์ ์ฒญ ๊ฐ๋ฅํ๋ค") @@ -23,7 +24,7 @@ void freeSession_joinable_whenRecruiting() { 0L, // tuition 0, // currentCount 0, // capacity (๋ฌด์ ํ์ด์ง๋ง ๊ทธ๋ฅ 0์ผ๋ก ๋ ) - validImage, + validImages, SessionStatus.ONGOING, RecruitmentStatus.RECRUITING, new FreeJoinStrategy() @@ -43,7 +44,7 @@ void freeSession_notJoinable_whenNotRecruiting() { 0L, 0, 0, - validImage, + validImages, SessionStatus.PREPARING, RecruitmentStatus.RECRUITING, new FreeJoinStrategy() @@ -63,7 +64,7 @@ void paidSession_joinable_whenRecruiting_underCapacity_andPaidCorrectly() { 10000L, // tuition 29, // currentCount 30, // capacity - validImage, + validImages, SessionStatus.ONGOING, RecruitmentStatus.RECRUITING, new PaidJoinStrategy() @@ -83,7 +84,7 @@ void paidSession_notJoinable_whenWrongAmount() { 10000L, 10, 30, - validImage, + validImages, SessionStatus.ONGOING, RecruitmentStatus.RECRUITING, new PaidJoinStrategy() @@ -103,7 +104,7 @@ void paidSession_notJoinable_whenOverCapacity() { 10000L, 30, 30, - validImage, + validImages, SessionStatus.ONGOING, RecruitmentStatus.RECRUITING, new PaidJoinStrategy() @@ -123,7 +124,7 @@ void notJoinable_whenLecturePreparing_evenIfRecruiting() { 0L, 0, 0, - validImage, + validImages, SessionStatus.PREPARING, RecruitmentStatus.RECRUITING, new FreeJoinStrategy() @@ -143,7 +144,7 @@ void notJoinable_whenLectureClosed_evenIfRecruiting() { 0L, 0, 0, - validImage, + validImages, SessionStatus.CLOSED, RecruitmentStatus.RECRUITING, new FreeJoinStrategy() @@ -163,7 +164,7 @@ void notJoinable_whenNotRecruiting() { 0L, 0, 0, - validImage, + validImages, SessionStatus.ONGOING, RecruitmentStatus.NOT_RECRUITING, new FreeJoinStrategy() diff --git a/src/test/java/nextstep/courses/domain/SessionsTest.java b/src/test/java/nextstep/courses/domain/SessionsTest.java index 4a9ff3a25..327f4ebfc 100644 --- a/src/test/java/nextstep/courses/domain/SessionsTest.java +++ b/src/test/java/nextstep/courses/domain/SessionsTest.java @@ -9,8 +9,8 @@ import static org.assertj.core.api.Assertions.*; class SessionsTest { - private final Image image = new Image(500f, "png", "cdn.com", 600, 400); + private final Images images = new Images(List.of(image)); private Session createSessionWithId(int id) { return new Session( @@ -21,7 +21,7 @@ private Session createSessionWithId(int id) { 0L, 0, 0, - image, + images, SessionStatus.ONGOING, RecruitmentStatus.RECRUITING, new FreeJoinStrategy() diff --git a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java index a2219b00e..ad4442989 100644 --- a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java +++ b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java @@ -10,6 +10,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import java.time.LocalDateTime; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -31,30 +32,39 @@ void setUp() { void crud() { // given Image image = new Image(100.0f, "png", "https://example.com/image.png", 300, 200); + Images images = new Images(List.of(image)); Session session = new Session( "๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ", - 1, + 0, // ์ ์ฅ ์ ์ id๋ ๋ฌด์๋ฏธํจ LocalDateTime.now(), LocalDateTime.now().plusDays(30), 10000L, 0, 20, - image, + images, SessionStatus.ONGOING, RecruitmentStatus.RECRUITING ); // when - int count = sessionRepository.save(session, 1L); // courseId = 1L - assertThat(count).isEqualTo(1); - - Session saved = sessionRepository.findById(1L); + int savedSessionId = sessionRepository.save(session, 1L); // courseId = 1L + Session saved = sessionRepository.findById((long) savedSessionId); // then assertThat(saved.getTitle()).isEqualTo(session.getTitle()); assertThat(saved.getTuition()).isEqualTo(session.getTuition()); assertThat(saved.getCapacity()).isEqualTo(session.getCapacity()); - assertThat(saved.getCoverImage().getImageUrl()).isEqualTo(session.getCoverImage().getImageUrl()); + + // ์ด๋ฏธ์ง ๊ฒ์ฆ + List<Image> savedImages = saved.getCoverImages().getImages(); + assertThat(savedImages).hasSize(1); + + Image savedImage = savedImages.get(0); + assertThat(savedImage.getImageUrl()).isEqualTo(image.getImageUrl()); + assertThat(savedImage.getSize()).isEqualTo(image.getSize()); + assertThat(savedImage.getType()).isEqualTo(image.getType()); + assertThat(savedImage.getWidth()).isEqualTo(image.getWidth()); + assertThat(savedImage.getHeight()).isEqualTo(image.getHeight()); LOGGER.debug("Session: {}", saved); } From e1db975f09c3aa48e2b90c71b1f45815b88873da Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Tue, 22 Apr 2025 13:22:38 +0900 Subject: [PATCH 4/9] feat: add domain model `Enrollment` --- .../nextstep/courses/domain/Enrollment.java | 39 ++++++++++ .../courses/domain/EnrollmentStatus.java | 5 ++ .../courses/domain/EnrollmentTest.java | 77 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/Enrollment.java create mode 100644 src/main/java/nextstep/courses/domain/EnrollmentStatus.java create mode 100644 src/test/java/nextstep/courses/domain/EnrollmentTest.java diff --git a/src/main/java/nextstep/courses/domain/Enrollment.java b/src/main/java/nextstep/courses/domain/Enrollment.java new file mode 100644 index 000000000..a9cc05f56 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Enrollment.java @@ -0,0 +1,39 @@ +package nextstep.courses.domain; + +public class Enrollment { + private Member student; + private Session session; + private EnrollmentStatus status; // PENDING, APPROVED, REJECTED + + public Enrollment(Member student, Session session) { + this.student = student; + this.session = session; + this.status = EnrollmentStatus.PENDING; + } + + public void approve() { + if (this.status == EnrollmentStatus.APPROVED) { + throw new IllegalStateException("์ด๋ฏธ ์น์ธ๋ ์๊ฐ ์ ์ฒญ์ ๋๋ค."); + } + + this.status = EnrollmentStatus.APPROVED; + session.accept(); + } + + public void reject() { + this.status = EnrollmentStatus.REJECTED; + } + + public boolean isApproved() { + return status == EnrollmentStatus.APPROVED; + } + + public Member getStudent() { + return student; + } + + public EnrollmentStatus getStatus() { + return status; + } +} + diff --git a/src/main/java/nextstep/courses/domain/EnrollmentStatus.java b/src/main/java/nextstep/courses/domain/EnrollmentStatus.java new file mode 100644 index 000000000..9e04b16b2 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/EnrollmentStatus.java @@ -0,0 +1,5 @@ +package nextstep.courses.domain; + +public enum EnrollmentStatus { + PENDING, APPROVED, REJECTED; +} diff --git a/src/test/java/nextstep/courses/domain/EnrollmentTest.java b/src/test/java/nextstep/courses/domain/EnrollmentTest.java new file mode 100644 index 000000000..44ffc03df --- /dev/null +++ b/src/test/java/nextstep/courses/domain/EnrollmentTest.java @@ -0,0 +1,77 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EnrollmentTest { + + private final Member member = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); + private final Image validImage = new Image(500f, "png", "cdn.com", 600, 400); + private final Images validImages = new Images(List.of(validImage)); + + private Session createDummySession() { + return new Session( + "๋๋ฏธ ๊ฐ์", + 1, + LocalDateTime.now(), + LocalDateTime.now().plusDays(7), + 0L, + 0, + 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + } + + @Test + @DisplayName("Enrollment๋ ์์ฑ ์ PENDING ์ํ์ด๋ค") + void created_enrollment_has_pending_status() { + Session session = createDummySession(); + Enrollment enrollment = new Enrollment(member, session); + + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.PENDING); + } + + @Test + @DisplayName("Enrollment๋ฅผ ์น์ธํ๋ฉด APPROVED ์ํ๊ฐ ๋๋ค") + void approve_sets_status_to_approved() { + Session session = createDummySession(); + Enrollment enrollment = new Enrollment(member, session); + + enrollment.approve(); + + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.APPROVED); + } + + @Test + @DisplayName("Enrollment๋ฅผ ๊ฑฐ์ ํ๋ฉด REJECTED ์ํ๊ฐ ๋๋ค") + void reject_sets_status_to_rejected() { + Session session = createDummySession(); + Enrollment enrollment = new Enrollment(member, session); + + enrollment.reject(); + + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.REJECTED); + } + + @Test + @DisplayName("์ด๋ฏธ ์น์ธ๋ Enrollment๋ ๋ค์ ์น์ธํ ์ ์๋ค") + void approving_twice_should_throw() { + Session session = createDummySession(); + Enrollment enrollment = new Enrollment(member, session); + + enrollment.approve(); + + assertThatThrownBy(enrollment::approve) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("์ด๋ฏธ ์น์ธ๋ ์๊ฐ ์ ์ฒญ์ ๋๋ค."); + } +} From 1ce727a2bd461f64ac6ae89a374c45c6aa2fba66 Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Tue, 22 Apr 2025 13:24:18 +0900 Subject: [PATCH 5/9] feat: add domain model `Enrollments` --- .../nextstep/courses/domain/Enrollments.java | 29 +++++ .../courses/domain/EnrollmentsTest.java | 100 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/Enrollments.java create mode 100644 src/test/java/nextstep/courses/domain/EnrollmentsTest.java diff --git a/src/main/java/nextstep/courses/domain/Enrollments.java b/src/main/java/nextstep/courses/domain/Enrollments.java new file mode 100644 index 000000000..a0a320137 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Enrollments.java @@ -0,0 +1,29 @@ +package nextstep.courses.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Enrollments { + private final List<Enrollment> values = new ArrayList<>(); + + public void addEnrollment(Enrollment enrollment) { + values.add(enrollment); + } + + public boolean isEnrolledBy(Member student) { + return values.stream() + .anyMatch(e -> e.getStudent().equals(student)); + } + + public Enrollment findByMember(Member student) { + return values.stream() + .filter(e -> e.getStudent().equals(student)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("ํด๋น ํ์์ ์๊ฐ ์ ์ฒญ์ด ์กด์ฌํ์ง ์์ต๋๋ค.")); + } + + public List<Enrollment> getValues() { + return Collections.unmodifiableList(values); + } +} diff --git a/src/test/java/nextstep/courses/domain/EnrollmentsTest.java b/src/test/java/nextstep/courses/domain/EnrollmentsTest.java new file mode 100644 index 000000000..f460731d1 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/EnrollmentsTest.java @@ -0,0 +1,100 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class EnrollmentsTest { + + private final Member member1 = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); + private final Member member2 = new Member(2L, "์ด๋ชฝ๋ฃก", "lee@example.com"); + + private final Image validImage = new Image(500f, "png", "cdn.com", 600, 400); + private final Images validImages = new Images(List.of(validImage)); + + private Session createSession() { + return new Session( + "๊ฐ์", + 1, + LocalDateTime.now(), + LocalDateTime.now().plusDays(7), + 0L, + 0, + 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + } + + @Test + @DisplayName("Enrollment๋ฅผ ์ถ๊ฐํ๋ฉด ๋ฆฌ์คํธ์ ํฌํจ๋๋ค") + void addEnrollment_stores_enrollment() { + Enrollments enrollments = new Enrollments(); + Session session = createSession(); + Enrollment enrollment = new Enrollment(member1, session); + + enrollments.addEnrollment(enrollment); + + assertThat(enrollments.getValues()).contains(enrollment); + } + + @Test + @DisplayName("ํด๋น ๋ฉค๋ฒ๊ฐ ์๊ฐ ์ ์ฒญํ ๊ฒฝ์ฐ isEnrolledBy()๋ true๋ฅผ ๋ฐํํ๋ค") + void isEnrolledBy_returns_true_for_existing_member() { + Enrollments enrollments = new Enrollments(); + Session session = createSession(); + enrollments.addEnrollment(new Enrollment(member1, session)); + + assertThat(enrollments.isEnrolledBy(member1)).isTrue(); + } + + @Test + @DisplayName("ํด๋น ๋ฉค๋ฒ๊ฐ ์๊ฐ ์ ์ฒญํ์ง ์์ ๊ฒฝ์ฐ isEnrolledBy()๋ false๋ฅผ ๋ฐํํ๋ค") + void isEnrolledBy_returns_false_for_non_enrolled_member() { + Enrollments enrollments = new Enrollments(); + assertThat(enrollments.isEnrolledBy(member1)).isFalse(); + } + + @Test + @DisplayName("findByMember()๋ ์๊ฐ ์ ์ฒญํ ๋ฉค๋ฒ์ Enrollment๋ฅผ ๋ฐํํ๋ค") + void findByMember_returns_enrollment() { + Enrollments enrollments = new Enrollments(); + Session session = createSession(); + Enrollment enrollment = new Enrollment(member1, session); + enrollments.addEnrollment(enrollment); + + Enrollment found = enrollments.findByMember(member1); + + assertThat(found).isEqualTo(enrollment); + } + + @Test + @DisplayName("findByMember()๋ ์๊ฐ ์ ์ฒญํ์ง ์์ ๋ฉค๋ฒ์ผ ๊ฒฝ์ฐ ์์ธ๋ฅผ ๋์ง๋ค") + void findByMember_throws_for_non_existing_member() { + Enrollments enrollments = new Enrollments(); + + assertThatThrownBy(() -> enrollments.findByMember(member2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ํด๋น ํ์์ ์๊ฐ ์ ์ฒญ์ด ์กด์ฌํ์ง ์์ต๋๋ค."); + } + + @Test + @DisplayName("getValues()๋ ๋ถ๋ณ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๋ค") + void getValues_returns_unmodifiable_list() { + Enrollments enrollments = new Enrollments(); + Session session = createSession(); + enrollments.addEnrollment(new Enrollment(member1, session)); + + List<Enrollment> values = enrollments.getValues(); + + assertThatThrownBy(() -> values.add(new Enrollment(member2, session))) + .isInstanceOf(UnsupportedOperationException.class); + } +} From 06ea9e5ef11e61470deecce2a9a5bcf706c5d657 Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Tue, 22 Apr 2025 13:25:43 +0900 Subject: [PATCH 6/9] feat: add domain model `Member` --- .../java/nextstep/courses/domain/Member.java | 41 +++++++++++++++++ .../nextstep/courses/domain/MemberTest.java | 46 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/main/java/nextstep/courses/domain/Member.java create mode 100644 src/test/java/nextstep/courses/domain/MemberTest.java diff --git a/src/main/java/nextstep/courses/domain/Member.java b/src/main/java/nextstep/courses/domain/Member.java new file mode 100644 index 000000000..03a131dfd --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Member.java @@ -0,0 +1,41 @@ +package nextstep.courses.domain; + +import java.util.Objects; + +public class Member { + private final Long id; + private final String name; + private final String email; + + public Member(Long id, String name, String email) { + this.id = id; + this.name = name; + this.email = email; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + // Member ์๋ณ ๋น๊ต (์: Set ์ฌ์ฉ ์ ํ์) + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Member)) return false; + Member member = (Member) o; + return Objects.equals(id, member.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/MemberTest.java b/src/test/java/nextstep/courses/domain/MemberTest.java new file mode 100644 index 000000000..dc05f66b9 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/MemberTest.java @@ -0,0 +1,46 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MemberTest { + + @Test + @DisplayName("Member๋ ์์ฑ ์ id, name, email ๊ฐ์ ๊ฐ์ง๋ค") + void member_creation_and_getters() { + Member member = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); + + assertThat(member.getId()).isEqualTo(1L); + assertThat(member.getName()).isEqualTo("ํ๊ธธ๋"); + assertThat(member.getEmail()).isEqualTo("hong@example.com"); + } + + @Test + @DisplayName("id๊ฐ ๊ฐ์ ๋ Member๋ equals๋ก ๊ฐ๋ค๊ณ ํ๋จํ๋ค") + void members_with_same_id_are_equal() { + Member member1 = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); + Member member2 = new Member(1L, "์ด๋ฆ๋ฌด๊ด", "๋ค๋ฅธ์ด๋ฉ์ผ@example.com"); + + assertThat(member1).isEqualTo(member2); + } + + @Test + @DisplayName("id๊ฐ ๋ค๋ฅธ ๋ Member๋ equals๋ก ๋ค๋ฅด๋ค๊ณ ํ๋จํ๋ค") + void members_with_different_id_are_not_equal() { + Member member1 = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); + Member member2 = new Member(2L, "ํ๊ธธ๋", "hong@example.com"); + + assertThat(member1).isNotEqualTo(member2); + } + + @Test + @DisplayName("id๊ฐ ๊ฐ์ผ๋ฉด hashCode๋ ๋์ผํ๋ค") + void members_with_same_id_have_same_hashcode() { + Member member1 = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); + Member member2 = new Member(1L, "๋ค๋ฅธ์ด๋ฆ", "๋ค๋ฅธ์ด๋ฉ์ผ@example.com"); + + assertThat(member1.hashCode()).isEqualTo(member2.hashCode()); + } +} From 5c95f8e061d3784a69f2b81e3cec37dab06890a8 Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Tue, 22 Apr 2025 13:28:35 +0900 Subject: [PATCH 7/9] feat: add enrollment to `Session` --- README.md | 3 +- .../java/nextstep/courses/domain/Session.java | 25 ++++- .../courses/service/EnrollService.java | 30 ++++-- .../nextstep/courses/domain/SessionTest.java | 96 +++++++++++++++++++ 4 files changed, 143 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3477a100e..c3a1b8e36 100644 --- a/README.md +++ b/README.md @@ -73,5 +73,4 @@ ## ๐ 4๋จ๊ณ - ์๊ฐ์ ์ฒญ(์๊ตฌ์ฌํญ ๋ณ๊ฒฝ) - [x] SessionStatus ์งํ์ํ์ ๋ชจ์ง์ํ๋ก ๋ถ๋ฆฌ - [x] coverImage ์ฌ๋ฌ๊ฐ ๊ฐ๋ฅํ๋๋ก ๊ธฐ๋ฅ ์ถ๊ฐ -- [] ์ ๋ฐ ์ฌ๋ถ ํ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ -- [] ์๊ฐ ์น์ธ ๋ฐ ์๊ฐ ์ทจ์ ๊ธฐ๋ฅ ์ถ๊ฐ \ No newline at end of file +- [x] ์๊ฐ ์น์ธ ๋ฐ ์๊ฐ ์ทจ์ ๊ธฐ๋ฅ ์ถ๊ฐ \ 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 84d7886de..b04fbbce9 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -14,6 +14,7 @@ public class Session { private RecruitmentStatus recruitmentStatus; private SessionStatus sessionStatus; private JoinStrategy joinStrategy; + private final Enrollments enrollments = new Enrollments(); public Session(String title, int id, LocalDateTime startDate, LocalDateTime endDate, Long tuition, int currentCount, int capacity, Images coverImages, SessionStatus sessionStatus, RecruitmentStatus recruitmentStatus) { @@ -52,14 +53,32 @@ public boolean hasId(long id) { return this.id == id; } - public void enroll(Payment pay) { + public void enroll(Payment pay, Member member) { if (!joinable(pay)) { throw new IllegalStateException("์๊ฐ ์ ์ฒญ ์กฐ๊ฑด์ ๋ง์กฑํ์ง ์์ต๋๋ค."); } + if (enrollments.isEnrolledBy(member)) { + throw new IllegalStateException("์ด๋ฏธ ์๊ฐ ์ ์ฒญํ ํ์์ ๋๋ค."); + } + + enrollments.addEnrollment(new Enrollment(member, this)); + } + + public void approveEnrollment(Member member) { + Enrollment enrollment = enrollments.findByMember(member); + enrollment.approve(); + } + + public void accept(){ this.capacityInfo.increaseCurrentCount(); } + public void rejectEnrollment(Member member) { + Enrollment enrollment = enrollments.findByMember(member); + enrollment.reject(); + } + public String getTitle() { return title; } @@ -103,4 +122,8 @@ public SessionStatus getSessionStatus() { public RecruitmentStatus getRecruitmentStatus() { return recruitmentStatus; } + + public Enrollments getEnrollments() { + return enrollments; + } } diff --git a/src/main/java/nextstep/courses/service/EnrollService.java b/src/main/java/nextstep/courses/service/EnrollService.java index 0f68163e3..cc9bc514f 100644 --- a/src/main/java/nextstep/courses/service/EnrollService.java +++ b/src/main/java/nextstep/courses/service/EnrollService.java @@ -1,9 +1,6 @@ package nextstep.courses.service; -import nextstep.courses.domain.Course; -import nextstep.courses.domain.CourseRepository; -import nextstep.courses.domain.Session; -import nextstep.courses.domain.Sessions; +import nextstep.courses.domain.*; import nextstep.payments.domain.Payment; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,14 +9,31 @@ @Service("enrollService") public class EnrollService { + @Resource(name = "courseRepository") private CourseRepository courseRepository; @Transactional - public void enrollCourse(long courseId, long sessionId, Payment payment) { + public void enrollCourse(long courseId, long sessionId, Payment payment, Member member) { + Session session = findSession(courseId, sessionId); + session.enroll(payment, member); // ์กฐ๊ฑด ๋ง์กฑํด์ผ๋ง Enrollment(PENDING) ์์ฑ + } + + @Transactional + public void approveEnrollment(long courseId, long sessionId, Member member) { + Session session = findSession(courseId, sessionId); + session.approveEnrollment(member); + } + + @Transactional + public void rejectEnrollment(long courseId, long sessionId, Member member) { + Session session = findSession(courseId, sessionId); + session.rejectEnrollment(member); + } + + private Session findSession(long courseId, long sessionId) { Course course = courseRepository.findById(courseId); - Sessions sessions = course.getSessions(); - Session session = sessions.findById(sessionId); - session.enroll(payment); + return course.getSessions().findById(sessionId); } } + diff --git a/src/test/java/nextstep/courses/domain/SessionTest.java b/src/test/java/nextstep/courses/domain/SessionTest.java index ad5c8020c..3af277e05 100644 --- a/src/test/java/nextstep/courses/domain/SessionTest.java +++ b/src/test/java/nextstep/courses/domain/SessionTest.java @@ -8,10 +8,12 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; class SessionTest { private final Image validImage = new Image(500f, "png", "cdn.com", 600, 400); private final Images validImages = new Images(List.of(validImage)); + private final Member member = new Member(1L, "ํ๊ธธ๋", "hong@example.com"); @Test @DisplayName("๋ชจ์ง์ค ์ํ์ ๋ฌด๋ฃ ๊ฐ์๋ ์๊ฐ ์ ์ฒญ ๊ฐ๋ฅํ๋ค") @@ -173,4 +175,98 @@ void notJoinable_whenNotRecruiting() { assertThat(session.joinable(new Payment())).isFalse(); } + @Test + @DisplayName("์๊ฐ ์กฐ๊ฑด์ ๋ง์กฑํ๋ฉด ์๊ฐ ์ ์ฒญ์ด PENDING ์ํ๋ก ๋ฑ๋ก๋๋ค") + void enroll_success_creates_pending_enrollment() { + Session session = new Session("๊ฐ์", 1, + LocalDateTime.now(), LocalDateTime.now().plusDays(7), + 0L, 0, 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + session.enroll(new Payment(), member); + + Enrollment enrollment = session.getEnrollments().findByMember(member); + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.PENDING); + } + + @Test + @DisplayName("๋์ผํ ์ฌ์ฉ์๊ฐ ์ค๋ณต ์ ์ฒญ ์ ์์ธ ๋ฐ์") + void duplicate_enrollment_should_throw() { + Session session = new Session("๊ฐ์", 1, + LocalDateTime.now(), LocalDateTime.now().plusDays(7), + 0L, 0, 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + session.enroll(new Payment(), member); + + assertThatThrownBy(() -> session.enroll(new Payment(), member)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("์ด๋ฏธ ์๊ฐ ์ ์ฒญํ ํ์์ ๋๋ค."); + } + + @Test + @DisplayName("๊ฐ์ฌ๊ฐ ์๊ฐ ์ ์ฒญ์ ์น์ธํ๋ฉด APPROVED ์ํ๊ฐ ๋๊ณ ์ ์์ด ์ฆ๊ฐํ๋ค") + void approve_enrollment_increases_capacity() { + Session session = new Session("๊ฐ์", 1, + LocalDateTime.now(), LocalDateTime.now().plusDays(7), + 0L, 0, 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + session.enroll(new Payment(), member); + session.approveEnrollment(member); + + Enrollment enrollment = session.getEnrollments().findByMember(member); + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.APPROVED); + assertThat(session.getCurrentCount()).isEqualTo(1); + } + + @Test + @DisplayName("๊ฐ์ฌ๊ฐ ์๊ฐ ์ ์ฒญ์ ๊ฑฐ์ ํ๋ฉด REJECTED ์ํ๊ฐ ๋๋ค") + void reject_enrollment_sets_rejected_status() { + Session session = new Session("๊ฐ์", 1, + LocalDateTime.now(), LocalDateTime.now().plusDays(7), + 0L, 0, 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + session.enroll(new Payment(), member); + session.rejectEnrollment(member); + + Enrollment enrollment = session.getEnrollments().findByMember(member); + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.REJECTED); + } + + @Test + @DisplayName("์น์ธํ์ง ์์ผ๋ฉด ์๊ฐ ์ ์ฒญ์ APPROVED ์ํ๊ฐ ์๋๋ค") + void pending_enrollment_not_approved_by_default() { + Session session = new Session("๊ฐ์", 1, + LocalDateTime.now(), LocalDateTime.now().plusDays(7), + 0L, 0, 10, + validImages, + SessionStatus.ONGOING, + RecruitmentStatus.RECRUITING, + new FreeJoinStrategy() + ); + + session.enroll(new Payment(), member); + + Enrollment enrollment = session.getEnrollments().findByMember(member); + assertThat(enrollment.isApproved()).isFalse(); + } + } From 987d58b4429a6f7a3c3b8188ad606c7bcec7cc2e Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Tue, 22 Apr 2025 13:28:55 +0900 Subject: [PATCH 8/9] db: add Member, Enrollment table --- src/main/resources/schema.sql | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index d54a6733b..ca3e49c42 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -27,6 +27,28 @@ CREATE TABLE session FOREIGN KEY (course_id) REFERENCES course (id) ); +CREATE TABLE member +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE +); + +CREATE TABLE enrollment +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + session_id BIGINT NOT NULL, + member_id BIGINT NOT NULL, + status VARCHAR(50) NOT NULL, -- PENDING, APPROVED, REJECTED + + -- FK ์ค์ + FOREIGN KEY (session_id) REFERENCES session (id), + FOREIGN KEY (member_id) REFERENCES member (id), + + UNIQUE (session_id, member_id) -- ์ค๋ณต ์๊ฐ ์ ์ฒญ ๋ฐฉ์ง +); + + CREATE TABLE image ( id BIGINT AUTO_INCREMENT PRIMARY KEY, From 504f5a5eda30c89e61cb90095c80f778ca2b05ea Mon Sep 17 00:00:00 2001 From: "kass.fresh" <kass.fresh@kakaocorp.com> Date: Tue, 22 Apr 2025 13:49:54 +0900 Subject: [PATCH 9/9] feat: implement `JdbcEnrollmentRepository` --- .../nextstep/courses/domain/Enrollment.java | 4 + .../JdbcEnrollmentRepository.java | 60 ++++++++++++++ .../EnrollmentRepositoryTest.java | 81 +++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 src/main/java/nextstep/courses/infrastructure/JdbcEnrollmentRepository.java create mode 100644 src/test/java/nextstep/courses/infrastructure/EnrollmentRepositoryTest.java diff --git a/src/main/java/nextstep/courses/domain/Enrollment.java b/src/main/java/nextstep/courses/domain/Enrollment.java index a9cc05f56..599fcb251 100644 --- a/src/main/java/nextstep/courses/domain/Enrollment.java +++ b/src/main/java/nextstep/courses/domain/Enrollment.java @@ -35,5 +35,9 @@ public Member getStudent() { public EnrollmentStatus getStatus() { return status; } + + public Session getSession() { + return session; + } } diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcEnrollmentRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcEnrollmentRepository.java new file mode 100644 index 000000000..524c22739 --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/JdbcEnrollmentRepository.java @@ -0,0 +1,60 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.Enrollment; +import nextstep.courses.domain.EnrollmentStatus; +import nextstep.courses.domain.Member; +import nextstep.courses.domain.Session; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public class JdbcEnrollmentRepository { + + private final JdbcOperations jdbc; + private final JdbcSessionRepository jdbcSessionRepository; + + public JdbcEnrollmentRepository(JdbcOperations jdbc, JdbcSessionRepository jdbcSessionRepository) { + this.jdbc = jdbc; + this.jdbcSessionRepository = jdbcSessionRepository; + } + + public void save(Long sessionId, Long memberId, EnrollmentStatus status) { + String sql = "INSERT INTO enrollment (session_id, member_id, status) VALUES (?, ?, ?)"; + jdbc.update(sql, sessionId, memberId, status.name()); + } + + public void updateStatus(Long sessionId, Long memberId, EnrollmentStatus status) { + String sql = "UPDATE enrollment SET status = ? WHERE session_id = ? AND member_id = ?"; + jdbc.update(sql, status.name(), sessionId, memberId); + } + + public List<Enrollment> findBySessionId(Long sessionId) { + String sql = + "SELECT e.*, m.id as member_id, m.name, m.email " + + "FROM enrollment e " + + "JOIN member m ON e.member_id = m.id " + + "WHERE e.session_id = ?"; + + + return jdbc.query(sql, (rs, rowNum) -> { + Member member = new Member( + rs.getLong("member_id"), + rs.getString("name"), + rs.getString("email") + ); + + Session session = jdbcSessionRepository.findById(sessionId); + Enrollment enrollment = new Enrollment(member, session); + EnrollmentStatus status = EnrollmentStatus.valueOf(rs.getString("status")); + if (status == EnrollmentStatus.APPROVED) { + enrollment.approve(); + } else if (status == EnrollmentStatus.REJECTED) { + enrollment.reject(); + } + + return enrollment; + }, sessionId); + } +} diff --git a/src/test/java/nextstep/courses/infrastructure/EnrollmentRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/EnrollmentRepositoryTest.java new file mode 100644 index 000000000..457d02761 --- /dev/null +++ b/src/test/java/nextstep/courses/infrastructure/EnrollmentRepositoryTest.java @@ -0,0 +1,81 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +class JdbcEnrollmentRepositoryTest { + + @Autowired + private JdbcTemplate jdbc; + + private JdbcSessionRepository sessionRepository; + private JdbcEnrollmentRepository enrollmentRepository; + + @BeforeEach + void setUp() { + sessionRepository = new JdbcSessionRepository(jdbc); + enrollmentRepository = new JdbcEnrollmentRepository(jdbc, sessionRepository); + + // ํ ์คํธ์ฉ ๋ฐ์ดํฐ ์ฝ์ + jdbc.update("INSERT INTO member (id, name, email) VALUES (?, ?, ?)", + 1L, "ํ๊ธธ๋", "hong@example.com"); + + jdbc.update("INSERT INTO session (id, title, start_date, end_date, tuition, current_count, capacity, status, recruitment_status, course_id) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + 1L, "๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ", + LocalDateTime.now(), LocalDateTime.now().plusDays(30), + 10000L, 0, 20, + "ONGOING", "RECRUITING", 1L + ); + + // image insert ์์ด๋ session์ ์์ฑ๋จ + } + + @Test + @DisplayName("Enrollment ์ ์ฅ ํ ์กฐํํ ์ ์๋ค (Session ํฌํจ)") + void save_and_find_with_session() { + // when + enrollmentRepository.save(1L, 1L, EnrollmentStatus.PENDING); + + // then + List<Enrollment> result = enrollmentRepository.findBySessionId(1L); + assertThat(result).hasSize(1); + + Enrollment enrollment = result.get(0); + assertThat(enrollment.getStudent().getId()).isEqualTo(1L); + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.PENDING); + + // โ Session๊น์ง ์ ๋๋ก ์ฐ๊ฒฐ๋์๋์ง ํ์ธ + Session session = enrollment.getSession(); + assertThat(session).isNotNull(); + assertThat(session.getTitle()).isEqualTo("๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ"); + assertThat(session.getTuition()).isEqualTo(10000L); + } + + @Test + @DisplayName("Enrollment ์ํ๋ฅผ ๋ณ๊ฒฝํ ์ ์๋ค") + void update_status() { + // given + enrollmentRepository.save(1L, 1L, EnrollmentStatus.PENDING); + + // when + enrollmentRepository.updateStatus(1L, 1L, EnrollmentStatus.APPROVED); + + // then + List<Enrollment> result = enrollmentRepository.findBySessionId(1L); + Enrollment enrollment = result.get(0); + + assertThat(enrollment.getStatus()).isEqualTo(EnrollmentStatus.APPROVED); + } +}