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);
+    }
+}