diff --git a/.gitignore b/.gitignore index 249cf086af..3f37a7b392 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ out/ ### VS Code ### .vscode/ + +/bin/ \ No newline at end of file diff --git a/src/main/java/nextstep/courses/domain/Capacity.java b/src/main/java/nextstep/courses/domain/Capacity.java index 05ddba85cd..9db736f60a 100644 --- a/src/main/java/nextstep/courses/domain/Capacity.java +++ b/src/main/java/nextstep/courses/domain/Capacity.java @@ -1,5 +1,7 @@ package nextstep.courses.domain; +import java.util.Objects; + public class Capacity { private final int value; @@ -17,4 +19,17 @@ public int getValue() { public boolean isFull(int registeredCount) { return registeredCount >= value; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Capacity)) return false; + Capacity capacity = (Capacity) o; + return value == capacity.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } } \ 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 0ae0186d8e..847a443d48 100644 --- a/src/main/java/nextstep/courses/domain/Image.java +++ b/src/main/java/nextstep/courses/domain/Image.java @@ -1,16 +1,16 @@ package nextstep.courses.domain; public class Image { + private static final int MAX_IMAGE_SIZE = 1024 * 1024; + private static final int WIDTH_LIMIT = 300; + private static final int HEIGHT_LIMIT = 200; + private static final double REQUIRED_ASPECT_RATIO = 1.5; + private static final double ASPECT_RATIO_TOLERANCE = 0.01; private String fileName; private String contentType; private long sizeInBytes; private int width; private int height; - private static final int IMAGE_SIZE_1MB = 1024 * 1024; - private static final int WIDTH_LIMIT = 300; - private static final int HEIGHT_LIMIT = 200; - private static final double REQUIRED_ASPECT_RATIO = 1.5; - private static final double ASPECT_RATIO_TOLERANCE = 0.01; public Image(String fileName, String contentType, long sizeInBytes, int width, int height) { validate(contentType, sizeInBytes, width, height); @@ -21,8 +21,24 @@ public Image(String fileName, String contentType, long sizeInBytes, int width, i this.height = height; } + public String getFileName() { + return fileName; + } + public String getContentType() { + return contentType; + } + public long getSizeInBytes() { + return sizeInBytes; + } + public int getWidth() { + return width; + } + public int getHeight() { + return height; + } + private void validate(String contentType, long sizeInBytes, int width, int height) { - if (sizeInBytes > IMAGE_SIZE_1MB) { + if (sizeInBytes > MAX_IMAGE_SIZE) { throw new IllegalArgumentException("이미지 크기는 1MB 이하여야 합니다."); } diff --git a/src/main/java/nextstep/courses/domain/PaidSession.java b/src/main/java/nextstep/courses/domain/PaidSession.java index 187190ef18..a849f74812 100644 --- a/src/main/java/nextstep/courses/domain/PaidSession.java +++ b/src/main/java/nextstep/courses/domain/PaidSession.java @@ -13,6 +13,14 @@ public PaidSession(Long id, String name, Period period, Image coverImage, this.tuitionFee = new TuitionFee(tuitionFee); } + public Capacity getCapacity() { + return maxCapacity; + } + + public TuitionFee getTuitionFee() { + return tuitionFee; + } + @Override protected void validateRegistration(Long studentId, Payment payment) { if (registeredStudents.size() >= maxCapacity.getValue()) { @@ -27,7 +35,7 @@ protected void validateRegistration(Long studentId, Payment payment) { if (!studentId.equals(payment.getNsUserId())) { throw new IllegalArgumentException("결제한 사용자와 일치하지 않습니다."); } - if (this.tuitionFee.getAmount() != payment.getAmount()) { + if (!this.tuitionFee.isSameAmount(payment.getAmount())) { throw new IllegalArgumentException("결제 금액과 일치하지 않습니다."); } } diff --git a/src/main/java/nextstep/courses/domain/Period.java b/src/main/java/nextstep/courses/domain/Period.java index ca47de8b49..6b4eced958 100644 --- a/src/main/java/nextstep/courses/domain/Period.java +++ b/src/main/java/nextstep/courses/domain/Period.java @@ -1,6 +1,7 @@ package nextstep.courses.domain; import java.time.LocalDate; +import java.util.Objects; public class Period { private final LocalDate startDate; @@ -21,4 +22,18 @@ public LocalDate getStartDate() { public LocalDate getEndDate() { return endDate; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Period)) return false; + Period period = (Period) o; + return Objects.equals(startDate, period.startDate) && + Objects.equals(endDate, period.endDate); + } + + @Override + public int hashCode() { + return Objects.hash(startDate, endDate); + } } diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java index 25a86af5b9..bfb6617f4a 100644 --- a/src/main/java/nextstep/courses/domain/Session.java +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -2,15 +2,16 @@ import nextstep.payments.domain.Payment; -import java.util.HashSet; -import java.util.Set; +import java.util.List; +import java.util.stream.Collectors; + public abstract class Session { protected Long id; protected String name; protected Period period; protected Image coverImage; protected SessionStatus status; - protected Set registeredStudents = new HashSet<>(); + protected Students registeredStudents; public Session(Long id, String name, Period period, Image coverImage, SessionStatus status) { this.id = id; @@ -18,6 +19,36 @@ public Session(Long id, String name, Period period, Image coverImage, SessionSta this.period = period; this.coverImage = coverImage; this.status = status; + this.registeredStudents = new Students(); + } + public List getStudentIds() { + return registeredStudents.getStudents().stream() + .map(Student::getId) + .collect(Collectors.toList()); + } + + public Students getRegisteredStudent() { + return registeredStudents; + } + + public Image getCoverImage() { + return coverImage; + } + + public String getName() { + return name; + } + + public Period getPeriod() { + return period; + } + + public SessionStatus getStatus() { + return status; + } + + public Long getId() { + return id; } public void register(Long studentId, Payment payment) { @@ -26,11 +57,7 @@ public void register(Long studentId, Payment payment) { } validateRegistration(studentId, payment); - registeredStudents.add(studentId); - } - - public boolean isRegistered(Long studentId) { - return registeredStudents.contains(studentId); + registeredStudents.addStudent(new Student(studentId)); } protected abstract void validateRegistration(Long studentId, Payment payment); diff --git a/src/main/java/nextstep/courses/domain/Student.java b/src/main/java/nextstep/courses/domain/Student.java new file mode 100644 index 0000000000..f76d9877af --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Student.java @@ -0,0 +1,30 @@ +package nextstep.courses.domain; + +public class Student { + Long id; + public Student() { + } + public Student(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public boolean contains(Long studentId) { + return id.equals(studentId); + } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Student student = (Student) o; + return id != null && id.equals(student.id); + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } +} diff --git a/src/main/java/nextstep/courses/domain/Students.java b/src/main/java/nextstep/courses/domain/Students.java new file mode 100644 index 0000000000..65d29e1f3e --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Students.java @@ -0,0 +1,33 @@ +package nextstep.courses.domain; + +import java.util.HashSet; +import java.util.Set; +import java.util.Collections; + +public class Students { + private Set students = new HashSet<>(); + + public void addStudent(Student student) { + if (contains(student)) { + throw new IllegalArgumentException("이미 등록된 학생입니다."); + } + students.add(student); + } + + public void removeStudent(Student student) { + students.remove(student); + } + + public boolean contains(Student student) { + return students.stream() + .anyMatch(s -> s.equals(student)); + } + + public int size() { + return students.size(); + } + + public Set getStudents() { + return Collections.unmodifiableSet(students); + } +} diff --git a/src/main/java/nextstep/courses/domain/TuitionFee.java b/src/main/java/nextstep/courses/domain/TuitionFee.java index 27d3935c3d..f7991f4cf3 100644 --- a/src/main/java/nextstep/courses/domain/TuitionFee.java +++ b/src/main/java/nextstep/courses/domain/TuitionFee.java @@ -1,5 +1,7 @@ package nextstep.courses.domain; +import java.util.Objects; + public class TuitionFee { private final int amount; @@ -10,7 +12,24 @@ public TuitionFee(int amount) { this.amount = amount; } - public int getAmount() { + public boolean isSameAmount(int amount) { + return this.amount == amount; + } + + public int getValue() { return amount; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TuitionFee)) return false; + TuitionFee that = (TuitionFee) o; + return amount == that.amount; + } + + @Override + public int hashCode() { + return Objects.hash(amount); + } } \ No newline at end of file diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java new file mode 100644 index 0000000000..ec5cdb063f --- /dev/null +++ b/src/main/java/nextstep/courses/infrastructure/JdbcSessionRepository.java @@ -0,0 +1,174 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.Image; +import nextstep.courses.domain.Period; +import nextstep.courses.domain.Session; +import nextstep.courses.domain.SessionStatus; +import nextstep.courses.domain.PaidSession; +import nextstep.courses.domain.FreeSession; + +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.stereotype.Repository; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Repository +public class JdbcSessionRepository { + private final NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private final JdbcTemplate jdbcTemplate; + + public JdbcSessionRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + } + + @Transactional + public void save(Session session, Long courseId) { + Long imageId = null; + if (session.getCoverImage() != null) { + imageId = insertImage(session.getCoverImage()); + } + + SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("session") + .usingGeneratedKeyColumns("id"); + + Map params = new HashMap<>(); + params.put("name", session.getName()); + params.put("type", session.getType().name()); + params.put("start_date", session.getPeriod().getStartDate()); + params.put("end_date", session.getPeriod().getEndDate()); + params.put("status", session.getStatus().name()); + params.put("capacity", session instanceof PaidSession ? ((PaidSession) session).getCapacity().getValue() : null); + params.put("tuition_fee", session instanceof PaidSession ? ((PaidSession) session).getTuitionFee().getValue() : null); + params.put("image_id", imageId); + params.put("course_id", courseId); + + insert.execute(params); + } + + @Transactional + public void registerStudent(Long sessionId, Long studentId) { + String sql = "insert into session_student (session_id, student_id) values (:sessionId, :studentId)"; + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("sessionId", sessionId) + .addValue("studentId", studentId); + namedParameterJdbcTemplate.update(sql, params); + } + + public List findAll() { + String sql = "select s.*, i.file_name, i.content_type, i.size_in_bytes, i.width, i.height " + + "from session s left join image i on s.image_id = i.id"; + List sessions = namedParameterJdbcTemplate.query(sql, sessionRowMapper()); + + // 각 세션에 대한 수강생 정보를 함께 조회 + for (Session session : sessions) { + List studentIds = findStudentIdsBySessionId(session.getId()); + for (Long studentId : studentIds) { + session.register(studentId, null); + } + } + return sessions; + } + + public Session findById(Long id) { + String sql = "select s.*, i.file_name, i.content_type, i.size_in_bytes, i.width, i.height " + + "from session s left join image i on s.image_id = i.id where s.id = :id"; + Session session = namedParameterJdbcTemplate.queryForObject(sql, + new MapSqlParameterSource("id", id), + sessionRowMapper()); + + if (session != null) { + // 세션의 수강생 정보를 함께 조회 + List studentIds = findStudentIdsBySessionId(id); + for (Long studentId : studentIds) { + session.register(studentId, null); + } + } + return session; + } + + @Transactional + public void update(Session session) { + String sql = "update session set name = :name, start_date = :startDate, end_date = :endDate, " + + "status = :status, capacity = :capacity, tuition_fee = :tuitionFee where id = :id"; + + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("name", session.getName()) + .addValue("startDate", session.getPeriod().getStartDate()) + .addValue("endDate", session.getPeriod().getEndDate()) + .addValue("status", session.getStatus().name()) + .addValue("capacity", session instanceof PaidSession ? ((PaidSession) session).getCapacity().getValue() : null) + .addValue("tuitionFee", session instanceof PaidSession ? ((PaidSession) session).getTuitionFee().isSameAmount(0) ? 0 : null : null) + .addValue("id", session.getId()); + + namedParameterJdbcTemplate.update(sql, params); + } + + @Transactional + public void deleteById(Long id) { + deleteAllStudentsBySessionId(id); + String sql = "delete from session where id = :id"; + namedParameterJdbcTemplate.update(sql, new MapSqlParameterSource("id", id)); + } + + private Long insertImage(Image image) { + SimpleJdbcInsert insert = new SimpleJdbcInsert(jdbcTemplate) + .withTableName("image") + .usingGeneratedKeyColumns("id"); + + Map params = new HashMap<>(); + params.put("file_name", image.getFileName()); + params.put("content_type", image.getContentType()); + params.put("size_in_bytes", image.getSizeInBytes()); + params.put("width", image.getWidth()); + params.put("height", image.getHeight()); + + return insert.executeAndReturnKey(params).longValue(); + } + + private List findStudentIdsBySessionId(Long sessionId) { + String sql = "select student_id from session_student where session_id = :sessionId"; + return namedParameterJdbcTemplate.query(sql, + new MapSqlParameterSource("sessionId", sessionId), + (rs, rowNum) -> rs.getLong("student_id")); + } + + private void deleteAllStudentsBySessionId(Long sessionId) { + String sql = "delete from session_student where session_id = :sessionId"; + namedParameterJdbcTemplate.update(sql, new MapSqlParameterSource("sessionId", sessionId)); + } + + private RowMapper sessionRowMapper() { + return (rs, rowNum) -> { + SessionStatus status = SessionStatus.valueOf(rs.getString("status")); + Period period = new Period(rs.getDate("start_date").toLocalDate(), rs.getDate("end_date").toLocalDate()); + + Image image = null; + if (rs.getString("file_name") != null) { + image = new Image( + rs.getString("file_name"), + rs.getString("content_type"), + rs.getLong("size_in_bytes"), + rs.getInt("width"), + rs.getInt("height") + ); + } + + String type = rs.getString("type"); + if (type.equals("FREE")) { + return new FreeSession(rs.getLong("id"), rs.getString("name"), period, image, status); + } else { + return new PaidSession(rs.getLong("id"), rs.getString("name"), period, image, status, + rs.getInt("capacity"), rs.getInt("tuition_fee")); + } + }; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 219fe02ad8..af5e9b2290 100755 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,5 +2,7 @@ spring.h2.console.enabled=true spring.datasource.url=jdbc:h2:mem://localhost/~/java-lms;DB_CLOSE_ON_EXIT=FALSE +spring.sql.init.mode=always + logging.level.org.springframework.jdbc.core=TRACE logging.level.org.springframework.jdbc.datasource.init=TRACE diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8d5a988c8b..a3848dde55 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -48,3 +48,40 @@ create table delete_history ( deleted_by_id bigint, primary key (id) ); + +-- 추가 +create table image +( + id bigint generated by default as identity primary key, + file_name varchar(255) not null, + content_type varchar(50) not null, + size_in_bytes bigint not null, + width int not null, + height int not null +); + +create table session +( + id bigint generated by default as identity primary key, + name varchar(255) not null, + type varchar(10) not null, -- FREE, PAID + start_date date not null, + end_date date not null, + status varchar(20) not null, -- RECRUITING 등 + capacity int, -- PaidSession 전용 + tuition_fee int, -- PaidSession 전용 + image_id bigint, + course_id bigint not null, + foreign key (image_id) references image (id), + foreign key (course_id) references course (id) +); + +create table session_student +( + session_id bigint not null, + student_id bigint not null, + + primary key (session_id, student_id), + foreign key (session_id) references session (id), + foreign key (student_id) references ns_user (id) +); \ No newline at end of file diff --git a/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java new file mode 100644 index 0000000000..82b1e374bc --- /dev/null +++ b/src/test/java/nextstep/courses/infrastructure/SessionRepositoryTest.java @@ -0,0 +1,134 @@ +package nextstep.courses.infrastructure; + +import nextstep.courses.domain.FreeSession; +import nextstep.courses.domain.PaidSession; +import nextstep.courses.domain.Image; +import nextstep.courses.domain.Period; +import nextstep.courses.domain.Session; +import nextstep.courses.domain.SessionStatus; + +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.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +public class SessionRepositoryTest { + private JdbcSessionRepository sessionRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + sessionRepository = new JdbcSessionRepository(jdbcTemplate); + + // 테이블 초기화 + jdbcTemplate.update("delete from session_student"); + jdbcTemplate.update("delete from session"); + jdbcTemplate.update("delete from image"); + jdbcTemplate.update("delete from ns_user"); + jdbcTemplate.update("delete from course"); + + // 테스트용 사용자 추가 + jdbcTemplate.update("insert into ns_user (user_id, password, name, created_at) " + + "values ('test100', 'password', 'Test User', current_timestamp)"); + + // 테스트용 강의 추가 + jdbcTemplate.update("insert into course (id, title, creator_id, created_at) " + + "values (1, '샘플 강의', 42, current_timestamp)"); + } + + @Test + @DisplayName("CRUD 테스트") + void crudTest() { + Long courseId = 1L; + + // Create - Free Session + FreeSession freeSession = new FreeSession(null, "무료 세션", + new Period(LocalDate.of(2025, 5, 1), LocalDate.of(2025, 5, 10)), + new Image("free.jpg", "jpeg", 100000, 600, 400), + SessionStatus.RECRUITING); + sessionRepository.save(freeSession, courseId); + + // Create - Paid Session + PaidSession paidSession = new PaidSession(null, "유료 세션", + new Period(LocalDate.of(2025, 6, 1), LocalDate.of(2025, 6, 10)), + new Image("paid.jpg", "jpeg", 200000, 600, 400), + SessionStatus.RECRUITING, + 30, 100000); + sessionRepository.save(paidSession, courseId); + + // Read (findAll) + List sessions = sessionRepository.findAll(); + assertThat(sessions).hasSize(2); + assertThat(sessions.get(0).getName()).isEqualTo("무료 세션"); + assertThat(sessions.get(1).getName()).isEqualTo("유료 세션"); + + // Read (findById) + Session loadedFree = sessionRepository.findById(sessions.get(0).getId()); + assertThat(loadedFree.getName()).isEqualTo("무료 세션"); + assertThat(loadedFree).isInstanceOf(FreeSession.class); + + Session loadedPaid = sessionRepository.findById(sessions.get(1).getId()); + assertThat(loadedPaid.getName()).isEqualTo("유료 세션"); + assertThat(loadedPaid).isInstanceOf(PaidSession.class); + assertThat(((PaidSession) loadedPaid).getCapacity().getValue()).isEqualTo(30); + assertThat(((PaidSession) loadedPaid).getTuitionFee().isSameAmount(100000)).isTrue(); + + // Update + Session updatedSession = new FreeSession( + loadedFree.getId(), + "수정된 무료 세션", + loadedFree.getPeriod(), + loadedFree.getCoverImage(), + loadedFree.getStatus() + ); + sessionRepository.update(updatedSession); + Session updated = sessionRepository.findById(loadedFree.getId()); + assertThat(updated.getName()).isEqualTo("수정된 무료 세션"); + + // Delete + sessionRepository.deleteById(loadedFree.getId()); + sessionRepository.deleteById(loadedPaid.getId()); + List afterDelete = sessionRepository.findAll(); + assertThat(afterDelete).isEmpty(); + } + + @Test + @DisplayName("학생 등록 테스트") + void registerStudentTest() { + Long courseId = 1L; + + // 테스트용 학생 ID 조회 + Long studentId = jdbcTemplate.queryForObject( + "select id from ns_user where user_id = 'test100'", Long.class); + + // Create session + FreeSession session = new FreeSession(null, "무료 세션", + new Period(LocalDate.of(2025, 5, 1), LocalDate.of(2025, 5, 10)), + null, // 이미지는 선택사항으로 처리 + SessionStatus.RECRUITING); + sessionRepository.save(session, courseId); + + // Get session ID + List sessions = sessionRepository.findAll(); + Long sessionId = sessions.get(0).getId(); + + // Register student + sessionRepository.registerStudent(sessionId, studentId); + + // Verify registration + Session loadedSession = sessionRepository.findById(sessionId); + List registeredStudents = loadedSession.getStudentIds(); + assertThat(registeredStudents).contains(studentId); + } +} diff --git a/src/test/java/nextstep/qna/domain/SessionTest.java b/src/test/java/nextstep/qna/domain/SessionTest.java index 98e285acd3..06de4283ae 100644 --- a/src/test/java/nextstep/qna/domain/SessionTest.java +++ b/src/test/java/nextstep/qna/domain/SessionTest.java @@ -36,7 +36,7 @@ void registerSession() { SessionStatus.RECRUITING ); session.register(1L, new Payment()); - assertThat(session.isRegistered(1L)).isTrue(); + assertThat(session.getRegisteredStudent().contains(new Student(1L))).isTrue(); } @Test diff --git a/src/test/java/nextstep/qna/domain/StudentsTest.java b/src/test/java/nextstep/qna/domain/StudentsTest.java new file mode 100644 index 0000000000..7807194ee7 --- /dev/null +++ b/src/test/java/nextstep/qna/domain/StudentsTest.java @@ -0,0 +1,44 @@ +package nextstep.qna.domain; + +import nextstep.courses.domain.Capacity; +import nextstep.courses.domain.Student; +import nextstep.courses.domain.Students; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +public class StudentsTest { + + @Test + @DisplayName("같은 id를 가진 학생을 두 번 추가할 수 없다.") + void cannotRegisteredWithSameId() { + Students students = new Students(); + students.addStudent(new Student(1L)); + assertThatThrownBy(() -> students.addStudent(new Student(1L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("이미 등록된 학생입니다."); + } + + @Test + void checkRegisteredStudents() { + Students students = new Students(); + students.addStudent(new Student(1L)); + assertTrue(students.contains(new Student(1L))); + assertFalse(students.contains(new Student(99L))); + } + + @Test + @DisplayName("학생을 추가하고 제거할 수 있다.") + void addAndRemoveStudent() { + Students students = new Students(); + + Student student = new Student(1L); + students.addStudent(student); + assertTrue(students.contains(student)); + + students.removeStudent(student); + assertFalse(students.contains(student)); + } +}