diff --git a/reservation/Dockerfile b/reservation/Dockerfile
index 4832775a..57aefbbe 100644
--- a/reservation/Dockerfile
+++ b/reservation/Dockerfile
@@ -2,15 +2,31 @@ FROM eclipse-temurin:17-jdk AS builder
WORKDIR /app
+# Gradle wrapper 및 설정 파일 복사
COPY gradlew .
COPY gradle gradle
-COPY authorization-shared authorization-shared
-COPY reservation reservation
+COPY build.gradle .
-RUN echo "include 'reservation'" > settings.gradle && \
- echo "include 'authorization-shared'" >> settings.gradle
+# reservation 빌드를 위한 settings.gradle 생성 (필요 모듈만 포함)
+RUN printf "include 'reservation'\ninclude 'authorization-shared'\ninclude 'event-schema-shared'\n" > settings.gradle
+# 의존성 다운로드를 위한 빌드 파일 복사
+COPY authorization-shared/build.gradle authorization-shared/build.gradle
+COPY event-schema-shared/build.gradle event-schema-shared/build.gradle
+COPY reservation/build.gradle reservation/build.gradle
+
+# Gradle wrapper 실행 권한 부여
RUN chmod +x gradlew
+
+# 의존성 다운로드
+RUN ./gradlew :reservation:dependencies --no-daemon || true
+
+# 전체 소스 코드 복사
+COPY authorization-shared authorization-shared
+COPY event-schema-shared event-schema-shared
+COPY reservation reservation
+
+# 빌드 실행
RUN ./gradlew :reservation:clean :reservation:build -x test --no-daemon
diff --git a/reservation/build.gradle b/reservation/build.gradle
index 4c843bd8..fcaf97df 100644
--- a/reservation/build.gradle
+++ b/reservation/build.gradle
@@ -40,6 +40,8 @@ jacocoTestReport {
}
dependencies {
+ // === Event Schema Shared 모듈 ===
+ implementation project(':event-schema-shared')
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
@@ -56,6 +58,10 @@ dependencies {
// === 롬복 의존성 ===
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
+
+ // === Kafka 의존성 ===
+ implementation 'org.springframework.kafka:spring-kafka'
+ testImplementation 'org.springframework.kafka:spring-kafka-test'
}
tasks.named('test') {
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/event/infrastructure/EventConfig.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/event/infrastructure/EventConfig.java
new file mode 100644
index 00000000..88766fd2
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/event/infrastructure/EventConfig.java
@@ -0,0 +1,21 @@
+package net.catsnap.CatsnapReservation.event.infrastructure;
+
+import net.catsnap.shared.application.EventDeserializer;
+import net.catsnap.shared.infrastructure.AvroEventDeserializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 이벤트 처리 관련 빈 설정
+ *
+ *
Kafka Consumer가 수신한 EventEnvelope의 payload를 도메인 이벤트로 변환할 때 사용됩니다.
+ *
+ */
+@Configuration
+public class EventConfig {
+
+ @Bean
+ public EventDeserializer eventDeserializer() {
+ return new AvroEventDeserializer();
+ }
+}
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/application/PhotographerScheduleService.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/application/PhotographerScheduleService.java
new file mode 100644
index 00000000..793471e6
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/application/PhotographerScheduleService.java
@@ -0,0 +1,45 @@
+package net.catsnap.CatsnapReservation.schedule.application;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.catsnap.CatsnapReservation.schedule.domain.PhotographerSchedule;
+import net.catsnap.CatsnapReservation.schedule.infrastructure.repository.PhotographerScheduleRepository;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 사진작가 스케줄 관리 서비스
+ *
+ * 사진작가의 예약 가능 스케줄을 생성하고 관리합니다.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PhotographerScheduleService {
+
+ private final PhotographerScheduleRepository photographerScheduleRepository;
+
+ /**
+ * 사진작가의 기본 스케줄을 생성합니다.
+ *
+ * 이미 스케줄이 존재하면 아무 작업도 수행하지 않습니다 (멱등성 보장).
+ * 동시 요청으로 인한 중복 생성 시도 시 DataIntegrityViolationException을 catch하여 정상 처리합니다.
+ *
+ * @param photographerId 사진작가 ID
+ */
+ @Transactional
+ public void createDefaultSchedule(Long photographerId) {
+ if (photographerScheduleRepository.existsByPhotographerId(photographerId)) {
+ return;
+ }
+
+ try {
+ PhotographerSchedule schedule = PhotographerSchedule.initSchedule(photographerId);
+ photographerScheduleRepository.saveAndFlush(schedule);
+ log.info("Schedule created: photographerId={}", photographerId);
+ } catch (DataIntegrityViolationException e) {
+ log.info("Schedule already created by concurrent request: photographerId={}", photographerId);
+ }
+ }
+}
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/infrastructure/event/PhotographerCreatedEventConsumer.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/infrastructure/event/PhotographerCreatedEventConsumer.java
new file mode 100644
index 00000000..6f48ef0c
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/infrastructure/event/PhotographerCreatedEventConsumer.java
@@ -0,0 +1,92 @@
+package net.catsnap.CatsnapReservation.schedule.infrastructure.event;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.catsnap.CatsnapReservation.schedule.application.PhotographerScheduleService;
+import net.catsnap.event.photographer.v1.PhotographerCreated;
+import net.catsnap.event.shared.EventEnvelope;
+import net.catsnap.shared.application.EventDeserializer;
+import org.springframework.kafka.annotation.DltHandler;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.annotation.RetryableTopic;
+import org.springframework.kafka.retrytopic.DltStrategy;
+import org.springframework.kafka.support.KafkaHeaders;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.retry.annotation.Backoff;
+import org.springframework.stereotype.Component;
+import java.nio.ByteBuffer;
+
+/**
+ * PhotographerCreated 이벤트 Kafka Consumer
+ *
+ * member-service에서 사진작가가 생성되면 해당 이벤트를 수신하여
+ * 기본 스케줄을 생성합니다.
+ *
+ * 재시도 정책
+ *
+ * - 최대 3회 시도 (원본 1회 + 재시도 2회)
+ * - 재시도 간격: 5초 → 10초 (exponential backoff)
+ * - 최종 실패 시 DLT(Dead Letter Topic)로 이동
+ *
+ *
+ * 생성되는 토픽
+ *
+ * - PhotographerCreated (원본)
+ * - PhotographerCreated-retry-0 (5초 후 재시도)
+ * - PhotographerCreated-retry-1 (10초 후 재시도)
+ * - PhotographerCreated-dlt (최종 실패)
+ *
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class PhotographerCreatedEventConsumer {
+
+ private final PhotographerScheduleService photographerScheduleService;
+ private final EventDeserializer eventDeserializer;
+
+ /**
+ * PhotographerCreated 이벤트를 처리합니다.
+ *
+ * @param envelope Kafka로부터 수신한 이벤트 envelope
+ */
+ @RetryableTopic(
+ attempts = "3",
+ backoff = @Backoff(delay = 5000, multiplier = 2),
+ dltStrategy = DltStrategy.FAIL_ON_ERROR,
+ autoCreateTopics = "true"
+ )
+ @KafkaListener(topics = "PhotographerCreated", groupId = "schedule-create")
+ public void consume(EventEnvelope envelope) {
+ log.info("Received PhotographerCreated event: eventId={}, aggregateId={}",
+ envelope.getEventId(), envelope.getAggregateId());
+
+ ByteBuffer buffer = envelope.getPayload();
+ byte[] payloadBytes = new byte[buffer.remaining()];
+ buffer.duplicate().get(payloadBytes);
+
+ PhotographerCreated event = eventDeserializer.deserialize(
+ payloadBytes,
+ PhotographerCreated.class
+ );
+
+ photographerScheduleService.createDefaultSchedule(event.getPhotographerId());
+ }
+
+ /**
+ * DLT(Dead Letter Topic)로 이동된 메시지를 처리합니다.
+ *
+ * 재시도 횟수를 모두 소진한 메시지가 이 핸들러로 전달됩니다.
+ * 현재는 로깅만 수행하며, 추후 알림 연동이 필요합니다.
+ *
+ * @param envelope 처리 실패한 이벤트 envelope
+ * @param errorMessage 실패 원인 메시지
+ */
+ @DltHandler
+ public void handleDlt(EventEnvelope envelope,
+ @Header(KafkaHeaders.EXCEPTION_MESSAGE) String errorMessage) {
+ log.error("DLT 이동 - eventId: {}, aggregateId: {}, error: {}",
+ envelope.getEventId(), envelope.getAggregateId(), errorMessage);
+ // TODO: Slack/Discord 알림 연동
+ }
+}
diff --git a/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/infrastructure/repository/PhotographerScheduleRepository.java b/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/infrastructure/repository/PhotographerScheduleRepository.java
new file mode 100644
index 00000000..504d449f
--- /dev/null
+++ b/reservation/src/main/java/net/catsnap/CatsnapReservation/schedule/infrastructure/repository/PhotographerScheduleRepository.java
@@ -0,0 +1,27 @@
+package net.catsnap.CatsnapReservation.schedule.infrastructure.repository;
+
+import java.util.Optional;
+import net.catsnap.CatsnapReservation.schedule.domain.PhotographerSchedule;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/**
+ * 사진작가 스케줄 Repository
+ */
+public interface PhotographerScheduleRepository extends JpaRepository {
+
+ /**
+ * 사진작가 ID로 스케줄을 조회합니다.
+ *
+ * @param photographerId 사진작가 ID
+ * @return 스케줄 (존재하지 않으면 empty)
+ */
+ Optional findByPhotographerId(Long photographerId);
+
+ /**
+ * 사진작가 ID로 스케줄 존재 여부를 확인합니다.
+ *
+ * @param photographerId 사진작가 ID
+ * @return 존재하면 true, 없으면 false
+ */
+ boolean existsByPhotographerId(Long photographerId);
+}
\ No newline at end of file
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/application/PhotographerScheduleServiceIntegrationTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/application/PhotographerScheduleServiceIntegrationTest.java
new file mode 100644
index 00000000..82be2f2b
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/application/PhotographerScheduleServiceIntegrationTest.java
@@ -0,0 +1,101 @@
+package net.catsnap.CatsnapReservation.schedule.application;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Optional;
+import net.catsnap.CatsnapReservation.schedule.domain.PhotographerSchedule;
+import net.catsnap.CatsnapReservation.schedule.fixture.PhotographerScheduleFixture;
+import net.catsnap.CatsnapReservation.schedule.infrastructure.repository.PhotographerScheduleRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+
+@DisplayName("PhotographerScheduleService 통합 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+@SpringBootTest
+@Transactional
+class PhotographerScheduleServiceIntegrationTest {
+
+ @Autowired
+ private PhotographerScheduleService photographerScheduleService;
+
+ @Autowired
+ private PhotographerScheduleRepository photographerScheduleRepository;
+
+ @BeforeEach
+ void setUp() {
+ photographerScheduleRepository.deleteAll();
+ }
+
+ @Test
+ void 새로운_photographerId로_기본_스케줄을_생성한다() {
+ // given
+ Long photographerId = 1L;
+
+ // when
+ photographerScheduleService.createDefaultSchedule(photographerId);
+
+ // then
+ Optional found = photographerScheduleRepository.findByPhotographerId(photographerId);
+ assertThat(found).isPresent();
+ assertThat(found.get().getPhotographerId()).isEqualTo(photographerId);
+ assertThat(found.get().getWeekdayRules()).hasSize(7);
+ }
+
+ @Test
+ void 이미_스케줄이_존재하면_중복_생성하지_않는다() {
+ // given
+ Long photographerId = 1L;
+ PhotographerSchedule existingSchedule = PhotographerScheduleFixture.createWithPhotographerId(photographerId);
+ photographerScheduleRepository.save(existingSchedule);
+ Long originalId = existingSchedule.getId();
+
+ // when
+ photographerScheduleService.createDefaultSchedule(photographerId);
+
+ // then
+ Optional found = photographerScheduleRepository.findByPhotographerId(photographerId);
+ assertThat(found).isPresent();
+ assertThat(found.get().getId()).isEqualTo(originalId);
+ assertThat(photographerScheduleRepository.count()).isEqualTo(1);
+ }
+
+ @Test
+ void 동일한_photographerId로_여러번_호출해도_스케줄은_하나만_존재한다() {
+ // given
+ Long photographerId = 1L;
+
+ // when
+ photographerScheduleService.createDefaultSchedule(photographerId);
+ photographerScheduleService.createDefaultSchedule(photographerId);
+ photographerScheduleService.createDefaultSchedule(photographerId);
+
+ // then
+ assertThat(photographerScheduleRepository.count()).isEqualTo(1);
+ }
+
+ @Test
+ void 서로_다른_photographerId로_호출하면_각각_스케줄이_생성된다() {
+ // given
+ Long photographerId1 = 1L;
+ Long photographerId2 = 2L;
+ Long photographerId3 = 3L;
+
+ // when
+ photographerScheduleService.createDefaultSchedule(photographerId1);
+ photographerScheduleService.createDefaultSchedule(photographerId2);
+ photographerScheduleService.createDefaultSchedule(photographerId3);
+
+ // then
+ assertThat(photographerScheduleRepository.count()).isEqualTo(3);
+ assertThat(photographerScheduleRepository.findByPhotographerId(photographerId1)).isPresent();
+ assertThat(photographerScheduleRepository.findByPhotographerId(photographerId2)).isPresent();
+ assertThat(photographerScheduleRepository.findByPhotographerId(photographerId3)).isPresent();
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/fixture/PhotographerScheduleFixture.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/fixture/PhotographerScheduleFixture.java
new file mode 100644
index 00000000..f7ab2961
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/fixture/PhotographerScheduleFixture.java
@@ -0,0 +1,49 @@
+package net.catsnap.CatsnapReservation.schedule.fixture;
+
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.util.List;
+import net.catsnap.CatsnapReservation.schedule.domain.PhotographerSchedule;
+import net.catsnap.CatsnapReservation.schedule.domain.vo.AvailableStartTimes;
+
+/**
+ * PhotographerSchedule 테스트용 Fixture
+ */
+public class PhotographerScheduleFixture {
+
+ public static final Long DEFAULT_PHOTOGRAPHER_ID = 1L;
+
+ /**
+ * 기본 스케줄 생성 (모든 요일 휴무)
+ */
+ public static PhotographerSchedule createDefault() {
+ return PhotographerSchedule.initSchedule(DEFAULT_PHOTOGRAPHER_ID);
+ }
+
+ /**
+ * 지정된 photographerId로 기본 스케줄 생성
+ */
+ public static PhotographerSchedule createWithPhotographerId(Long photographerId) {
+ return PhotographerSchedule.initSchedule(photographerId);
+ }
+
+ /**
+ * 평일 근무 스케줄 생성 (월~금 9:00, 10:00, 11:00)
+ */
+ public static PhotographerSchedule createWeekdaySchedule(Long photographerId) {
+ PhotographerSchedule schedule = PhotographerSchedule.initSchedule(photographerId);
+ AvailableStartTimes workingTimes = AvailableStartTimes.of(List.of(
+ LocalTime.of(9, 0),
+ LocalTime.of(10, 0),
+ LocalTime.of(11, 0)
+ ));
+
+ schedule.updateWeekdayRule(DayOfWeek.MONDAY, workingTimes);
+ schedule.updateWeekdayRule(DayOfWeek.TUESDAY, workingTimes);
+ schedule.updateWeekdayRule(DayOfWeek.WEDNESDAY, workingTimes);
+ schedule.updateWeekdayRule(DayOfWeek.THURSDAY, workingTimes);
+ schedule.updateWeekdayRule(DayOfWeek.FRIDAY, workingTimes);
+
+ return schedule;
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/infrastructure/event/PhotographerCreatedEventConsumerTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/infrastructure/event/PhotographerCreatedEventConsumerTest.java
new file mode 100644
index 00000000..50055b87
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/infrastructure/event/PhotographerCreatedEventConsumerTest.java
@@ -0,0 +1,107 @@
+package net.catsnap.CatsnapReservation.schedule.infrastructure.event;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.never;
+
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Map;
+import net.catsnap.CatsnapReservation.schedule.application.PhotographerScheduleService;
+import net.catsnap.event.photographer.v1.PhotographerCreated;
+import net.catsnap.event.shared.EventEnvelope;
+import net.catsnap.shared.application.EventDeserializer;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@DisplayName("PhotographerCreatedEventConsumer 단위 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+@ExtendWith(MockitoExtension.class)
+class PhotographerCreatedEventConsumerTest {
+
+ @InjectMocks
+ private PhotographerCreatedEventConsumer consumer;
+
+ @Mock
+ private PhotographerScheduleService photographerScheduleService;
+
+ @Mock
+ private EventDeserializer eventDeserializer;
+
+ @Test
+ void 이벤트를_수신하면_스케줄_생성_서비스를_호출한다() {
+ // given
+ Long photographerId = 1L;
+ EventEnvelope envelope = createEventEnvelope("event-1", "1");
+ PhotographerCreated event = new PhotographerCreated(photographerId);
+
+ given(eventDeserializer.deserialize(any(byte[].class), eq(PhotographerCreated.class)))
+ .willReturn(event);
+
+ // when
+ consumer.consume(envelope);
+
+ // then
+ then(photographerScheduleService).should().createDefaultSchedule(photographerId);
+ }
+
+ @Test
+ void 이벤트_페이로드를_역직렬화한다() {
+ // given
+ Long photographerId = 1L;
+ byte[] payloadBytes = "test-payload".getBytes();
+ EventEnvelope envelope = createEventEnvelope("event-1", "1", payloadBytes);
+ PhotographerCreated event = new PhotographerCreated(photographerId);
+
+ given(eventDeserializer.deserialize(any(byte[].class), eq(PhotographerCreated.class)))
+ .willReturn(event);
+
+ // when
+ consumer.consume(envelope);
+
+ // then
+ then(eventDeserializer).should()
+ .deserialize(argThat(bytes -> Arrays.equals(bytes, payloadBytes)), eq(PhotographerCreated.class));
+ }
+
+ @Test
+ void DLT_핸들러가_예외_없이_실행된다() {
+ // given
+ EventEnvelope envelope = createEventEnvelope("event-1", "1");
+ String errorMessage = "Test error message";
+
+ // when & then (예외 없이 실행되면 성공)
+ consumer.handleDlt(envelope, errorMessage);
+
+ // 서비스는 호출되지 않음
+ then(photographerScheduleService).should(never()).createDefaultSchedule(any());
+ }
+
+ private EventEnvelope createEventEnvelope(String eventId, String aggregateId) {
+ return createEventEnvelope(eventId, aggregateId, "payload".getBytes());
+ }
+
+ private EventEnvelope createEventEnvelope(String eventId, String aggregateId, byte[] payload) {
+ return EventEnvelope.newBuilder()
+ .setEventId(eventId)
+ .setEventType("PhotographerCreated")
+ .setAggregateId(aggregateId)
+ .setAggregateType("Photographer")
+ .setVersion(1)
+ .setTimestamp(Instant.now())
+ .setPayload(ByteBuffer.wrap(payload))
+ .setMetadata(Map.of())
+ .build();
+ }
+}
diff --git a/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/infrastructure/repository/PhotographerScheduleRepositoryTest.java b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/infrastructure/repository/PhotographerScheduleRepositoryTest.java
new file mode 100644
index 00000000..43601a6b
--- /dev/null
+++ b/reservation/src/test/java/net/catsnap/CatsnapReservation/schedule/infrastructure/repository/PhotographerScheduleRepositoryTest.java
@@ -0,0 +1,75 @@
+package net.catsnap.CatsnapReservation.schedule.infrastructure.repository;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Optional;
+import net.catsnap.CatsnapReservation.schedule.domain.PhotographerSchedule;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.DisplayNameGeneration;
+import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+
+@DisplayName("PhotographerScheduleRepository 테스트")
+@DisplayNameGeneration(ReplaceUnderscores.class)
+@SuppressWarnings("NonAsciiCharacters")
+@DataJpaTest
+class PhotographerScheduleRepositoryTest {
+
+ @Autowired
+ private PhotographerScheduleRepository photographerScheduleRepository;
+
+ @Test
+ void photographerId로_스케줄을_조회할_수_있다() {
+ // given
+ Long photographerId = 1L;
+ PhotographerSchedule schedule = PhotographerSchedule.initSchedule(photographerId);
+ photographerScheduleRepository.save(schedule);
+
+ // when
+ Optional found = photographerScheduleRepository.findByPhotographerId(photographerId);
+
+ // then
+ assertThat(found).isPresent();
+ assertThat(found.get().getPhotographerId()).isEqualTo(photographerId);
+ }
+
+ @Test
+ void 존재하지_않는_photographerId로_조회하면_빈_Optional을_반환한다() {
+ // given
+ Long nonExistentPhotographerId = 999L;
+
+ // when
+ Optional found = photographerScheduleRepository.findByPhotographerId(nonExistentPhotographerId);
+
+ // then
+ assertThat(found).isEmpty();
+ }
+
+ @Test
+ void photographerId로_스케줄_존재_여부를_확인할_수_있다() {
+ // given
+ Long photographerId = 1L;
+ PhotographerSchedule schedule = PhotographerSchedule.initSchedule(photographerId);
+ photographerScheduleRepository.save(schedule);
+
+ // when
+ boolean exists = photographerScheduleRepository.existsByPhotographerId(photographerId);
+
+ // then
+ assertThat(exists).isTrue();
+ }
+
+ @Test
+ void 존재하지_않는_photographerId로_존재_여부를_확인하면_false를_반환한다() {
+ // given
+ Long nonExistentPhotographerId = 999L;
+
+ // when
+ boolean exists = photographerScheduleRepository.existsByPhotographerId(nonExistentPhotographerId);
+
+ // then
+ assertThat(exists).isFalse();
+ }
+}
diff --git a/reservation/src/test/resources/application.yml b/reservation/src/test/resources/application.yml
index d11f286a..e169b7d3 100644
--- a/reservation/src/test/resources/application.yml
+++ b/reservation/src/test/resources/application.yml
@@ -1,4 +1,7 @@
spring:
+ test:
+ database:
+ replace: none
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;INIT=CREATE DOMAIN IF NOT EXISTS JSONB AS JSON;
driver-class-name: org.h2.Driver