├── .adr-dir ├── settings.gradle ├── run.sh ├── .junie └── guidelines.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── http ├── http-client.env.json ├── time-table.http ├── adjustment-result.http └── room.http ├── dockerfile.dev ├── src ├── test │ ├── resources │ │ ├── org │ │ │ └── springframework │ │ │ │ └── restdocs │ │ │ │ └── templates │ │ │ │ └── asciidoctor │ │ │ │ ├── curl-request.snippet │ │ │ │ ├── httpie-request.snippet │ │ │ │ ├── http-request.snippet │ │ │ │ ├── http-response.snippet │ │ │ │ ├── response-fields.snippet │ │ │ │ ├── query-parameters.snippet │ │ │ │ ├── README.md │ │ │ │ └── request-fields.snippet │ │ └── application.yaml │ └── java │ │ └── com │ │ └── dnd │ │ └── modutime │ │ ├── TestConstant.java │ │ ├── ModutimeApplicationTests.java │ │ ├── config │ │ └── TimeConfiguration.java │ │ ├── util │ │ ├── FakeTimeProvider.java │ │ └── TimerTest.java │ │ ├── core │ │ ├── adjustresult │ │ │ ├── domain │ │ │ │ ├── AdjustmentResultTest.java │ │ │ │ └── CandidateDateTimeTest.java │ │ │ ├── application │ │ │ │ └── AdjustmentResultEventHandlerTest.java │ │ │ └── util │ │ │ │ ├── sorter │ │ │ │ └── FastFirstSorterTest.java │ │ │ │ ├── convertor │ │ │ │ └── DateRoomConvertorTest.java │ │ │ │ └── executor │ │ │ │ └── AdjustmentResultExecutorFactoryTest.java │ │ ├── participant │ │ │ ├── domain │ │ │ │ ├── EmailTest.java │ │ │ │ └── ParticipantTest.java │ │ │ ├── application │ │ │ │ └── ParticipantFacadeTest.java │ │ │ └── integration │ │ │ │ └── ParticipantIntegrationTest.java │ │ ├── room │ │ │ ├── domain │ │ │ │ └── RoomDateTimeTest.java │ │ │ └── util │ │ │ │ └── CandidateDateTimeConvertorFactoryTest.java │ │ ├── timeblock │ │ │ ├── util │ │ │ │ └── DateTimeToAvailableDateTimeConvertorFactoryTest.java │ │ │ ├── domain │ │ │ │ ├── TimeBlockTest.java │ │ │ │ └── AvailableDateTimeTest.java │ │ │ ├── repository │ │ │ │ └── AvailableDateTimeRepositoryTest.java │ │ │ └── application │ │ │ │ └── TimeBlockEventHandlerTest.java │ │ └── timetable │ │ │ ├── domain │ │ │ └── TimeTableTest.java │ │ │ └── application │ │ │ └── TimeTableEventHandlerTest.java │ │ ├── documentation │ │ └── DocumentUtils.java │ │ ├── acceptance │ │ ├── request │ │ │ └── RoomRequestWithNoNull.java │ │ ├── ParticipantAcceptanceTest.java │ │ ├── AuthAcceptanceTest.java │ │ └── RoomAcceptanceTest.java │ │ ├── fixture │ │ ├── TimeTableFixture.java │ │ └── RoomRequestFixture.java │ │ └── annotation │ │ └── ApiDocsTest.java ├── main │ ├── java │ │ └── com │ │ │ └── dnd │ │ │ └── modutime │ │ │ ├── util │ │ │ ├── TimeProvider.java │ │ │ ├── RealTimeProvider.java │ │ │ ├── DateTimeConstants.java │ │ │ └── Timer.java │ │ │ ├── exception │ │ │ ├── NotFoundException.java │ │ │ └── InvalidPasswordException.java │ │ │ ├── core │ │ │ ├── participant │ │ │ │ ├── domain │ │ │ │ │ ├── ParticipantRepository.java │ │ │ │ │ ├── ParticipantRemovedEvent.java │ │ │ │ │ ├── ParticipantQueryRepository.java │ │ │ │ │ ├── Email.java │ │ │ │ │ └── Participants.java │ │ │ │ ├── application │ │ │ │ │ ├── request │ │ │ │ │ │ └── EmailCreationRequest.java │ │ │ │ │ ├── response │ │ │ │ │ │ └── EmailResponse.java │ │ │ │ │ ├── command │ │ │ │ │ │ ├── ParticipantCreateCommand.java │ │ │ │ │ │ └── ParticipantsDeleteCommand.java │ │ │ │ │ ├── ParticipantCommandHandler.java │ │ │ │ │ ├── ParticipantFacade.java │ │ │ │ │ └── ParticipantQueryService.java │ │ │ │ └── controller │ │ │ │ │ ├── dto │ │ │ │ │ └── ParticipantsDeleteRequest.java │ │ │ │ │ └── ParticipantCommandController.java │ │ │ ├── timeblock │ │ │ │ ├── domain │ │ │ │ │ ├── AvailableDateTimeValidator.java │ │ │ │ │ ├── TimeBlockReplaceEvent.java │ │ │ │ │ ├── TimeBlockRemovedEvent.java │ │ │ │ │ ├── AvailableTime.java │ │ │ │ │ └── AvailableDateTime.java │ │ │ │ ├── application │ │ │ │ │ ├── ParticipantCreationEvent.java │ │ │ │ │ ├── TimeReplaceValidator.java │ │ │ │ │ ├── request │ │ │ │ │ │ └── TimeReplaceRequest.java │ │ │ │ │ ├── TimeBlockEventHandler.java │ │ │ │ │ └── response │ │ │ │ │ │ └── TimeBlockResponse.java │ │ │ │ ├── util │ │ │ │ │ ├── DateTimeToAvailableDateTimeConvertor.java │ │ │ │ │ ├── DateTimeToAvailableDateTimeConvertorFactory.java │ │ │ │ │ ├── DateConvertor.java │ │ │ │ │ └── DateTimeConvertor.java │ │ │ │ ├── repository │ │ │ │ │ ├── AvailableDateTimeRepository.java │ │ │ │ │ └── TimeBlockRepository.java │ │ │ │ └── controller │ │ │ │ │ └── TimeBlockController.java │ │ │ ├── timetable │ │ │ │ ├── application │ │ │ │ │ ├── TimeTableInitializer.java │ │ │ │ │ ├── response │ │ │ │ │ │ ├── TimeAndCountPerDate.java │ │ │ │ │ │ ├── AvailableTimeInfo.java │ │ │ │ │ │ └── TimeTableResponse.java │ │ │ │ │ ├── TimeTableQueryService.java │ │ │ │ │ ├── TimeTableFacade.java │ │ │ │ │ ├── command │ │ │ │ │ │ └── TimeTableUpdateCommand.java │ │ │ │ │ ├── TimeTableEventHandler.java │ │ │ │ │ └── TimeTableService.java │ │ │ │ ├── repository │ │ │ │ │ ├── TimeTableRepository.java │ │ │ │ │ └── TimeInfoParticipantNameRepository.java │ │ │ │ ├── controller │ │ │ │ │ ├── dto │ │ │ │ │ │ └── AvailableTimeGroupRequest.java │ │ │ │ │ ├── TimeTableController.java │ │ │ │ │ └── TimeTableQueryController.java │ │ │ │ └── domain │ │ │ │ │ ├── view │ │ │ │ │ └── TimeTableSearchCondition.java │ │ │ │ │ ├── TimeTableReplaceEvent.java │ │ │ │ │ └── TimeInfoParticipantName.java │ │ │ ├── room │ │ │ │ ├── repository │ │ │ │ │ └── RoomRepository.java │ │ │ │ ├── application │ │ │ │ │ ├── request │ │ │ │ │ │ ├── TimerRequest.java │ │ │ │ │ │ └── RoomRequest.java │ │ │ │ │ ├── response │ │ │ │ │ │ ├── RoomCreationResponse.java │ │ │ │ │ │ ├── RoomInfoResponse.java │ │ │ │ │ │ └── V2RoomInfoResponse.java │ │ │ │ │ ├── RoomTimeValidator.java │ │ │ │ │ └── RoomTimeTableInitializer.java │ │ │ │ ├── util │ │ │ │ │ └── CandidateDateTimeConvertorFactory.java │ │ │ │ ├── domain │ │ │ │ │ └── RoomDate.java │ │ │ │ └── controller │ │ │ │ │ └── RoomController.java │ │ │ ├── adjustresult │ │ │ │ ├── repository │ │ │ │ │ ├── CandidateDateTimeRepository.java │ │ │ │ │ └── AdjustmentResultRepository.java │ │ │ │ ├── application │ │ │ │ │ ├── DateTimeInfoDto.java │ │ │ │ │ ├── command │ │ │ │ │ │ └── AdjustmentResultReplaceCommand.java │ │ │ │ │ ├── CandidateDateTimeSortStandard.java │ │ │ │ │ ├── condition │ │ │ │ │ │ └── AdjustmentResultSearchCondition.java │ │ │ │ │ ├── AdjustmentResultEventHandler.java │ │ │ │ │ ├── response │ │ │ │ │ │ ├── AdjustmentResultResponseV1.java │ │ │ │ │ │ ├── CandidateDateTimeResponse.java │ │ │ │ │ │ ├── AdjustmentResultResponse.java │ │ │ │ │ │ └── CandidateDateTimeResponseV1.java │ │ │ │ │ └── AdjustmentResultService.java │ │ │ │ ├── util │ │ │ │ │ ├── sorter │ │ │ │ │ │ ├── CandidateDateTimesSorter.java │ │ │ │ │ │ ├── FastFirstSorter.java │ │ │ │ │ │ ├── LongFirstSorter.java │ │ │ │ │ │ └── CandidateDateTimesSorterFactory.java │ │ │ │ │ ├── convertor │ │ │ │ │ │ ├── CandidateDateTimeConvertor.java │ │ │ │ │ │ └── DateRoomConvertor.java │ │ │ │ │ └── executor │ │ │ │ │ │ ├── AdjustmentResultResponseGenerator.java │ │ │ │ │ │ └── AdjustmentResultExecutorFactory.java │ │ │ │ ├── controller │ │ │ │ │ ├── dto │ │ │ │ │ │ └── AdjustmentResultRequest.java │ │ │ │ │ └── AdjustmentResultController.java │ │ │ │ └── domain │ │ │ │ │ ├── CandidateDateTimeParticipantName.java │ │ │ │ │ └── AdjustmentResult.java │ │ │ ├── auth │ │ │ │ ├── application │ │ │ │ │ ├── response │ │ │ │ │ │ └── LoginPageResponse.java │ │ │ │ │ └── request │ │ │ │ │ │ └── LoginRequest.java │ │ │ │ └── controller │ │ │ │ │ └── AuthController.java │ │ │ ├── Pageable.java │ │ │ ├── Page.java │ │ │ └── entity │ │ │ │ ├── Auditable.java │ │ │ │ └── BaseEntity.java │ │ │ ├── config │ │ │ ├── persistence │ │ │ │ ├── JpaConfig.java │ │ │ │ └── AuditorAwareConfig.java │ │ │ ├── HealthCheckController.java │ │ │ └── WebConfig.java │ │ │ ├── common │ │ │ ├── DisplayableEnum.java │ │ │ └── convert │ │ │ │ └── DisplayableEnumJsonConverter.java │ │ │ ├── ModutimeApplication.java │ │ │ ├── advice │ │ │ ├── response │ │ │ │ └── ExceptionResponse.java │ │ │ └── GlobalControllerAdvice.java │ │ │ └── infrastructure │ │ │ ├── persistence │ │ │ └── participant │ │ │ │ ├── ParticipantJpaRepository.java │ │ │ │ └── ParticipantJpaQueryRepository.java │ │ │ ├── PageRequest.java │ │ │ └── PageResponse.java │ └── resources │ │ ├── application.yaml │ │ ├── application-logging.yaml │ │ ├── application-db.yaml │ │ └── logback-spring.xml └── docs │ └── asciidoc │ ├── index.adoc │ ├── adjustment-result.adoc │ └── timetable.adoc ├── .github ├── ISSUE_TEMPLATE │ └── backend-issue-template.md ├── pull_request_template.md └── workflows │ └── ci-pull-request.yml ├── dockerfile ├── scripts └── ec2-swap-settings.sh ├── docker ├── DockerFile-local └── run.sh ├── .coderabbit.yaml ├── architecture-decision-records ├── readme.md ├── templates │ └── template.md ├── 0001-record-architecture-decisions.md └── 0002-api-docs-rest-docs.md ├── .gitignore ├── docker-compose-dev.yml ├── docker-compose.yml ├── install-docker.sh ├── README.md └── gradlew.bat /.adr-dir: -------------------------------------------------------------------------------- 1 | architecture-decision-records 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'modutime' 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | ./install-docker.sh 2 | 3 | docker-compose -f docker-compose-dev.yml up -------------------------------------------------------------------------------- /.junie/guidelines.md: -------------------------------------------------------------------------------- 1 | - 한국어를 주로 사용합니다. 2 | - 로직이 복잡한 메서드의 경우 한글 주석(javadoc)을 작성합니다. 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnd-side-project/dnd-8th-5-backend/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /http/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "host": "http://localhost:8080" 4 | }, 5 | "prod": { 6 | "host": "{}" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:17-alpine 2 | ARG JAR_FILE_PATH=build/libs/*.jar 3 | COPY ${JAR_FILE_PATH} app.jar 4 | EXPOSE 8080 5 | ENTRYPOINT ["java", "-jar", "app.jar"] 6 | -------------------------------------------------------------------------------- /http/time-table.http: -------------------------------------------------------------------------------- 1 | ### 타임 테이블 조회 2 | GET https://api2.modutime.site/api/room/41616ae0-c45a-4ea8-a468-9f664051fec8/available-time/overview 3 | ?participantNames=동호1,동호3 4 | Content-Type: application/json 5 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet: -------------------------------------------------------------------------------- 1 | .link:https://curl.se/docs/manual.html[`curl`] 명령어 2 | [source,bash] 3 | ---- 4 | $ curl {{url}} {{options}} 5 | ---- 6 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/util/TimeProvider.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.util; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public interface TimeProvider { 6 | 7 | LocalDateTime getCurrentLocalDateTime(); 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/TestConstant.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime; 2 | 3 | public class TestConstant { 4 | public static final String LOCALHOST = "127.0.0.1"; 5 | public static final String CLIENT_IP = "192.168.0.1"; 6 | } 7 | -------------------------------------------------------------------------------- /http/adjustment-result.http: -------------------------------------------------------------------------------- 1 | ### 우선순위 조회 2 | @roomUuid = 0N9SSTSH9G8KY 3 | 4 | GET https://{{host}}/api/v1/room/{{roomUuid}}/adjustment-results 5 | ?participantNames=동호,동호1 6 | &page=2 7 | &size=5 8 | Content-Type: application/json 9 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/httpie-request.snippet: -------------------------------------------------------------------------------- 1 | .link:https://httpie.io/docs/cli/usage[`httpie`] 명령어 2 | [source,bash] 3 | ---- 4 | ${{echoContent}} http {{options}} {{url}} {{requestItems}} 5 | ---- 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/backend-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: backend-issue-template 3 | about: 백엔드 이슈 템플릿 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## As-is 11 | 12 | 13 | ## To-be 14 | - [ ] 15 | - [ ] 16 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazoncorretto:17-alpine 2 | ARG JAR_FILE=build/libs/*.jar 3 | COPY ${JAR_FILE} app.jar 4 | ENTRYPOINT ["java", \ 5 | "-Xms256m", \ 6 | "-Xmx384m", \ 7 | "-Dspring.profiles.active=prod", \ 8 | "-jar", \ 9 | "/app.jar"] 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.exception; 2 | 3 | public class NotFoundException extends RuntimeException { 4 | public NotFoundException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/exception/InvalidPasswordException.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.exception; 2 | 3 | public class InvalidPasswordException extends RuntimeException { 4 | public InvalidPasswordException() { 5 | super("비밀번호가 일치하지 않습니다."); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet: -------------------------------------------------------------------------------- 1 | .HTTP 요청예제 2 | [source,http,options="nowrap"] 3 | ---- 4 | {{method}} {{path}} HTTP/1.1 5 | {{#headers}} 6 | {{name}}:{{value}} 7 | {{/headers}} 8 | 9 | {{requestBody}} 10 | ---- 11 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/domain/ParticipantRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | public interface ParticipantRepository { 4 | void delete(Participant participant); 5 | 6 | Participant save(Participant participant); 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet: -------------------------------------------------------------------------------- 1 | .HTTP 응답예제 2 | [source,http,options="nowrap"] 3 | ---- 4 | HTTP/1.1 {{statusCode}} {{statusReason}} 5 | {{#headers}} 6 | {{name}}:{{value}} 7 | {{/headers}} 8 | {{responseBody}} 9 | ---- 10 | -------------------------------------------------------------------------------- /http/room.http: -------------------------------------------------------------------------------- 1 | ### 약속 방 만들기 2 | POST https://{{host}}/api/room 3 | Content-Type: application/json 4 | 5 | { 6 | "title": "title_e1864bc11f21", 7 | "headCount": 5, 8 | "dates": [ 9 | "2025-10-20" 10 | ], 11 | "startTime": "14:30:00", 12 | "endTime": "15:30:00" 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/domain/AvailableDateTimeValidator.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import java.util.List; 4 | 5 | public interface AvailableDateTimeValidator { 6 | 7 | void validate(String roomUuid, List availableDateTimes); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/TimeTableInitializer.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application; 2 | 3 | import com.dnd.modutime.core.timetable.domain.TimeTable; 4 | 5 | public interface TimeTableInitializer { 6 | 7 | void initialize(String roomUuid, TimeTable timeTable); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: local 4 | include: 5 | - db 6 | - logging 7 | 8 | management: 9 | endpoints: 10 | web: 11 | exposure: 12 | include: health 13 | base-path: / 14 | endpoint: 15 | health: 16 | show-details: never 17 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/ModutimeApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ModutimeApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /scripts/ec2-swap-settings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # swap 1기가 설정 4 | sudo fallocate -l 1G /swapfile 5 | sudo chmod 600 /swapfile 6 | sudo mkswap /swapfile 7 | sudo swapon /swapfile 8 | echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab 9 | sudo sysctl vm.swappiness=10 10 | echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf 11 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/config/persistence/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.config.persistence; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @EnableJpaAuditing 7 | @Configuration 8 | public class JpaConfig { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet: -------------------------------------------------------------------------------- 1 | .응답필드 2 | [cols="2,1,1,4a"] 3 | |=== 4 | |필드|타입|필수값|설명 5 | 6 | {{#fields}} 7 | |{{path}} 8 | |{{type}} 9 | |{{^optional}}필수{{/optional}} 10 | |{{description}} + 11 | {{#format}}{{format}}{{/format}}{{^format}}{{/format}} 12 | {{/fields}} 13 | 14 | |=== 15 | -------------------------------------------------------------------------------- /docker/DockerFile-local: -------------------------------------------------------------------------------- 1 | FROM mysql:8.2.0 2 | 3 | # 환경 변수를 설정하여 root 사용자의 비밀번호를 설정합니다. 4 | ENV MYSQL_ROOT_PASSWORD=password 5 | 6 | # 사용자 계정 및 비밀번호를 설정합니다. 7 | ENV MYSQL_USER=user 8 | ENV MYSQL_PASSWORD=password 9 | 10 | # 초기 데이터베이스 이름을 설정합니다. 11 | ENV MYSQL_DATABASE=modutime 12 | 13 | # 컨테이너 외부에서 MySQL에 접근할 수 있도록 3306 포트를 노출합니다. 14 | EXPOSE 3306 15 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/common/DisplayableEnum.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.common; 2 | 3 | /** 4 | * 화면에 노출되는 Enum 을 일정한 형식으로 처리하기 위한 용도 5 | * 6 | * { 7 | * "code": "CODE", 8 | * "text": "코드문구" 9 | * } 10 | * 11 | */ 12 | public interface DisplayableEnum { 13 | String getCode(); 14 | 15 | String getText(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/domain/ParticipantRemovedEvent.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public class ParticipantRemovedEvent { 9 | 10 | private String roomUuid; 11 | private String name; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/application-logging.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | config: 3 | activate: 4 | on-profile: local 5 | logging: 6 | level: 7 | org: 8 | hibernate: 9 | type: 10 | descriptor: 11 | sql: 12 | BasicBinder: TRACE 13 | 14 | --- 15 | spring: 16 | config: 17 | activate: 18 | on-profile: prod 19 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/application/ParticipantCreationEvent.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.application; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public class ParticipantCreationEvent { 9 | 10 | private String roomUuid; 11 | private String name; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/application/TimeReplaceValidator.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.application; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import java.util.List; 5 | 6 | public interface TimeReplaceValidator { 7 | 8 | void validate(String roomUuid, List availableDateTimes); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/repository/RoomRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.repository; 2 | 3 | import com.dnd.modutime.core.room.domain.Room; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface RoomRepository extends JpaRepository { 8 | 9 | Optional findByUuid(String uuid); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/ModutimeApplication.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ModutimeApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ModutimeApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/advice/response/ExceptionResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.advice.response; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | public class ExceptionResponse { 12 | private String message; 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/query-parameters.snippet: -------------------------------------------------------------------------------- 1 | .요청파라미터 2 | [cols="3,2,3,2a"] 3 | |=== 4 | |파라미터|필수여부|설명|형식 5 | 6 | {{#parameters}} 7 | |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} 8 | |{{^optional}}필수{{/optional}} 9 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 10 | |{{#format}}{{format}}{{/format}}{{^format}}{{/format}} 11 | {{/parameters}} 12 | 13 | |=== 14 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/README.md: -------------------------------------------------------------------------------- 1 | Spring REST Docs 사용자정의 스니펫 적용시 주의사항 2 | ============================================== 3 | 4 | * Spring REST Docs 스니펫이 적용되지 않은 이유: 스니펫 폴더이름이 잘못 되었음 5 | * AS-IS: `src/test/resources/org.springframework.restdocs.templates.asciidoctor` 6 | * TO-BE: `src/test/resources/org/springframework/restdocs/templates/asciidoctor` 7 | * 인텔리제이에서는 동일하게 보이지만, 실제로 스니펫 폴더 경로는 달랐음 8 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/repository/TimeTableRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.repository; 2 | 3 | import com.dnd.modutime.core.timetable.domain.TimeTable; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface TimeTableRepository extends JpaRepository { 8 | Optional findByRoomUuid(String roomUuid); 9 | } 10 | -------------------------------------------------------------------------------- /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://storage.googleapis.com/coderabbit_public_assets/schema.v2.json 2 | 3 | language: "ko-KR" 4 | early_access: false 5 | reviews: 6 | profile: "chill" 7 | request_changes_workflow: false # 8 | high_level_summary: true 9 | poem: false 10 | review_status: true 11 | collapse_walkthrough: false 12 | auto_review: 13 | enabled: true 14 | drafts: false 15 | chat: 16 | auto_reply: true 17 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/repository/CandidateDateTimeRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.repository; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface CandidateDateTimeRepository extends JpaRepository { 7 | void deleteAllByAdjustmentResultId(Long adjustmentResultId); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/DateTimeInfoDto.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | public class DateTimeInfoDto { 11 | 12 | private final LocalDateTime dateTime; 13 | private final List participantNames; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/auth/application/response/LoginPageResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.auth.application.response; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | public class LoginPageResponse { 12 | 13 | private String roomTitle; 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet: -------------------------------------------------------------------------------- 1 | .요청파라미터 2 | [cols="3,2,3,2a"] 3 | |=== 4 | |파라미터|필수여부|설명|형식 5 | 6 | {{#parameters}} 7 | |{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}} 8 | |{{^optional}}필수{{/optional}}{{#optional}}선택{{/optional}} 9 | |{{#tableCellContent}}{{description}}{{/tableCellContent}} 10 | |{{#format}}{{format}}{{/format}}{{^format}}{{/format}} 11 | {{/parameters}} 12 | 13 | |=== 14 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/sorter/CandidateDateTimesSorter.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.sorter; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | 7 | public interface CandidateDateTimesSorter { 8 | void sort(List candidateDateTimes); 9 | 10 | Comparator getComparator(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/convertor/CandidateDateTimeConvertor.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.convertor; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.DateTimeInfoDto; 4 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 5 | import java.util.List; 6 | 7 | public interface CandidateDateTimeConvertor { 8 | 9 | List convert(List dateTimeInfosDto); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/repository/AdjustmentResultRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.repository; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.AdjustmentResult; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface AdjustmentResultRepository extends JpaRepository { 8 | Optional findByRoomUuid(String roomUuid); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/request/TimerRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application.request; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | public class TimerRequest { 12 | private int day; 13 | private int hour; 14 | private int minute; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/repository/TimeInfoParticipantNameRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import com.dnd.modutime.core.timetable.domain.TimeInfoParticipantName; 6 | 7 | public interface TimeInfoParticipantNameRepository extends JpaRepository { 8 | 9 | void deleteByTimeInfoIdAndName(Long timeInfoId, String name); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/config/HealthCheckController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.config; 2 | 3 | import org.springframework.http.ResponseEntity; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | @RestController 8 | public class HealthCheckController { 9 | 10 | @GetMapping("/aws") 11 | public ResponseEntity healthCheck() { 12 | return ResponseEntity.ok().build(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | closed 2 | 3 | 4 | ## 어떤 기능을 개발했나요? 5 | 6 | 7 | ## 어떻게 해결했나요? 8 | 9 | 10 | ## 어떤 부분에 집중하여 리뷰해야 할까요? 11 | 12 | 13 | ## (option) 이 부분은 주의해 주세요. 14 | 15 | 16 | ## (option) 참고자료 17 | 18 | 19 | ## (option) 결과 20 | 21 | 22 | 23 | --- 24 | 25 | ## RCA rule 26 | - r: 꼭 반영해 주세요. 적극적으로 고려해 주세요. 27 | - c: 웬만하면 반영해 주세요. 28 | - a: 반영해도 좋고 안 해도 좋습니다. 사소한 의견입니다. 29 | - 규칙 30 | - submit 할 코멘트들 중에서 1개라도 r이 포함되어 있다면 request change를 날린다. 31 | - r 이 하나도 없다면 approve를 한다. -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/request/EmailCreationRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application.request; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @AllArgsConstructor 11 | public class EmailCreationRequest { 12 | 13 | private String name; 14 | private String email; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/infrastructure/persistence/participant/ParticipantJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.infrastructure.persistence.participant; 2 | 3 | import com.dnd.modutime.core.participant.domain.Participant; 4 | import com.dnd.modutime.core.participant.domain.ParticipantRepository; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface ParticipantJpaRepository extends JpaRepository, 8 | ParticipantRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/config/TimeConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.config; 2 | 3 | import com.dnd.modutime.util.FakeTimeProvider; 4 | import com.dnd.modutime.util.TimeProvider; 5 | import org.springframework.boot.test.context.TestConfiguration; 6 | import org.springframework.context.annotation.Bean; 7 | 8 | @TestConfiguration 9 | public class TimeConfiguration { 10 | 11 | @Bean 12 | public TimeProvider timeProvider() { 13 | return new FakeTimeProvider(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | jpa: 3 | show-sql: true 4 | hibernate: 5 | ddl-auto: create 6 | properties: 7 | hibernate: 8 | format_sql: true 9 | generate-ddl: true 10 | datasource: 11 | url: 12 | username: sa 13 | h2: 14 | console: 15 | enabled: 16 | true 17 | logging: 18 | level: 19 | org: 20 | hibernate: 21 | type: 22 | descriptor: 23 | sql: 24 | BasicBinder: TRACE 25 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/util/FakeTimeProvider.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.util; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class FakeTimeProvider implements TimeProvider { 6 | 7 | private LocalDateTime dateTime = LocalDateTime.of(2023, 2, 9, 0, 0); 8 | 9 | @Override 10 | public LocalDateTime getCurrentLocalDateTime() { 11 | return dateTime; 12 | } 13 | 14 | public void setTime(LocalDateTime dateTime) { 15 | this.dateTime = dateTime; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/Pageable.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core; 2 | 3 | /** 4 | * 페이징 요청 파라미터 인터페이스 5 | */ 6 | public interface Pageable { 7 | /** 8 | * 페이지 번호 9 | * 10 | * @return 페이지번호 11 | */ 12 | int getPage(); 13 | 14 | /** 15 | * 페이징 혹은 오프렛에서 요청크기로 지정 16 | * 17 | * @return 페이지크기 18 | */ 19 | int getSize(); 20 | 21 | /** 22 | * 오프셋 시작위치 23 | * 24 | * @return 오프셋데이터 25 | */ 26 | long getOffset(); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /architecture-decision-records/readme.md: -------------------------------------------------------------------------------- 1 | ## 작성 방법 2 | 3 | ### 1. 터미널에서 ADR(Architecture Decision Record) 을 설치합니다. 4 | - `brew install adr-tools` 5 | - https://github.com/npryce/adr-tools/blob/master/INSTALL.md 6 | 7 | ### 2. adr new 명령어를 터미널에 입력합니다. 8 | - `adr new {파일명}` 9 | - ex) `adr new Implement as Unix shell scripts` 10 | - https://github.com/npryce/adr-tools?tab=readme-ov-file 11 | - 프로젝트 폴더 내부 어디에서든 명령어를 입력해도 괜찮습니다. 12 | 13 | ### 3. 생성된 파일을 열고 내용을 작성합니다. 14 | - 생성된 파일은 `architecture-decision-records` 폴더에 위치합니다. 15 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/infrastructure/persistence/participant/ParticipantJpaQueryRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.infrastructure.persistence.participant; 2 | 3 | import com.dnd.modutime.core.participant.domain.Participant; 4 | import com.dnd.modutime.core.participant.domain.ParticipantQueryRepository; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface ParticipantJpaQueryRepository extends JpaRepository, 8 | ParticipantQueryRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/config/persistence/AuditorAwareConfig.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.config.persistence; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.domain.AuditorAware; 6 | 7 | import java.util.Optional; 8 | 9 | @Configuration 10 | public class AuditorAwareConfig { 11 | 12 | @Bean 13 | public AuditorAware auditorProvider() { 14 | return () -> Optional.of("system"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/util/DateTimeToAvailableDateTimeConvertor.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.util; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | 8 | public interface DateTimeToAvailableDateTimeConvertor { 9 | 10 | List convert(TimeBlock timeBlock, 11 | List dateTimes); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/repository/AvailableDateTimeRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.repository; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import java.util.List; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface AvailableDateTimeRepository extends JpaRepository { 8 | 9 | void deleteAllByTimeBlockId(Long timeBlockId); 10 | 11 | List findByTimeBlockId(Long timeBlockId); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/util/RealTimeProvider.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.util; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.ZoneId; 5 | import java.time.ZonedDateTime; 6 | 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class RealTimeProvider implements TimeProvider { 11 | @Override 12 | public LocalDateTime getCurrentLocalDateTime() { 13 | ZoneId seoulZoneId = ZoneId.of("Asia/Seoul"); 14 | return ZonedDateTime.now(seoulZoneId).toLocalDateTime(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/response/RoomCreationResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor 12 | public class RoomCreationResponse { 13 | 14 | @JsonProperty(value = "roomUuid") 15 | private String uuid; 16 | } 17 | -------------------------------------------------------------------------------- /src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = 모두의 시간 API 2 | 모두의 시간팀, 3 | v0.0.1, 2025-07-18 4 | //문서 초기 설정 5 | :doctype: book 6 | :icons: font 7 | :source-highlighter: coderay 8 | :toc: left 9 | :toc-title: 목차 10 | :toclevels: 3 11 | :sectlinks: 12 | :sectnums: 13 | 14 | == 공통 15 | 16 | 모두의 시간(Modutime API) 입니다. 17 | 18 | [NOTE] 19 | ==== 20 | 쉽고 빠른 약속 정하기, 모두타임 21 | ==== 22 | 23 | .담당자 24 | |==== 25 | |담당 |이름 |이메일 |비고 26 | 27 | |개발 |김동호 |mailto:dongho1088@gmail.com[] | 28 | |==== 29 | 30 | // 코드 31 | include::timetable.adoc[] 32 | include::adjustment-result.adoc[] 33 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/controller/dto/AvailableTimeGroupRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.controller.dto; 2 | 3 | import com.dnd.modutime.core.timetable.domain.view.TimeTableSearchCondition; 4 | 5 | import java.util.List; 6 | 7 | public record AvailableTimeGroupRequest( 8 | List participantNames 9 | ) { 10 | public TimeTableSearchCondition toCondition(String roomUuid) { 11 | return TimeTableSearchCondition.of( 12 | roomUuid, 13 | this.participantNames 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "BUILD MYSQL_DB IMAGE" 4 | docker build -t modutime-local -f DockerFile-local . 5 | 6 | # 컨테이너 이름 설정 7 | CONTAINER_NAME="modutime-container" 8 | 9 | # 컨테이너가 존재하는지 확인 10 | if [ $(docker ps -a -q -f name=$CONTAINER_NAME) ]; then 11 | # 컨테이너가 존재하면 시작 12 | echo "Starting existing container $CONTAINER_NAME..." 13 | docker start $CONTAINER_NAME 14 | else 15 | # 컨테이너가 존재하지 않으면 새로 생성 및 실행 16 | echo "Creating and running a new container $CONTAINER_NAME..." 17 | docker run -d --name $CONTAINER_NAME -p 3306:3306 modutime-local 18 | fi 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /architecture-decision-records/templates/template.md: -------------------------------------------------------------------------------- 1 | # NUMBER. TITLE 2 | > Date: DATE 3 | 4 | --- 5 | 6 | ## Status 7 | > 상태: 제안됨(Proposed)|승인됨(Accepted)|반려됨(Rejected)|대체됨(Superseded)|사용중단됨(Deprecated) 8 | 9 | STATUS 10 | 11 | ## 상황(Context) 12 | > 이 결정이 필요하게 된 배경과 문제 상황을 설명합니다. 여기에는 결정을 내리게 된 기술적, 비즈니스적, 또는 규제적 조건들이 포함될 수 있습니다. 13 | 14 | ## 결정(Decision) 15 | > 선택한 솔루션에 대한 명확한 설명을 제공합니다. 여기에는 고려된 대안들과 그 대안들을 배제한 이유도 포함될 수 있습니다. 16 | 17 | ## 영향(Consequence) 18 | > 결정을 내린 후 무슨 일이 벌어졌는지를 설명합니다(프로젝트나 조직에 미치는 영향을 평가). 결정(Decision) 후 (좋은 혹은 나쁜)모든 결과를 기술합니다. 19 | 20 | ## 참조(Reference) 21 | > 기술결정과 관련된 참조정보를 기술합니다. 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/repository/TimeBlockRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.repository; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | public interface TimeBlockRepository extends JpaRepository { 9 | 10 | Optional findByRoomUuidAndParticipantName(String roomUuid, String participantName); 11 | 12 | List findByRoomUuid(String roomUuid); 13 | 14 | boolean existsByRoomUuid(String roomUuid); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/controller/dto/ParticipantsDeleteRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.controller.dto; 2 | 3 | import com.dnd.modutime.core.participant.application.command.ParticipantsDeleteCommand; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.util.List; 7 | 8 | public record ParticipantsDeleteRequest( 9 | @NotNull(message = "참여자 ids 는 필수값 입니다.") 10 | List participantIds 11 | ) { 12 | public ParticipantsDeleteCommand toCommand(String roomUuid) { 13 | return ParticipantsDeleteCommand.of(roomUuid, this.participantIds); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/adjustresult/domain/AdjustmentResultTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.domain; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import com.dnd.modutime.core.adjustresult.domain.AdjustmentResult; 7 | import java.util.List; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class AdjustmentResultTest { 11 | 12 | @Test 13 | void 생성시_확정상태_false_로_생성된다() { 14 | AdjustmentResult adjustmentResult = new AdjustmentResult(ROOM_UUID, List.of()); 15 | assertThat(adjustmentResult.isConfirmation()).isFalse(); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/domain/ParticipantQueryRepository.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface ParticipantQueryRepository { 7 | Optional findByRoomUuidAndName(String roomUuid, String name); 8 | 9 | boolean existsByRoomUuidAndName(String roomUuid, String name); 10 | 11 | List findByRoomUuid(String roomUuid); 12 | 13 | List findByRoomUuidAndNameIn(String roomUuid, List participantNames); 14 | 15 | List findByRoomUuidAndIdIn(String roomUuid, List participantIds); 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose-dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | db: 5 | container_name: db 6 | image: mysql 7 | restart: always 8 | environment: 9 | MYSQL_DATABASE: modutime 10 | MYSQL_ROOT_PASSWORD: 0000 11 | expose: 12 | - 3306 13 | ports: 14 | - "3306:3306" 15 | 16 | web: 17 | container_name: web 18 | build: 19 | context: . 20 | dockerfile: dockerfile.dev 21 | restart: always 22 | environment: 23 | SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/modutime 24 | SPRING_DATASOURCE_USERNAME: root 25 | SPRING_DATASOURCE_PASSWORD: 0000 26 | ports: 27 | - "8080:8080" 28 | depends_on: 29 | - db 30 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/documentation/DocumentUtils.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.documentation; 2 | 3 | import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; 4 | import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; 5 | 6 | import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; 7 | 8 | public class DocumentUtils { 9 | public static OperationRequestPreprocessor getDocumentRequest() { 10 | return preprocessRequest(prettyPrint()); 11 | } 12 | 13 | public static OperationResponsePreprocessor getDocumentResponse() { 14 | return preprocessResponse(prettyPrint()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/domain/view/TimeTableSearchCondition.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.domain.view; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.List; 7 | 8 | @NoArgsConstructor 9 | @Getter 10 | public class TimeTableSearchCondition { 11 | private String roomUuid; 12 | private List participantName; 13 | 14 | public static TimeTableSearchCondition of(String roomUuid, List participantName) { 15 | var condition = new TimeTableSearchCondition(); 16 | condition.roomUuid = roomUuid; 17 | condition.participantName = participantName; 18 | return condition; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/participant/domain/EmailTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 4 | 5 | import com.dnd.modutime.core.participant.domain.Email; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.ValueSource; 8 | 9 | class EmailTest { 10 | 11 | @ParameterizedTest 12 | @ValueSource(strings = {"", "aa", "asd.com", "asd@", "asd@@email.com"}) 13 | void 이메일형식이_맞지않으면_예외를_발생한다(String email) { 14 | assertThatThrownBy(() -> new Email(email)) 15 | .isInstanceOf(IllegalArgumentException.class); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/util/DateTimeToAvailableDateTimeConvertorFactory.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.util; 2 | 3 | import java.util.Map; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @RequiredArgsConstructor 9 | public class DateTimeToAvailableDateTimeConvertorFactory { 10 | 11 | private final Map convertors; 12 | 13 | public DateTimeToAvailableDateTimeConvertor getInstance(boolean hasTime) { 14 | if (hasTime) { 15 | return convertors.get("dateTimeConvertor"); 16 | } 17 | return convertors.get("dateConvertor"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/auth/application/request/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.auth.application.request; 2 | 3 | import com.dnd.modutime.core.participant.application.command.ParticipantCreateCommand; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor 12 | public class LoginRequest { 13 | 14 | private String name; 15 | private String password; 16 | 17 | public ParticipantCreateCommand toParticipantCreateCommand(String roomUuid) { 18 | return ParticipantCreateCommand.of(roomUuid, this.name, this.password); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/domain/TimeTableReplaceEvent.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.domain; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.command.AdjustmentResultReplaceCommand; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.util.List; 8 | 9 | @Getter 10 | @AllArgsConstructor 11 | public class TimeTableReplaceEvent { 12 | 13 | private final String roomUuid; 14 | private final List dateInfos; 15 | 16 | public AdjustmentResultReplaceCommand toAdjustmentResultReplaceCommand() { 17 | return AdjustmentResultReplaceCommand.of( 18 | this.roomUuid, 19 | this.dateInfos 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/response/TimeAndCountPerDate.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import java.time.LocalDate; 5 | import java.util.List; 6 | import lombok.AccessLevel; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Getter 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | @AllArgsConstructor 14 | public class TimeAndCountPerDate { 15 | 16 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 17 | private LocalDate availableDate; 18 | private List availableTimeInfos; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/response/EmailResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application.response; 2 | 3 | import com.dnd.modutime.core.participant.domain.Email; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Getter 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 12 | public class EmailResponse { 13 | 14 | private String email; 15 | 16 | public static EmailResponse from(Email email) { 17 | if (email == null) { 18 | return new EmailResponse(null); 19 | } 20 | return new EmailResponse(email.getValue()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/docs/asciidoc/adjustment-result.adoc: -------------------------------------------------------------------------------- 1 | [[Adjustment-Result]] 2 | == 우선순위 결과 3 | 4 | * 언제 만날지 우선순위를 계산한 결과입니다. 5 | 6 | === 우선순위 결과 조회 7 | 8 | * 우선순위 결과를 페이징으로 조회합니다. 9 | 10 | [discrete] 11 | ==== 요청 12 | 13 | include::{snippets}/get-api-v1-room-room-uuid-adjustment-result/path-parameters.adoc[] 14 | include::{snippets}/get-api-v1-room-room-uuid-adjustment-result/curl-request.adoc[] 15 | include::{snippets}/get-api-v1-room-room-uuid-adjustment-result/request-parameters.adoc[] 16 | include::{snippets}/get-api-v1-room-room-uuid-adjustment-result/http-request.adoc[] 17 | 18 | [discrete] 19 | ==== 응답 20 | 21 | include::{snippets}/get-api-v1-room-room-uuid-adjustment-result/http-response.adoc[] 22 | include::{snippets}/get-api-v1-room-room-uuid-adjustment-result/response-fields.adoc[] -------------------------------------------------------------------------------- /src/docs/asciidoc/timetable.adoc: -------------------------------------------------------------------------------- 1 | [[TimeTable]] 2 | == 시간 등록 현황 3 | 4 | * 방에 참여한 인원들이 등록한 시간이 합쳐져 있는 테이블입니다. 5 | 6 | === 참여 현황 조회 7 | 8 | * 방에 참여한 인원들에 대한 시간 등록 현황을 조회합니다. 9 | 10 | [discrete] 11 | ==== 요청 12 | 13 | include::{snippets}/get-api-room-room-uuid-available-time-overview/path-parameters.adoc[] 14 | include::{snippets}/get-api-room-room-uuid-available-time-overview/curl-request.adoc[] 15 | include::{snippets}/get-api-room-room-uuid-available-time-overview/request-parameters.adoc[] 16 | include::{snippets}/get-api-room-room-uuid-available-time-overview/http-request.adoc[] 17 | 18 | [discrete] 19 | ==== 응답 20 | 21 | include::{snippets}/get-api-room-room-uuid-available-time-overview/http-response.adoc[] 22 | include::{snippets}/get-api-room-room-uuid-available-time-overview/response-fields.adoc[] -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.http.HttpHeaders; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 7 | 8 | @Configuration 9 | public class WebConfig implements WebMvcConfigurer { 10 | 11 | public static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH"; 12 | 13 | @Override 14 | public void addCorsMappings(CorsRegistry registry) { 15 | registry.addMapping("/api/**") 16 | .allowedMethods(ALLOWED_METHOD_NAMES.split(",")) 17 | .exposedHeaders(HttpHeaders.LOCATION); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/util/DateTimeConstants.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.util; 2 | 3 | import java.time.ZoneId; 4 | import java.time.ZoneOffset; 5 | 6 | /** 7 | * 시간관련 상수 8 | */ 9 | public class DateTimeConstants { 10 | public static final String FORMAT_DEFAULT_DATE_TIME = "yyyy-MM-dd'T'HH:mm:ss"; 11 | public static final String FORMAT_DATE_TIME_WITHOUT_T = "yyyy-MM-dd HH:mm:ss"; 12 | public static final ZoneId DEFAULT_ZONE_ID = ZoneId.of("Asia/Seoul"); 13 | public static final ZoneOffset DEFAULT_ZONE_OFF = ZoneOffset.of("+09:00"); 14 | 15 | public static final String FORMAT_DATE = "yyyy-MM-dd"; 16 | public static final int FIRST_DAY_OF_MONTH = 1; 17 | 18 | public static final String FORMAT_TIME = "HH:mm:ss"; 19 | public static final String FORMAT_TIME_WITHOUT_S = "HH:mm"; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/TimeTableQueryService.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application; 2 | 3 | import com.dnd.modutime.core.timetable.domain.TimeTable; 4 | import com.dnd.modutime.core.timetable.repository.TimeTableRepository; 5 | import com.dnd.modutime.exception.NotFoundException; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class TimeTableQueryService { 12 | 13 | private final TimeTableRepository timeTableRepository; 14 | 15 | public TimeTable findByRoomUuid(String roomUuid) { 16 | return timeTableRepository.findByRoomUuid(roomUuid) 17 | .orElseThrow(() -> new NotFoundException("해당하는 TimeTable을 찾을 수 없습니다.")); 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/application/request/TimeReplaceRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.application.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | 12 | 13 | @Getter 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor 16 | public class TimeReplaceRequest { 17 | 18 | /** 19 | * Participant의 name 20 | */ 21 | private String name; 22 | private Boolean hasTime; 23 | 24 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") 25 | private List availableDateTimes; 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/acceptance/request/RoomRequestWithNoNull.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.acceptance.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import java.time.LocalDate; 5 | import java.util.List; 6 | 7 | public class RoomRequestWithNoNull { 8 | private String title; 9 | 10 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 11 | private List dates; 12 | 13 | public RoomRequestWithNoNull() { 14 | } 15 | 16 | public RoomRequestWithNoNull(String title, List dates) { 17 | this.title = title; 18 | this.dates = dates; 19 | } 20 | 21 | public String getTitle() { 22 | return title; 23 | } 24 | 25 | public List getDates() { 26 | return dates; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /architecture-decision-records/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | > Date: 2025-08-03 4 | --- 5 | 6 | ## 상태(Status) 7 | 8 | > 상태: 제안됨(Proposed)|승인됨(Accepted)|반려됨(Rejected)|대체됨(Superseded)|사용중단됨(Deprecated) 9 | 10 | Accepted 11 | 12 | ## 상황(Context) 13 | 14 | - 모두타임 프로젝트에서 ADR(Architectural decision)을 기록으로 남기길 바란다. 15 | 16 | ## 결정(Decision) 17 | 18 | - [Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions)이 기술한 ADR 정의를 이용한다. 19 | 20 | ## 영향(Consequence) 21 | 22 | - See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat 23 | Pryce's [adr-tools](https://github.com/npryce/adr-tools). 24 | - `adr-tools` 를 설치하고 `adr` 명령어를 이용해서 ADR 템플릿을 생성하고 작성한다. 25 | 26 | ## 참조(Reference) 27 | 28 | - https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions 29 | -------------------------------------------------------------------------------- /src/main/resources/application-db.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | config: 3 | activate: 4 | on-profile: local 5 | datasource: 6 | url: jdbc:h2:mem:modutime;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 7 | driver-class-name: org.h2.Driver 8 | username: sa 9 | password: 10 | jpa: 11 | show-sql: true 12 | hibernate: 13 | ddl-auto: update 14 | properties: 15 | hibernate: 16 | dialect: org.hibernate.dialect.H2Dialect 17 | format_sql: true 18 | generate-ddl: true 19 | 20 | --- 21 | spring: 22 | config: 23 | activate: 24 | on-profile: prod 25 | jpa: 26 | show-sql: false 27 | generate-ddl: true 28 | hibernate: 29 | ddl-auto: none 30 | datasource: 31 | url: ${DB_URL} 32 | username: ${DB_USERNAME} 33 | password: ${DB_PASSWORD} 34 | 35 | sql: 36 | init: 37 | mode: always 38 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/controller/dto/AdjustmentResultRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.controller.dto; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.CandidateDateTimeSortStandard; 4 | import com.dnd.modutime.core.adjustresult.application.condition.AdjustmentResultSearchCondition; 5 | import java.util.List; 6 | 7 | public record AdjustmentResultRequest( 8 | CandidateDateTimeSortStandard sorted, 9 | List participantNames 10 | ) { 11 | public AdjustmentResultSearchCondition toSearchCondition(final String roomUuid) { 12 | return AdjustmentResultSearchCondition.of( 13 | roomUuid, 14 | participantNames == null ? List.of() : participantNames, 15 | sorted == null ? CandidateDateTimeSortStandard.FAST : sorted 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/sorter/FastFirstSorter.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.sorter; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class FastFirstSorter implements CandidateDateTimesSorter{ 10 | 11 | @Override 12 | public void sort(List candidateDateTimes) { 13 | candidateDateTimes.sort(getComparator()); 14 | } 15 | 16 | @Override 17 | public Comparator getComparator() { 18 | return Comparator 19 | .comparing(CandidateDateTime::getParticipantSize, Comparator.reverseOrder()) 20 | .thenComparing(CandidateDateTime::getStartDateTime); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/room/domain/RoomDateTimeTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.domain; 2 | 3 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_09; 4 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_10; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import com.dnd.modutime.core.room.domain.RoomDate; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class RoomDateTimeTest { 11 | 12 | @Test 13 | void 같은_date라면_true를_반환한다() { 14 | RoomDate roomDate = new RoomDate(_2023_02_10); 15 | assertThat(roomDate.isSameDate(new RoomDate(_2023_02_10))).isTrue(); 16 | } 17 | 18 | @Test 19 | void 다른_date라면_false를_반환한다() { 20 | RoomDate roomDate = new RoomDate(_2023_02_10); 21 | assertThat(roomDate.isSameDate(new RoomDate(_2023_02_09))).isFalse(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | services: 3 | 4 | web: 5 | container_name: web 6 | image: ssssujini99/modutime-web 7 | expose: 8 | - "8080" 9 | ports: 10 | - "8080:8080" 11 | environment: 12 | DB_URL: ${DB_URL} 13 | DB_USERNAME: ${DB_USERNAME} 14 | DB_PASSWORD: ${DB_PASSWORD} 15 | mem_limit: 512m 16 | mem_reservation: 256m 17 | restart: unless-stopped 18 | logging: 19 | driver: "json-file" 20 | options: 21 | max-size: "10m" 22 | max-file: "3" 23 | 24 | nginx: 25 | container_name: nginx 26 | image: ssssujini99/modutime-nginx 27 | ports: 28 | - "80:80" 29 | depends_on: 30 | - web 31 | mem_limit: 64m 32 | restart: unless-stopped 33 | logging: 34 | driver: "json-file" 35 | options: 36 | max-size: "5m" 37 | max-file: "2" 38 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/util/DateConvertor.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.util; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import org.springframework.stereotype.Component; 9 | 10 | // TODO: test 11 | @Component 12 | public class DateConvertor implements DateTimeToAvailableDateTimeConvertor { 13 | 14 | @Override 15 | public List convert(TimeBlock timeBlock, 16 | List dateTimes) { 17 | return dateTimes.stream() 18 | .map(it -> new AvailableDateTime(timeBlock, it.toLocalDate(), null)) 19 | .collect(Collectors.toList()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/command/AdjustmentResultReplaceCommand.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application.command; 2 | 3 | import com.dnd.modutime.core.timetable.domain.DateInfo; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | public class AdjustmentResultReplaceCommand { 13 | private String roomUuid; 14 | private List dateInfos; 15 | 16 | public static AdjustmentResultReplaceCommand of( 17 | String roomUuid, 18 | List dateInfos 19 | ) { 20 | var command = new AdjustmentResultReplaceCommand(); 21 | command.roomUuid = roomUuid; 22 | command.dateInfos = dateInfos; 23 | return command; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/domain/TimeBlockReplaceEvent.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import com.dnd.modutime.core.timetable.application.command.TimeTableUpdateCommand; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | 7 | import java.util.List; 8 | 9 | @Getter 10 | @AllArgsConstructor 11 | public class TimeBlockReplaceEvent { 12 | 13 | private String roomUuid; 14 | private List oldAvailableDateTimes; 15 | private List newAvailableDateTimes; 16 | private String participantName; 17 | 18 | public TimeTableUpdateCommand toTimeTableUpdateCommand() { 19 | return TimeTableUpdateCommand.of( 20 | this.roomUuid, 21 | this.oldAvailableDateTimes, 22 | this.newAvailableDateTimes, 23 | this.participantName 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/sorter/LongFirstSorter.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.sorter; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | import org.springframework.stereotype.Component; 7 | 8 | // TODO: test 9 | @Component 10 | public class LongFirstSorter implements CandidateDateTimesSorter{ 11 | 12 | @Override 13 | public void sort(List candidateDateTimes) { 14 | candidateDateTimes.sort(getComparator()); 15 | } 16 | 17 | @Override 18 | public Comparator getComparator() { 19 | return Comparator 20 | .comparing(CandidateDateTime::getParticipantSize, Comparator.reverseOrder()) 21 | .thenComparing(CandidateDateTime::calculateTerm) 22 | .thenComparing(CandidateDateTime::getStartDateTime); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/util/Timer.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.util; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class Timer { 6 | 7 | private static final int MIN = 0; 8 | 9 | public static LocalDateTime calculateDeadLine(int day, 10 | int hour, 11 | int minute, 12 | TimeProvider timeProvider) { 13 | validateNegative(day, hour, minute); 14 | LocalDateTime now = timeProvider.getCurrentLocalDateTime(); 15 | return now.plusMinutes(minute) 16 | .plusHours(hour) 17 | .plusDays(day); 18 | } 19 | 20 | private static void validateNegative(int day, int hour, int minute) { 21 | if (day < MIN || hour < MIN || minute < MIN) { 22 | throw new IllegalArgumentException("시간값에는 음수가 들어올 수 없습니다."); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/TimeTableFacade.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application; 2 | 3 | import com.dnd.modutime.core.timetable.domain.view.TimeTableOverview; 4 | import com.dnd.modutime.core.timetable.domain.view.TimeTableSearchCondition; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | @RequiredArgsConstructor 10 | public class TimeTableFacade { 11 | 12 | private final TimeTableQueryService timeTableQueryService; 13 | 14 | /** 15 | * roomUuid와 participantName을 기반으로 TimeTableOverview를 조회합니다. 16 | * 17 | * @param condition 18 | * @return 19 | */ 20 | public TimeTableOverview getOverview(TimeTableSearchCondition condition) { 21 | var timeTable = this.timeTableQueryService.findByRoomUuid(condition.getRoomUuid()); 22 | 23 | return TimeTableOverview.from(timeTable, condition.getParticipantName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /install-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Installing docker engine if not exists 4 | if ! type docker > /dev/null 5 | then 6 | echo "docker does not exist" 7 | echo "Start installing docker" 8 | sudo apt-get update 9 | sudo apt install -y apt-transport-https ca-certificates curl software-properties-common 10 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 11 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable" 12 | sudo apt update 13 | apt-cache policy docker-ce 14 | sudo apt install -y docker-ce 15 | fi 16 | 17 | # Installing docker-compose if not exists 18 | if ! type docker-compose > /dev/null 19 | then 20 | echo "docker-compose does not exist" 21 | echo "Start installing docker-compose" 22 | sudo curl -L "https://github.com/docker/compose/releases/download/1.27.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 23 | sudo chmod +x /usr/local/bin/docker-compose 24 | fi 25 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/CandidateDateTimeSortStandard.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application; 2 | 3 | import java.util.Arrays; 4 | import lombok.AllArgsConstructor; 5 | 6 | @AllArgsConstructor 7 | public enum CandidateDateTimeSortStandard { 8 | FAST("fast"), 9 | LONG("long") 10 | ; 11 | 12 | private final String value; 13 | 14 | public static CandidateDateTimeSortStandard getByValue(String value) { 15 | return Arrays.stream(CandidateDateTimeSortStandard.values()) 16 | .filter(candidateDateTimeSortStandard -> candidateDateTimeSortStandard.value.equals(value)) 17 | .findFirst() 18 | .orElseThrow(() -> new IllegalArgumentException("해당하는 value의 정렬 기준이 없습니다.")); 19 | } 20 | 21 | public boolean isFast() { 22 | return this.value.equals(FAST.value); 23 | } 24 | 25 | public boolean isLong() { 26 | return this.value.equals(LONG.value); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/sorter/CandidateDateTimesSorterFactory.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.sorter; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.CandidateDateTimeSortStandard; 4 | import java.util.Map; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | 8 | // TODO: test 9 | @Component 10 | @RequiredArgsConstructor 11 | public class CandidateDateTimesSorterFactory { 12 | 13 | private final Map sorters; 14 | 15 | public CandidateDateTimesSorter getInstance(CandidateDateTimeSortStandard candidateDateTimeSortStandard) { 16 | if (candidateDateTimeSortStandard.isFast()) { 17 | return sorters.get("fastFirstSorter"); 18 | } 19 | if (candidateDateTimeSortStandard.isLong()) { 20 | return sorters.get("longFirstSorter"); 21 | } 22 | throw new IllegalArgumentException("해당하는 정렬기가 없습니다."); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/domain/Email.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | import java.util.regex.Pattern; 4 | import javax.persistence.Column; 5 | import javax.persistence.Embeddable; 6 | import lombok.AccessLevel; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Embeddable 10 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 11 | public class Email { 12 | 13 | private static final Pattern EMAIL_PATTERN = Pattern.compile("^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$"); 14 | 15 | @Column(name = "email") 16 | private String value; 17 | 18 | public Email(String value) { 19 | validateRightEmailPattern(value); 20 | this.value = value; 21 | } 22 | 23 | private void validateRightEmailPattern(String email) { 24 | if (!EMAIL_PATTERN.matcher(email).find()) { 25 | throw new IllegalArgumentException("email 형식에 맞지 않습니다."); 26 | } 27 | } 28 | 29 | public String getValue() { 30 | return value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/command/ParticipantCreateCommand.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application.command; 2 | 3 | import com.dnd.modutime.core.participant.domain.Participant; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Getter 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | public class ParticipantCreateCommand { 11 | private String roomUuid; 12 | private String name; 13 | private String password; 14 | 15 | public static ParticipantCreateCommand of(String roomUuid, String name, String password) { 16 | var command = new ParticipantCreateCommand(); 17 | command.roomUuid = roomUuid; 18 | command.name = name; 19 | command.password = password; 20 | return command; 21 | } 22 | 23 | public Participant execute() { 24 | return new Participant( 25 | this.roomUuid, 26 | this.name, 27 | this.password); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/condition/AdjustmentResultSearchCondition.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application.condition; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.CandidateDateTimeSortStandard; 4 | import java.util.List; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @Getter 11 | public class AdjustmentResultSearchCondition { 12 | private String roomUuid; 13 | private List participantNames; 14 | private CandidateDateTimeSortStandard sortedStandard; 15 | 16 | public static AdjustmentResultSearchCondition of(String roomUuid, List participantNames, CandidateDateTimeSortStandard sortedStandard) { 17 | var condition = new AdjustmentResultSearchCondition(); 18 | condition.roomUuid = roomUuid; 19 | condition.participantNames = participantNames; 20 | condition.sortedStandard = sortedStandard; 21 | return condition; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/executor/AdjustmentResultResponseGenerator.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.executor; 2 | 3 | import com.dnd.modutime.core.Page; 4 | import com.dnd.modutime.core.Pageable; 5 | import com.dnd.modutime.core.adjustresult.application.CandidateDateTimeSortStandard; 6 | import com.dnd.modutime.core.adjustresult.application.condition.AdjustmentResultSearchCondition; 7 | import com.dnd.modutime.core.adjustresult.application.response.AdjustmentResultResponse; 8 | import com.dnd.modutime.core.adjustresult.application.response.CandidateDateTimeResponseV1; 9 | import java.util.List; 10 | 11 | public interface AdjustmentResultResponseGenerator { 12 | 13 | AdjustmentResultResponse generate(String roomUuid, 14 | CandidateDateTimeSortStandard candidateDateTimeSortStandard, 15 | List names); 16 | 17 | Page v1generate(final AdjustmentResultSearchCondition condition, 18 | final Pageable pageable); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/command/ParticipantsDeleteCommand.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application.command; 2 | 3 | import com.dnd.modutime.core.participant.domain.Participant; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | public class ParticipantsDeleteCommand { 13 | 14 | private String roomUuid; 15 | private List participantIds; 16 | 17 | private List participants; 18 | 19 | public static ParticipantsDeleteCommand of(String roomUuid, List participantIds) { 20 | var command = new ParticipantsDeleteCommand(); 21 | command.roomUuid = roomUuid; 22 | command.participantIds = participantIds; 23 | return command; 24 | } 25 | 26 | public void assign(List participants) { 27 | this.participants = participants; 28 | } 29 | 30 | public List execute() { 31 | return this.participants; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/AdjustmentResultEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application; 2 | 3 | import com.dnd.modutime.core.timetable.domain.TimeTableReplaceEvent; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.transaction.annotation.Propagation; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import org.springframework.transaction.event.TransactionalEventListener; 8 | 9 | @Component 10 | public class AdjustmentResultEventHandler { 11 | 12 | private final AdjustmentResultReplaceService adjustmentResultReplaceService; 13 | 14 | public AdjustmentResultEventHandler(AdjustmentResultReplaceService adjustmentResultReplaceService) { 15 | this.adjustmentResultReplaceService = adjustmentResultReplaceService; 16 | } 17 | 18 | @Transactional(propagation = Propagation.REQUIRES_NEW) 19 | @TransactionalEventListener 20 | public void handle(TimeTableReplaceEvent event) { 21 | var command = event.toAdjustmentResultReplaceCommand(); 22 | adjustmentResultReplaceService.replace(command); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timeblock/util/DateTimeToAvailableDateTimeConvertorFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.util; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | 9 | @SpringBootTest 10 | public class DateTimeToAvailableDateTimeConvertorFactoryTest { 11 | 12 | @Autowired 13 | private DateTimeToAvailableDateTimeConvertorFactory dateTimeToAvailableDateTimeConvertorFactory; 14 | 15 | @Test 16 | void hasTime이_true이면_dateTimeConvertor를_반환한다() { 17 | DateTimeToAvailableDateTimeConvertor instance = dateTimeToAvailableDateTimeConvertorFactory.getInstance(true); 18 | assertThat(instance).isInstanceOf(DateTimeConvertor.class); 19 | } 20 | 21 | @Test 22 | void hasTime이_false이면_dateConvertor를_반환한다() { 23 | DateTimeToAvailableDateTimeConvertor instance = dateTimeToAvailableDateTimeConvertorFactory.getInstance(false); 24 | assertThat(instance).isInstanceOf(DateConvertor.class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/response/AdjustmentResultResponseV1.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application.response; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import com.dnd.modutime.core.participant.domain.Participants; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor 16 | public class AdjustmentResultResponseV1 { 17 | 18 | @JsonProperty(value = "candidateTimes") 19 | private List candidateDateTimeResponse; 20 | 21 | public static AdjustmentResultResponseV1 of(List candidateDateTimes, Participants participants) { 22 | return new AdjustmentResultResponseV1(candidateDateTimes.stream() 23 | .map(it -> CandidateDateTimeResponseV1.of(it, participants)) 24 | .collect(Collectors.toList())); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/request/RoomRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import java.time.LocalDate; 6 | import java.time.LocalTime; 7 | import java.util.List; 8 | import lombok.AccessLevel; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Getter 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | @AllArgsConstructor 16 | public class RoomRequest { 17 | 18 | private String title; 19 | private Integer headCount; 20 | 21 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 22 | private List dates; 23 | 24 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 25 | private LocalTime startTime; 26 | 27 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 28 | private LocalTime endTime; 29 | 30 | @JsonProperty(value = "timer") 31 | private TimerRequest timerRequest; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/controller/TimeTableController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.controller; 2 | 3 | 4 | import com.dnd.modutime.core.timetable.application.TimeTableService; 5 | import com.dnd.modutime.core.timetable.application.response.TimeTableResponse; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | @RestController 13 | @RequiredArgsConstructor 14 | public class TimeTableController { 15 | 16 | private final TimeTableService timeTableService; 17 | 18 | /** 19 | * @deprecated `/api/room/{roomUuid}/available-time/overview` 로 대체 20 | */ 21 | @Deprecated 22 | @GetMapping("/api/room/{roomUuid}/available-time/group") 23 | public ResponseEntity getTimeTable(@PathVariable String roomUuid) { 24 | TimeTableResponse timeTableResponse = timeTableService.getTimeTable(roomUuid); 25 | return ResponseEntity.ok(timeTableResponse); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /architecture-decision-records/0002-api-docs-rest-docs.md: -------------------------------------------------------------------------------- 1 | # 2. api docs rest docs 2 | 3 | > Date: 2025-08-03 4 | 5 | --- 6 | 7 | ## Status 8 | 9 | > 상태: 제안됨(Proposed)|승인됨(Accepted)|반려됨(Rejected)|대체됨(Superseded)|사용중단됨(Deprecated) 10 | 11 | Accepted 12 | 13 | ## 상황(Context) 14 | 15 | > 이 결정이 필요하게 된 배경과 문제 상황을 설명합니다. 여기에는 결정을 내리게 된 기술적, 비즈니스적, 또는 규제적 조건들이 포함될 수 있습니다. 16 | 17 | - 모두타임 프로젝트에서 API 문서화를 위해 RestDocs를 사용하기로 결정했습니다. 18 | - 백엔드개발과 프론트개발이 비동기적으로 수행되며 API를 먼저 개발하여 반영하는 개발방식을 사용 중입니다. 19 | 20 | ## 결정(Decision) 21 | 22 | > 선택한 솔루션에 대한 명확한 설명을 제공합니다. 여기에는 고려된 대안들과 그 대안들을 배제한 이유도 포함될 수 있습니다. 23 | 24 | - 모두타임 프로젝트는 프로젝트 초기부터 TDD 를 적용하여 테스트 코드가 촘촘합니다. 25 | - E2E API 테스트도 항상 작성하는 프로젝트입니다. 26 | - API 테스트가 필수인 프로젝트 이므로 Spring RestDocs를 사용하여 API 문서를 작성합니다. 27 | 28 | ## 영향(Consequence) 29 | 30 | > 결정을 내린 후 무슨 일이 벌어졌는지를 설명합니다(프로젝트나 조직에 미치는 영향을 평가). 결정(Decision) 후 (좋은 혹은 나쁜)모든 결과를 기술합니다. 31 | 32 | - 앞으로 API 문서화는 RestDocs를 통해 자동으로 생성됩니다. 33 | - 컨트롤러 테스트 코드를 필수로 작성해야 합니다. 34 | 35 | ## 참조(Reference) 36 | 37 | > 기술결정과 관련된 참조정보를 기술합니다. 38 | 39 | - [Spring RestDocs](https://spring.io/projects/spring-restdocs) 40 | - https://helloworld.kurly.com/blog/spring-rest-docs-guide/ 41 | - https://www.openapis.org/ 42 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/response/CandidateDateTimeResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import java.time.LocalDate; 5 | import java.time.LocalTime; 6 | import java.util.List; 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Getter 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @AllArgsConstructor 15 | public class CandidateDateTimeResponse { 16 | 17 | private Long id; 18 | 19 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 20 | private LocalDate date; 21 | private String dayOfWeek; 22 | 23 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 24 | private LocalTime startTime; 25 | 26 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 27 | private LocalTime endTime; 28 | private List availableParticipantNames; 29 | private List unavailableParticipantNames; 30 | private Boolean isConfirmed; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/command/TimeTableUpdateCommand.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application.command; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | public class TimeTableUpdateCommand { 13 | private String roomUuid; 14 | private List oldAvailableDateTimes; 15 | private List newAvailableDateTimes; 16 | private String participantName; 17 | 18 | public static TimeTableUpdateCommand of( 19 | String roomUuid, 20 | List oldAvailableDateTimes, 21 | List newAvailableDateTimes, 22 | String participantName) { 23 | var command = new TimeTableUpdateCommand(); 24 | command.roomUuid = roomUuid; 25 | command.oldAvailableDateTimes = oldAvailableDateTimes; 26 | command.newAvailableDateTimes = newAvailableDateTimes; 27 | command.participantName = participantName; 28 | return command; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/application/TimeBlockEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.application; 2 | 3 | import com.dnd.modutime.core.participant.domain.ParticipantRemovedEvent; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.transaction.annotation.Propagation; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import org.springframework.transaction.event.TransactionalEventListener; 8 | 9 | @Component 10 | public class TimeBlockEventHandler { 11 | 12 | private final TimeBlockService timeBlockService; 13 | 14 | public TimeBlockEventHandler(TimeBlockService timeBlockService) { 15 | this.timeBlockService = timeBlockService; 16 | } 17 | 18 | @Transactional(propagation = Propagation.REQUIRES_NEW) 19 | @TransactionalEventListener 20 | public void handle(ParticipantCreationEvent event) { 21 | timeBlockService.create(event.getRoomUuid(), event.getName()); 22 | } 23 | 24 | @Transactional(propagation = Propagation.REQUIRES_NEW) 25 | @TransactionalEventListener 26 | public void handle(ParticipantRemovedEvent event) { 27 | timeBlockService.remove(event.getRoomUuid(), event.getName()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/util/CandidateDateTimeConvertorFactory.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.util; 2 | 3 | import com.dnd.modutime.core.adjustresult.util.convertor.CandidateDateTimeConvertor; 4 | import com.dnd.modutime.core.room.domain.Room; 5 | import com.dnd.modutime.core.room.repository.RoomRepository; 6 | import com.dnd.modutime.exception.NotFoundException; 7 | import java.util.Map; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | @RequiredArgsConstructor 13 | public class CandidateDateTimeConvertorFactory { 14 | 15 | private final RoomRepository roomRepository; 16 | private final Map convertors; 17 | 18 | public CandidateDateTimeConvertor getInstance(String roomUuid) { 19 | Room room = getByUuid(roomUuid); 20 | if (room.hasStartAndEndTime()) { 21 | return convertors.get("dateTimeRoomConvertor"); 22 | } 23 | return convertors.get("dateRoomConvertor"); 24 | } 25 | 26 | private Room getByUuid(String roomUuid) { 27 | return roomRepository.findByUuid(roomUuid) 28 | .orElseThrow(() -> new NotFoundException("해당하는 방이 없습니다.")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/controller/TimeTableQueryController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.controller; 2 | 3 | import com.dnd.modutime.core.timetable.application.TimeTableFacade; 4 | import com.dnd.modutime.core.timetable.controller.dto.AvailableTimeGroupRequest; 5 | import com.dnd.modutime.core.timetable.domain.view.TimeTableOverview; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import javax.validation.Valid; 13 | 14 | @RestController 15 | @RequiredArgsConstructor 16 | public class TimeTableQueryController { 17 | 18 | private final TimeTableFacade facade; 19 | 20 | @GetMapping("/api/room/{roomUuid}/available-time/overview") 21 | public ResponseEntity v2getOverview(@PathVariable String roomUuid, 22 | @Valid AvailableTimeGroupRequest request) { 23 | var timeTableResponse = facade.getOverview(request.toCondition(roomUuid)); 24 | return ResponseEntity.ok(timeTableResponse); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/Page.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core; 2 | 3 | import java.util.List; 4 | 5 | public interface Page { 6 | 7 | /** 8 | * 이전 페이지 존재여부 반환 9 | * 10 | * @return 이전 페이지 존재여부 11 | */ 12 | boolean hasPrevious(); 13 | 14 | /** 15 | * 다음 페이지 존재여부 반환 16 | * 17 | * @return 다음페이지 존재여부 18 | */ 19 | boolean hasNext(); 20 | 21 | /** 22 | * 페이징 검색조건 포함여부 반환 23 | * 24 | * @return 페이징 검색조건 포함여부 25 | */ 26 | boolean hasContent(); 27 | 28 | /** 29 | * 첫번째 페이지 여부 반환 30 | * 31 | * @return 첫번째 페이지 여부 32 | */ 33 | boolean isFirst(); 34 | 35 | /** 36 | * 마지막 페이지 여부 반환 37 | * 38 | * @return 마지막 페이지 여부 39 | */ 40 | boolean isLast(); 41 | 42 | /** 43 | * 현재 페이지에 포함된 검색결과 반환 44 | * 45 | * @return 현재 페이지에 포함된 검색결과 46 | */ 47 | List getContent(); 48 | 49 | /** 50 | * 페이징 조건 반환 51 | * 52 | * @return 페이징 조건 53 | */ 54 | Pageable getPageRequest(); 55 | 56 | /** 57 | * 해당검색 페이징 조건 전체 건수 반환 58 | * 59 | * @return 해당검색 페이징 조건 전체 건수 60 | */ 61 | long getTotal(); 62 | 63 | /** 64 | * 전체 페이지 수 반환 65 | * 66 | * @return 전체 페이지 수 반환 67 | */ 68 | int getTotalPages(); 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/ParticipantCommandHandler.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application; 2 | 3 | import com.dnd.modutime.core.participant.application.command.ParticipantCreateCommand; 4 | import com.dnd.modutime.core.participant.application.command.ParticipantsDeleteCommand; 5 | import com.dnd.modutime.core.participant.domain.Participant; 6 | import com.dnd.modutime.core.participant.domain.ParticipantRepository; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | @Service 11 | public class ParticipantCommandHandler { 12 | 13 | private final ParticipantRepository participantRepository; 14 | 15 | public ParticipantCommandHandler(ParticipantRepository participantRepository) { 16 | this.participantRepository = participantRepository; 17 | } 18 | 19 | public void handle(ParticipantsDeleteCommand command) { 20 | var participants = command.execute(); 21 | for (Participant participant : participants) { 22 | participantRepository.delete(participant); 23 | } 24 | } 25 | 26 | @Transactional 27 | public Participant handle(ParticipantCreateCommand command) { 28 | return participantRepository.save(command.execute()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/controller/ParticipantCommandController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.controller; 2 | 3 | import com.dnd.modutime.core.participant.application.ParticipantFacade; 4 | import com.dnd.modutime.core.participant.controller.dto.ParticipantsDeleteRequest; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.DeleteMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import javax.validation.Valid; 12 | 13 | @RestController 14 | public class ParticipantCommandController { 15 | 16 | private final ParticipantFacade participantFacade; 17 | 18 | public ParticipantCommandController(ParticipantFacade participantFacade) { 19 | this.participantFacade = participantFacade; 20 | } 21 | 22 | @DeleteMapping("/api/room/{roomUuid}") 23 | public ResponseEntity deleteParticipants(@PathVariable String roomUuid, 24 | @RequestBody @Valid ParticipantsDeleteRequest request) { 25 | participantFacade.delete(request.toCommand(roomUuid)); 26 | return ResponseEntity.ok().build(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/entity/Auditable.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.entity; 2 | 3 | import org.springframework.data.annotation.CreatedBy; 4 | import org.springframework.data.annotation.CreatedDate; 5 | import org.springframework.data.annotation.LastModifiedBy; 6 | import org.springframework.data.annotation.LastModifiedDate; 7 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 8 | 9 | import javax.persistence.Column; 10 | import javax.persistence.EntityListeners; 11 | import javax.persistence.MappedSuperclass; 12 | import java.time.LocalDateTime; 13 | 14 | @MappedSuperclass 15 | @EntityListeners(AuditingEntityListener.class) 16 | public interface Auditable { 17 | @CreatedBy 18 | @Column(name = "created_by", columnDefinition = "VARCHAR(50) COMMENT '생성자'") 19 | void setCreatedBy(String createdBy); 20 | 21 | @CreatedDate 22 | @Column(name = "created_at", columnDefinition = "DATETIME(6) COMMENT '생성일시'") 23 | void setCreatedAt(LocalDateTime createdAt); 24 | 25 | @LastModifiedBy 26 | @Column(name = "modified_by", columnDefinition = "VARCHAR(50) COMMENT '수정자'") 27 | void setModifiedBy(String modifiedBy); 28 | 29 | @LastModifiedDate 30 | @Column(name = "modified_at", columnDefinition = "DATETIME(6) COMMENT '수정일시'") 31 | void setModifiedAt(LocalDateTime modifiedAt); 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timeblock/domain/TimeBlockTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 6 | 7 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class TimeBlockTest { 11 | 12 | @Test 13 | void TimeBlock_생성시_roomUuid가_null이면_예외가_발생한다() { 14 | assertThatThrownBy(() -> getTimeBlock(ROOM_UUID, null)) 15 | .isInstanceOf(IllegalArgumentException.class); 16 | } 17 | 18 | @Test 19 | void TimeBlock_생성시_participantName이_null이면_예외가_발생한다() { 20 | assertThatThrownBy(() -> getTimeBlock(null, "참여자1")) 21 | .isInstanceOf(IllegalArgumentException.class); 22 | } 23 | 24 | @Test 25 | void TimeBlock_생성시_가능한시간들이_빈리스트로_초기화된다() { 26 | TimeBlock timeBlock = getTimeBlock(ROOM_UUID, "참여자1"); 27 | assertThat(timeBlock.getAvailableDateTimes()).isEmpty(); 28 | } 29 | 30 | private TimeBlock getTimeBlock(String roomUuid, 31 | String participantName) { 32 | return new TimeBlock(roomUuid, participantName); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/ci-pull-request.yml: -------------------------------------------------------------------------------- 1 | # Pull Request 이벤트 발생시 통합테스트 수행 2 | name: "CI: Pull Request" 3 | 4 | on: 5 | pull_request: 6 | types: [ opened, synchronize, reopened ] 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout source 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 # Disabling shallow clone is recommended for improving relevancy of reporting 16 | 17 | - name: Setup Java 18 | uses: actions/setup-java@v3 #@see https://github.com/actions/setup-java 19 | with: 20 | java-version: '17' 21 | distribution: 'corretto' 22 | 23 | - name: Setup local-cache 24 | uses: maxnowack/local-cache@v1 25 | with: 26 | path: | 27 | ~/.gradle/caches 28 | ~/.gradle/wrapper 29 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 30 | 31 | - name: Build with gradle 32 | run: ./gradlew clean test --info --build-cache 33 | 34 | #https://github.com/actions/upload-artifact 35 | - name: "Upload failure test report" 36 | uses: actions/upload-artifact@v4 37 | if: failure() 38 | with: 39 | name: test-report 40 | path: '**/build/reports/tests/test' 41 | retention-days: 1 42 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/TimeTableEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.TimeBlockRemovedEvent; 4 | import com.dnd.modutime.core.timeblock.domain.TimeBlockReplaceEvent; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.transaction.annotation.Propagation; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import org.springframework.transaction.event.TransactionalEventListener; 9 | 10 | @Component 11 | public class TimeTableEventHandler { 12 | 13 | private final TimeTableService timeTableService; 14 | 15 | public TimeTableEventHandler(TimeTableService timeTableService) { 16 | this.timeTableService = timeTableService; 17 | } 18 | 19 | @Transactional(propagation = Propagation.REQUIRES_NEW) 20 | @TransactionalEventListener 21 | public void handle(TimeBlockRemovedEvent event) { 22 | var command = event.toTimeTableUpdateCommand(); 23 | this.timeTableService.update(command); 24 | } 25 | 26 | @Transactional(propagation = Propagation.REQUIRES_NEW) 27 | @TransactionalEventListener 28 | public void handle(TimeBlockReplaceEvent event) { 29 | var command = event.toTimeTableUpdateCommand(); 30 | this.timeTableService.update(command); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/response/RoomInfoResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import java.time.LocalDate; 6 | import java.time.LocalDateTime; 7 | import java.time.LocalTime; 8 | import java.util.List; 9 | import lombok.AccessLevel; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | 14 | @Getter 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor 17 | public class RoomInfoResponse { 18 | 19 | private String title; 20 | 21 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") 22 | private LocalDateTime deadLine; 23 | 24 | private Integer headCount; 25 | 26 | @JsonProperty(value = "participants") 27 | private List participantNames; 28 | 29 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 30 | private List dates; 31 | 32 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 33 | private LocalTime startTime; 34 | 35 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 36 | private LocalTime endTime; 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/response/AvailableTimeInfo.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import java.time.LocalTime; 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | @AllArgsConstructor 13 | public class AvailableTimeInfo { 14 | 15 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 16 | private LocalTime time; 17 | private int count; 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) { 22 | return true; 23 | } 24 | if (o == null || getClass() != o.getClass()) { 25 | return false; 26 | } 27 | 28 | final AvailableTimeInfo that = (AvailableTimeInfo) o; 29 | 30 | if (getCount() != that.getCount()) { 31 | return false; 32 | } 33 | return getTime() != null ? getTime().equals(that.getTime()) : that.getTime() == null; 34 | } 35 | 36 | @Override 37 | public int hashCode() { 38 | int result = getTime() != null ? getTime().hashCode() : 0; 39 | result = 31 * result + getCount(); 40 | return result; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/response/V2RoomInfoResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import java.time.LocalDate; 10 | import java.time.LocalDateTime; 11 | import java.time.LocalTime; 12 | import java.util.List; 13 | 14 | @Getter 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor 17 | public class V2RoomInfoResponse { 18 | 19 | private String title; 20 | 21 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") 22 | private LocalDateTime deadLine; 23 | 24 | private Integer headCount; 25 | 26 | private List participants; 27 | 28 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 29 | private List dates; 30 | 31 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 32 | private LocalTime startTime; 33 | 34 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 35 | private LocalTime endTime; 36 | 37 | public record Participant( 38 | Long id, 39 | String name 40 | ) { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/convertor/DateRoomConvertor.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.convertor; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.DateTimeInfoDto; 4 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 5 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTimeParticipantName; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class DateRoomConvertor implements CandidateDateTimeConvertor { 13 | 14 | @Override 15 | public List convert(List dateTimeInfosDto) { 16 | List candidateDateTimes = new ArrayList<>(); 17 | for (DateTimeInfoDto dateTimeInfoDto : dateTimeInfosDto) { 18 | List participantNames = dateTimeInfoDto.getParticipantNames(); 19 | candidateDateTimes.add(new CandidateDateTime( 20 | null, 21 | dateTimeInfoDto.getDateTime(), 22 | dateTimeInfoDto.getDateTime(), 23 | null, 24 | participantNames.stream() 25 | .map(CandidateDateTimeParticipantName::new) 26 | .collect(Collectors.toList()))); 27 | } 28 | return candidateDateTimes; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/util/executor/AdjustmentResultExecutorFactory.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.executor; 2 | 3 | import com.dnd.modutime.core.participant.application.ParticipantQueryService; 4 | import com.dnd.modutime.core.participant.domain.Participants; 5 | import java.util.Objects; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | @Component 13 | @RequiredArgsConstructor 14 | public class AdjustmentResultExecutorFactory { 15 | 16 | private final Map executors; 17 | private final ParticipantQueryService participantQueryService; 18 | 19 | public AdjustmentResultResponseGenerator getInstance(String roomUuid, List names) { 20 | var participants = new Participants(participantQueryService.getByRoomUuid(roomUuid)); 21 | if (!participants.containsAll(names)) { 22 | throw new IllegalArgumentException("방에 존재하지 않는 이름이 있습니다."); 23 | } 24 | if (Objects.isNull(names) || names.isEmpty()) { 25 | return executors.get("adjustmentResponseGenerator"); 26 | } 27 | if (participants.isSameAllNames(names)) { 28 | return executors.get("adjustmentResponseGenerator"); 29 | } 30 | return executors.get("timeTableResponseGenerator"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/infrastructure/PageRequest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.infrastructure; 2 | 3 | import com.dnd.modutime.core.Pageable; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | 7 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 8 | public class PageRequest implements Pageable { 9 | public static final Integer DEFAULT_PAGE = 0; 10 | public static final Integer DEFAULT_SIZE = 20; 11 | 12 | private Integer page; 13 | private Integer size; 14 | 15 | public PageRequest(Integer page, Integer size) { 16 | this.page = page == null ? DEFAULT_PAGE : page; 17 | this.size = size == null ? DEFAULT_SIZE : size; 18 | } 19 | 20 | /** 21 | * 페이지 요청 반환 22 | * 23 | * @param page 페이지번호 24 | * @param size 페이지 크기 25 | * @return 페이지 요청 개체 26 | */ 27 | public static PageRequest of(int page, int size) { 28 | if (page < 0) { 29 | page = 0; 30 | } 31 | 32 | if (size < 1) { 33 | size = 20; 34 | } else if (size > 1000) { 35 | size = 1000; 36 | } 37 | 38 | return new PageRequest(page, size); 39 | } 40 | 41 | public int getPage() { 42 | return page > 0 ? page - 1 : 0; 43 | } 44 | 45 | public int getSize() { 46 | return size; 47 | } 48 | 49 | public long getOffset() { 50 | return (long) getPage() * getSize(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/auth/controller/AuthController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.auth.controller; 2 | 3 | import com.dnd.modutime.core.auth.application.request.LoginRequest; 4 | import com.dnd.modutime.core.auth.application.response.LoginPageResponse; 5 | import com.dnd.modutime.core.participant.application.ParticipantFacade; 6 | import com.dnd.modutime.core.room.application.RoomService; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | @RestController 11 | @RequestMapping("/api/room/{roomUuid}/login") 12 | public class AuthController { 13 | 14 | private final ParticipantFacade participantFacade; 15 | private final RoomService roomService; 16 | 17 | public AuthController(ParticipantFacade participantFacade, RoomService roomService) { 18 | this.participantFacade = participantFacade; 19 | this.roomService = roomService; 20 | } 21 | 22 | @PostMapping 23 | public ResponseEntity login(@PathVariable String roomUuid, 24 | @RequestBody LoginRequest loginRequest) { 25 | participantFacade.login(loginRequest.toParticipantCreateCommand(roomUuid)); 26 | return ResponseEntity.ok().build(); 27 | } 28 | 29 | @GetMapping 30 | public ResponseEntity loginPage(@PathVariable String roomUuid) { 31 | String roomName = roomService.getTitleByUuid(roomUuid); 32 | return ResponseEntity.ok(new LoginPageResponse(roomName)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/domain/TimeBlockRemovedEvent.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import com.dnd.modutime.core.timetable.application.command.TimeTableUpdateCommand; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 12 | public class TimeBlockRemovedEvent { 13 | private String roomUuid; 14 | private List oldAvailableDateTimes; 15 | private List newAvailableDateTimes; 16 | private String participantName; 17 | 18 | public static TimeBlockRemovedEvent of( 19 | String roomUuid, 20 | List oldAvailableDateTimes, 21 | List newAvailableDateTimes, 22 | String participantName) { 23 | var event = new TimeBlockRemovedEvent(); 24 | event.roomUuid = roomUuid; 25 | event.oldAvailableDateTimes = oldAvailableDateTimes; 26 | event.newAvailableDateTimes = newAvailableDateTimes; 27 | event.participantName = participantName; 28 | return event; 29 | } 30 | 31 | public TimeTableUpdateCommand toTimeTableUpdateCommand() { 32 | return TimeTableUpdateCommand.of( 33 | this.roomUuid, 34 | this.oldAvailableDateTimes, 35 | this.newAvailableDateTimes, 36 | this.participantName 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timeblock/domain/AvailableDateTimeTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static com.dnd.modutime.fixture.TimeFixture._12_00; 5 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_10; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 8 | 9 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 10 | import com.dnd.modutime.core.timeblock.domain.AvailableTime; 11 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 12 | import java.util.List; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class AvailableDateTimeTest { 16 | 17 | @Test 18 | void AvailableDateTime생성시_date가_null이면_예외가_발생한다() { 19 | assertThatThrownBy(() -> new AvailableDateTime(null, null, List.of(new AvailableTime(_12_00)))) 20 | .isInstanceOf(IllegalArgumentException.class); 21 | } 22 | 23 | @Test 24 | void times가_null이면_꺼낼때_null을_반환한다() { 25 | AvailableDateTime availableDateTime = new AvailableDateTime(null, _2023_02_10, null); 26 | assertThat(availableDateTime.getTimesOrNull()).isNull(); 27 | } 28 | 29 | @Test 30 | void time이_null_이면_가지고있지_않다고_판단한다() { 31 | AvailableDateTime availableDateTime = new AvailableDateTime(new TimeBlock(ROOM_UUID, "참여자1"), _2023_02_10, null); 32 | assertThat(availableDateTime.hasTime()).isFalse(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/fixture/TimeTableFixture.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.fixture; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_10; 5 | 6 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 7 | import com.dnd.modutime.core.timeblock.domain.AvailableTime; 8 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 9 | import com.dnd.modutime.core.timetable.domain.DateInfo; 10 | import com.dnd.modutime.core.timetable.domain.TimeInfo; 11 | import com.dnd.modutime.core.timetable.domain.TimeTable; 12 | import java.time.LocalDate; 13 | import java.time.LocalTime; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | public class TimeTableFixture { 18 | 19 | public static DateInfo getDateInfo(List timeInfos) { 20 | return getDateInfo(_2023_02_10, timeInfos); 21 | } 22 | 23 | public static DateInfo getDateInfo(LocalDate date, List timeInfos) { 24 | return new DateInfo(getTimeTable(), date, timeInfos); 25 | } 26 | 27 | public static TimeTable getTimeTable() { 28 | return new TimeTable(ROOM_UUID); 29 | } 30 | 31 | public static TimeInfo getTimeInfo(LocalTime time) { 32 | return new TimeInfo(time, new ArrayList<>()); 33 | } 34 | 35 | public static AvailableDateTime getAvailableDateTime(String participantName, LocalDate date, List availableTimes) { 36 | return new AvailableDateTime(new TimeBlock(ROOM_UUID, participantName), date, availableTimes); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/response/TimeTableResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application.response; 2 | 3 | import com.dnd.modutime.core.timetable.domain.DateInfo; 4 | import com.dnd.modutime.core.timetable.domain.TimeTable; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | import lombok.AccessLevel; 10 | import lombok.AllArgsConstructor; 11 | import lombok.Getter; 12 | import lombok.NoArgsConstructor; 13 | 14 | @Getter 15 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 16 | @AllArgsConstructor 17 | public class TimeTableResponse { 18 | 19 | @JsonProperty(value = "availableDateTimes") 20 | private List timeAndCountPerDates; 21 | 22 | public static TimeTableResponse from(TimeTable timeTable) { 23 | List timeAndCountPerDates = new ArrayList<>(); 24 | List dateInfos = timeTable.getDateInfos(); 25 | for (DateInfo dateInfo : dateInfos) { 26 | List availableTimeInfos = dateInfo.getTimeInfos().stream() 27 | .map(timeInfo -> new AvailableTimeInfo(timeInfo.getTime(), timeInfo.getParticipantsSize())) 28 | .collect(Collectors.toList()); 29 | TimeAndCountPerDate timeAndCountPerDate = new TimeAndCountPerDate(dateInfo.getDate(), availableTimeInfos); 30 | timeAndCountPerDates.add(timeAndCountPerDate); 31 | } 32 | return new TimeTableResponse(timeAndCountPerDates); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/advice/GlobalControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.advice; 2 | 3 | import com.dnd.modutime.advice.response.ExceptionResponse; 4 | import com.dnd.modutime.exception.InvalidPasswordException; 5 | import com.dnd.modutime.exception.NotFoundException; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.RestControllerAdvice; 10 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 11 | 12 | @RestControllerAdvice 13 | public class GlobalControllerAdvice extends ResponseEntityExceptionHandler { 14 | 15 | @ExceptionHandler(InvalidPasswordException.class) 16 | public ResponseEntity handleUnAuthorizedException(InvalidPasswordException exception) { 17 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED) 18 | .body(new ExceptionResponse(exception.getMessage())); 19 | } 20 | 21 | @ExceptionHandler(NotFoundException.class) 22 | public ResponseEntity handleNotFoundException(NotFoundException exception) { 23 | return ResponseEntity.status(HttpStatus.NOT_FOUND) 24 | .body(new ExceptionResponse(exception.getMessage())); 25 | } 26 | 27 | @ExceptionHandler(IllegalArgumentException.class) 28 | public ResponseEntity handleIllegalArgumentException(IllegalArgumentException exception) { 29 | return ResponseEntity.status(HttpStatus.BAD_REQUEST) 30 | .body(new ExceptionResponse(exception.getMessage())); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/annotation/ApiDocsTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.annotation; 2 | 3 | import org.junit.jupiter.api.Tag; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.junit.jupiter.MockitoExtension; 6 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 8 | import org.springframework.core.annotation.AliasFor; 9 | import org.springframework.restdocs.RestDocumentationExtension; 10 | 11 | import java.lang.annotation.*; 12 | 13 | /** 14 | * Spring REST Docs 생성 테스트 15 | * 16 | * @see AutoConfigureMockMvc 17 | * @see AutoConfigureRestDocs 18 | */ 19 | @Target(ElementType.TYPE) 20 | @Retention(RetentionPolicy.RUNTIME) 21 | @Documented 22 | @Tag("apiDocs") 23 | @ExtendWith({MockitoExtension.class, RestDocumentationExtension.class}) 24 | @AutoConfigureMockMvc 25 | @AutoConfigureRestDocs 26 | public @interface ApiDocsTest { 27 | /** 28 | * Specifies the controllers to test. This is an alias of {@link #controllers()} which 29 | * can be used for brevity if no other attributes are defined. See 30 | * {@link #controllers()} for details. 31 | * 32 | * @return the controllers to test 33 | * @see #controllers() 34 | */ 35 | @AliasFor("controllers") 36 | Class[] value() default {}; 37 | 38 | /** 39 | * Specifies the controllers to test. May be left blank if all {@code @Controller} 40 | * beans should be added to the application context. 41 | * 42 | * @return the controllers to test 43 | * @see #value() 44 | */ 45 | @AliasFor("value") 46 | Class[] controllers() default {}; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/ParticipantFacade.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application; 2 | 3 | import com.dnd.modutime.core.participant.application.command.ParticipantCreateCommand; 4 | import com.dnd.modutime.core.participant.application.command.ParticipantsDeleteCommand; 5 | import com.dnd.modutime.exception.InvalidPasswordException; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | @Service 10 | public class ParticipantFacade { 11 | 12 | private final ParticipantQueryService queryService; 13 | private final ParticipantCommandHandler commandHandler; 14 | 15 | public ParticipantFacade(ParticipantQueryService queryService, 16 | ParticipantCommandHandler commandHandler) { 17 | this.queryService = queryService; 18 | this.commandHandler = commandHandler; 19 | } 20 | 21 | public void login(ParticipantCreateCommand command) { 22 | if (!queryService.existsBy(command.getRoomUuid(), command.getName())) { 23 | commandHandler.handle(command); 24 | return; 25 | } 26 | var participant = queryService.findByRoomUuidAndName(command.getRoomUuid(), command.getName()); 27 | if (!participant.matchPassword(command.getPassword())) { 28 | throw new InvalidPasswordException(); 29 | } 30 | } 31 | 32 | @Transactional 33 | public void delete(ParticipantsDeleteCommand command) { 34 | var participants = queryService.getByRoomUuidAndIds(command.getRoomUuid(), command.getParticipantIds()); 35 | command.assign(participants); 36 | commandHandler.handle(command); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/domain/CandidateDateTimeParticipantName.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.domain; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.EntityListeners; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import lombok.AccessLevel; 9 | import lombok.NoArgsConstructor; 10 | import com.dnd.modutime.core.entity.Auditable; 11 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 12 | import java.time.LocalDateTime; 13 | 14 | @Entity 15 | @EntityListeners(AuditingEntityListener.class) 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | public class CandidateDateTimeParticipantName implements Auditable { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Long id; 22 | private String name; 23 | 24 | private String createdBy; 25 | private LocalDateTime createdAt; 26 | private String modifiedBy; 27 | private LocalDateTime modifiedAt; 28 | 29 | public CandidateDateTimeParticipantName(String name) { 30 | this.name = name; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | @Override 38 | public void setCreatedBy(String createdBy) { 39 | this.createdBy = createdBy; 40 | } 41 | 42 | @Override 43 | public void setCreatedAt(LocalDateTime createdAt) { 44 | this.createdAt = createdAt; 45 | } 46 | 47 | @Override 48 | public void setModifiedBy(String modifiedBy) { 49 | this.modifiedBy = modifiedBy; 50 | } 51 | 52 | @Override 53 | public void setModifiedAt(LocalDateTime modifiedAt) { 54 | this.modifiedAt = modifiedAt; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/util/DateTimeConvertor.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.util; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import com.dnd.modutime.core.timeblock.domain.AvailableTime; 5 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 6 | import java.time.LocalDate; 7 | import java.time.LocalDateTime; 8 | import java.time.LocalTime; 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | import org.springframework.stereotype.Component; 14 | 15 | // TODO: test 16 | @Component 17 | public class DateTimeConvertor implements DateTimeToAvailableDateTimeConvertor { 18 | 19 | @Override 20 | public List convert(TimeBlock timeBlock, 21 | List dateTimes) { 22 | List availableDateTimes = new ArrayList<>(); 23 | HashMap> dates = new HashMap<>(); 24 | dateTimes.forEach(dateTime -> addTime(dates, dateTime.toLocalDate(), dateTime.toLocalTime())); 25 | for (LocalDate localDate : dates.keySet()) { 26 | availableDateTimes.add(new AvailableDateTime(timeBlock, localDate, dates.get(localDate).stream() 27 | .map(AvailableTime::new) 28 | .collect(Collectors.toList()))); 29 | } 30 | return availableDateTimes; 31 | } 32 | 33 | private void addTime(HashMap> dates, LocalDate date, LocalTime time) { 34 | if (!dates.containsKey(date)) { 35 | dates.put(date, new ArrayList<>(List.of(time))); 36 | return; 37 | } 38 | dates.get(date).add(time); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/util/TimerTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.util; 2 | 3 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_10_00_00; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | 7 | import java.time.LocalDateTime; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class TimerTest { 11 | 12 | @Test 13 | void 현재시간에_day_hour_minute_을_더한값을_반환한다() { 14 | FakeTimeProvider timeProvider = new FakeTimeProvider(); 15 | timeProvider.setTime(_2023_02_10_00_00); 16 | LocalDateTime deadLine = Timer.calculateDeadLine(2, 10, 30, timeProvider); 17 | assertThat(deadLine).isEqualTo(LocalDateTime.of(2023, 2, 12, 10, 30)); 18 | } 19 | 20 | @Test 21 | void day_값은_음수가_들어오면_예외를_반환한다() { 22 | FakeTimeProvider timeProvider = new FakeTimeProvider(); 23 | timeProvider.setTime(_2023_02_10_00_00); 24 | assertThatThrownBy(() -> Timer.calculateDeadLine(-2, 10, 30, timeProvider)) 25 | .isInstanceOf(IllegalArgumentException.class); 26 | } 27 | 28 | @Test 29 | void hour_값은_음수가_들어오면_예외를_반환한다() { 30 | FakeTimeProvider timeProvider = new FakeTimeProvider(); 31 | timeProvider.setTime(_2023_02_10_00_00); 32 | assertThatThrownBy(() -> Timer.calculateDeadLine(2, -10, 30, timeProvider)) 33 | .isInstanceOf(IllegalArgumentException.class); 34 | } 35 | 36 | @Test 37 | void minute_값은_음수가_들어오면_예외를_반환한다() { 38 | FakeTimeProvider timeProvider = new FakeTimeProvider(); 39 | timeProvider.setTime(_2023_02_10_00_00); 40 | assertThatThrownBy(() -> Timer.calculateDeadLine(2, 10, -30, timeProvider)) 41 | .isInstanceOf(IllegalArgumentException.class); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/controller/TimeBlockController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.controller; 2 | 3 | 4 | import com.dnd.modutime.core.timeblock.application.TimeBlockService; 5 | import com.dnd.modutime.core.timeblock.application.request.TimeReplaceRequest; 6 | import com.dnd.modutime.core.timeblock.application.response.TimeBlockResponse; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.PutMapping; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | @RestController 18 | @RequestMapping("/api/room/{roomUuid}/available-time") 19 | @RequiredArgsConstructor 20 | public class TimeBlockController { 21 | 22 | private final TimeBlockService timeBlockService; 23 | 24 | @PutMapping 25 | public ResponseEntity replace(@PathVariable String roomUuid, 26 | @RequestBody TimeReplaceRequest timeReplaceRequest) { 27 | timeBlockService.replace(roomUuid, timeReplaceRequest); 28 | return ResponseEntity.ok().build(); 29 | } 30 | 31 | @GetMapping 32 | public ResponseEntity getTimeBlock(@PathVariable String roomUuid, 33 | @RequestParam String name) { 34 | TimeBlockResponse timeBlockResponse = timeBlockService.getTimeBlock(roomUuid, name); 35 | return ResponseEntity.ok(timeBlockResponse); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.entity; 2 | 3 | import lombok.Getter; 4 | import org.springframework.data.annotation.CreatedBy; 5 | import org.springframework.data.annotation.CreatedDate; 6 | import org.springframework.data.annotation.LastModifiedBy; 7 | import org.springframework.data.annotation.LastModifiedDate; 8 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 9 | 10 | import javax.persistence.Column; 11 | import javax.persistence.EntityListeners; 12 | import javax.persistence.MappedSuperclass; 13 | import java.time.LocalDateTime; 14 | 15 | @Getter 16 | @MappedSuperclass 17 | @EntityListeners(AuditingEntityListener.class) 18 | public abstract class BaseEntity { 19 | @CreatedBy 20 | @Column(name = "created_by", columnDefinition = "VARCHAR(50) COMMENT '생성자'") 21 | private String createdBy; 22 | 23 | @CreatedDate 24 | @Column(name = "created_at", columnDefinition = "DATETIME(6) COMMENT '생성일시'") 25 | private LocalDateTime createdAt; 26 | 27 | @LastModifiedBy 28 | @Column(name = "modified_by", columnDefinition = "VARCHAR(50) COMMENT '수정자'") 29 | private String modifiedBy; 30 | 31 | @LastModifiedDate 32 | @Column(name = "modified_at", columnDefinition = "DATETIME(6) COMMENT '수정일시'") 33 | private LocalDateTime modifiedAt; 34 | 35 | public void setCreatedBy(final String createdBy) { 36 | this.createdBy = createdBy; 37 | } 38 | 39 | public void setCreatedAt(final LocalDateTime creationDate) { 40 | this.createdAt = creationDate; 41 | } 42 | 43 | public void setModifiedBy(final String modifiedBy) { 44 | this.modifiedBy = modifiedBy; 45 | } 46 | 47 | public void setModifiedAt(final LocalDateTime modifiedAt) { 48 | this.modifiedAt = modifiedAt; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/domain/AvailableTime.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import java.time.LocalTime; 4 | import javax.persistence.Column; 5 | import javax.persistence.Entity; 6 | import javax.persistence.EntityListeners; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import lombok.AccessLevel; 11 | import lombok.NoArgsConstructor; 12 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 13 | 14 | import com.dnd.modutime.core.entity.Auditable; 15 | import java.time.LocalDateTime; 16 | 17 | @Entity 18 | @EntityListeners(AuditingEntityListener.class) 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | public class AvailableTime implements Auditable { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | @Column 27 | private LocalTime time; 28 | 29 | private String createdBy; 30 | private LocalDateTime createdAt; 31 | private String modifiedBy; 32 | private LocalDateTime modifiedAt; 33 | 34 | public AvailableTime(LocalTime time) { 35 | this.time = time; 36 | } 37 | 38 | public LocalTime getTime() { 39 | return time; 40 | } 41 | 42 | @Override 43 | public void setCreatedBy(String createdBy) { 44 | this.createdBy = createdBy; 45 | } 46 | 47 | @Override 48 | public void setCreatedAt(LocalDateTime createdAt) { 49 | this.createdAt = createdAt; 50 | } 51 | 52 | @Override 53 | public void setModifiedBy(String modifiedBy) { 54 | this.modifiedBy = modifiedBy; 55 | } 56 | 57 | @Override 58 | public void setModifiedAt(LocalDateTime modifiedAt) { 59 | this.modifiedAt = modifiedAt; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timeblock/repository/AvailableDateTimeRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.repository; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static com.dnd.modutime.fixture.TimeFixture._12_00; 5 | import static com.dnd.modutime.fixture.TimeFixture._13_00; 6 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_10; 7 | 8 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 9 | import com.dnd.modutime.core.timeblock.domain.AvailableTime; 10 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 11 | import com.dnd.modutime.core.timeblock.repository.AvailableDateTimeRepository; 12 | import com.dnd.modutime.core.timeblock.repository.TimeBlockRepository; 13 | import java.util.List; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 17 | 18 | @DataJpaTest 19 | class AvailableDateTimeRepositoryTest { 20 | 21 | @Autowired 22 | private AvailableDateTimeRepository availableDateTimeRepository; 23 | 24 | @Autowired 25 | private TimeBlockRepository timeBlockRepository; 26 | 27 | @Test 28 | void AvailableDateTime을_조회하면_times도_같이_조회해온다() { 29 | TimeBlock savedTimeBlock = timeBlockRepository.save(new TimeBlock(ROOM_UUID, "참여자1")); 30 | AvailableDateTime availableDateTime = new AvailableDateTime(savedTimeBlock, _2023_02_10, 31 | List.of( 32 | new AvailableTime(_12_00), 33 | new AvailableTime(_13_00)) 34 | ); 35 | availableDateTimeRepository.save(availableDateTime); 36 | 37 | final List byTimeBlockId = availableDateTimeRepository.findByTimeBlockId(savedTimeBlock.getId()); 38 | int a = 0; 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/fixture/RoomRequestFixture.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.fixture; 2 | 3 | import static com.dnd.modutime.fixture.TimeFixture.*; 4 | 5 | import java.time.LocalDate; 6 | import java.util.List; 7 | 8 | import com.dnd.modutime.core.room.application.request.RoomRequest; 9 | import com.dnd.modutime.core.room.application.request.TimerRequest; 10 | 11 | public class RoomRequestFixture { 12 | 13 | public static String ROOM_UUID = "7c64aa0e-6e8f-4f61-b8ee-d5a86493d3a9"; 14 | 15 | public static RoomRequest getRoomRequestNoTime() { 16 | return getRoomRequestNoTime(List.of(_2023_02_10)); 17 | } 18 | 19 | public static RoomRequest getRoomRequestNoTime(List dates) { 20 | TimerRequest timerRequest = new TimerRequest(2, 1, 30); 21 | return new RoomRequest("이멤버리멤버", 22 | 10, dates, null, null, timerRequest); 23 | } 24 | 25 | public static RoomRequest getRoomRequest() { 26 | TimerRequest timerRequest = new TimerRequest(2, 1, 30); 27 | return getRoomRequest(timerRequest); 28 | } 29 | 30 | public static RoomRequest getRoomRequest(List dates) { 31 | TimerRequest timerRequest = new TimerRequest(2, 1, 30); 32 | return getRoomRequest(dates, timerRequest); 33 | } 34 | 35 | public static RoomRequest getRoomRequest(TimerRequest timerRequest) { 36 | return getRoomRequest(List.of(_2023_02_10), timerRequest); 37 | } 38 | 39 | public static RoomRequest getRoomRequest(List dates, TimerRequest timerRequest) { 40 | return new RoomRequest("이멤버리멤버", 41 | 10, dates, _11_00, _14_00, timerRequest); 42 | } 43 | 44 | public static RoomRequest getRoomRequestWithStartTimeIsAfterEndTime() { 45 | return new RoomRequest("이멤버리멤버", 10, List.of(_2023_02_10), 46 | _01_00, _00_00, new TimerRequest(2, 1, 30)); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/domain/RoomDate.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.domain; 2 | 3 | import com.dnd.modutime.core.entity.Auditable; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.EntityListeners; 10 | import javax.persistence.GeneratedValue; 11 | import javax.persistence.Id; 12 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 13 | import java.time.LocalDate; 14 | import java.time.LocalDateTime; 15 | 16 | import static javax.persistence.GenerationType.IDENTITY; 17 | 18 | @Entity 19 | @EntityListeners(AuditingEntityListener.class) 20 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 21 | public class RoomDate implements Auditable { 22 | 23 | @Id 24 | @GeneratedValue(strategy = IDENTITY) 25 | private Long id; 26 | 27 | @Column(nullable = false) 28 | private LocalDate date; 29 | 30 | private String createdBy; 31 | private LocalDateTime createdAt; 32 | private String modifiedBy; 33 | private LocalDateTime modifiedAt; 34 | 35 | public RoomDate(LocalDate date) { 36 | this.date = date; 37 | } 38 | 39 | public boolean isSameDate(RoomDate roomDate) { 40 | return this.date.equals(roomDate.getDate()); 41 | } 42 | 43 | public LocalDate getDate() { 44 | return date; 45 | } 46 | 47 | @Override 48 | public void setCreatedBy(String createdBy) { 49 | this.createdBy = createdBy; 50 | } 51 | 52 | @Override 53 | public void setCreatedAt(LocalDateTime createdAt) { 54 | this.createdAt = createdAt; 55 | } 56 | 57 | @Override 58 | public void setModifiedBy(String modifiedBy) { 59 | this.modifiedBy = modifiedBy; 60 | } 61 | 62 | @Override 63 | public void setModifiedAt(LocalDateTime modifiedAt) { 64 | this.modifiedAt = modifiedAt; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/application/ParticipantQueryService.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application; 2 | 3 | import com.dnd.modutime.core.participant.domain.Participant; 4 | import com.dnd.modutime.core.participant.domain.ParticipantQueryRepository; 5 | import com.dnd.modutime.exception.NotFoundException; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | @Transactional(readOnly = true) 13 | @Service 14 | public class ParticipantQueryService { 15 | 16 | private final ParticipantQueryRepository queryRepository; 17 | 18 | public ParticipantQueryService(ParticipantQueryRepository queryRepository) { 19 | this.queryRepository = queryRepository; 20 | } 21 | 22 | public List getByRoomUuidAndName(String roomUuid, List participantNames) { 23 | return queryRepository.findByRoomUuidAndNameIn(roomUuid, participantNames); 24 | } 25 | 26 | public List getByRoomUuidAndIds(String roomUuid, List participantIds) { 27 | return queryRepository.findByRoomUuidAndIdIn(roomUuid, participantIds); 28 | } 29 | 30 | public Participant findByRoomUuidAndName(String roomUuid, String name) { 31 | return queryRepository.findByRoomUuidAndName(roomUuid, name) 32 | .orElseThrow(() -> new NotFoundException("해당하는 참여자를 찾을 수 없습니다.")); 33 | } 34 | 35 | public boolean existsBy(String roomUuid, String name) { 36 | return queryRepository.existsByRoomUuidAndName(roomUuid, name); 37 | } 38 | 39 | public List getByRoomUuid(String roomUuid) { 40 | return queryRepository.findByRoomUuid(roomUuid); 41 | } 42 | 43 | public Optional getByRoomUuidAndName(String roomUuid, String name) { 44 | return queryRepository.findByRoomUuidAndName(roomUuid, name); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 모두타임 (Modutime) 2 | 3 | 모두타임은 여러 사람들의 일정을 조율하여 최적의 미팅 시간을 찾아주는 서비스입니다. 4 | 5 | ## 프로젝트 정보 6 | 7 | - **프로젝트명**: 모두타임 (Modutime) 8 | - **접속주소**: https://modutime.site 9 | - **기술 스택**: 10 | - Java 17 11 | - Spring Boot 2.7.8 12 | - Spring Data JPA 13 | - MySQL 14 | - Spring RestDocs 15 | - Docker 16 | 17 | ## 개발자 정보 18 | 19 | | 이름 | 역할 | 소속 | 이메일 | 20 | |-----|--------|----|----------------------| 21 | | 김동호 | 백엔드 개발 | 컬리 | dongho1088@gmail.com | 22 | 23 | ## 프로젝트 실행 방법 24 | 25 | ### 백엔드 실행 방법 (Mac용) 26 | 27 | > `./run.sh` 실행 28 | 29 | * permission denied가 뜬다면, 아래 두 명령어를 실행해주세요. 30 | * `chmod +x ./run.sh` 31 | * `chmod +x ./install-docker.sh` 32 | * 내부적으로 도커를 설치하고 컨테이너를 실행시킵니다. 33 | 이미 도커가 설치되어있더라도 건너뜁니다. 34 | 35 | ## 코드 작성 규칙 36 | 37 | - **TDD 적용**: 프로젝트 초기부터 TDD를 적용하여 테스트 코드를 촘촘하게 작성합니다. 38 | - **API 문서 테스트 필수**: Spring RestDocs를 사용하여 API 문서를 자동으로 생성합니다. 39 | - **컨트롤러 테스트 코드 필수**: 모든 컨트롤러에 대한 테스트 코드를 작성해야 합니다. 40 | - **한국어 주석**: 로직이 복잡한 메서드의 경우 한글 주석(javadoc)을 작성합니다. 41 | 42 | ## 시스템 아키텍처 43 | 44 | ``` 45 | +----------------+ +----------------+ +----------------+ 46 | | | | | | | 47 | | 클라이언트 +----->+ Nginx +----->+ Spring Boot | 48 | | | | | | Application | 49 | +----------------+ +----------------+ +-------+--------+ 50 | | 51 | v 52 | +----------------+ 53 | | | 54 | | MySQL | 55 | | | 56 | +----------------+ 57 | ``` 58 | 59 | ## 주요 기능 60 | 61 | - 방 생성 및 관리 62 | - 참가자 로그인 및 인증 63 | - 일정 조율 및 최적 시간 계산 64 | - 조율 결과 제공 -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | ${LOG_PATH}/app.log 19 | 20 | ${LOG_PATH}/app.%d{yyyy-MM-dd}.%i.log.gz 21 | ${MAX_FILE_SIZE} 22 | ${MAX_HISTORY} 23 | ${TOTAL_SIZE_CAP} 24 | 25 | 26 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/participant/domain/Participants.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | import java.util.HashSet; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | // TODO: test 8 | public class Participants { 9 | 10 | private final List participants; 11 | 12 | public Participants(List participants) { 13 | validateParticipantsNull(participants); 14 | this.participants = participants; 15 | } 16 | 17 | private void validateParticipantsNull(List participants) { 18 | if (participants == null) { 19 | throw new IllegalArgumentException("참여자에 null일 수 없습니다."); 20 | } 21 | } 22 | 23 | public boolean isSameAllNames(List names) { 24 | validateNamesNull(names); 25 | List participantNames = getParticipantNames(); 26 | return new HashSet<>(names).containsAll(participantNames); 27 | } 28 | 29 | public boolean containsAll(List names) { 30 | if (names == null || names.isEmpty()) { 31 | return true; 32 | } 33 | var participantNames = getParticipantNames(); 34 | return new HashSet<>(participantNames).containsAll(names); 35 | } 36 | 37 | private List getParticipantNames() { 38 | return participants.stream() 39 | .map(Participant::getName) 40 | .collect(Collectors.toList()); 41 | } 42 | 43 | private void validateNamesNull(List names) { 44 | if (names == null) { 45 | throw new IllegalArgumentException("이름에 null이 올 수 없습니다."); 46 | } 47 | } 48 | 49 | public List getExcludedParticipantNames(List availableParticipantNames) { 50 | return participants.stream() 51 | .map(Participant::getName) 52 | .filter(name -> !availableParticipantNames.contains(name)) 53 | .collect(Collectors.toList()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/controller/RoomController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.controller; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.AdjustmentResultService; 4 | import com.dnd.modutime.core.room.application.RoomService; 5 | import com.dnd.modutime.core.room.application.request.RoomRequest; 6 | import com.dnd.modutime.core.room.application.response.RoomCreationResponse; 7 | import com.dnd.modutime.core.room.application.response.RoomInfoResponse; 8 | import com.dnd.modutime.core.room.application.response.V2RoomInfoResponse; 9 | import com.dnd.modutime.core.timetable.application.TimeTableService; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | @RestController 15 | @RequiredArgsConstructor 16 | public class RoomController { 17 | 18 | private final RoomService roomService; 19 | private final TimeTableService timeTableService; 20 | private final AdjustmentResultService adjustmentResultService; 21 | 22 | @PostMapping("/api/room") 23 | public ResponseEntity create(@RequestBody RoomRequest roomRequest) { 24 | RoomCreationResponse roomCreationResponse = roomService.create(roomRequest); 25 | timeTableService.create(roomCreationResponse.getUuid()); 26 | adjustmentResultService.create(roomCreationResponse.getUuid()); 27 | 28 | return ResponseEntity.ok(roomCreationResponse); 29 | } 30 | 31 | @GetMapping("/api/room/{roomUuid}") 32 | public ResponseEntity getInfo(@PathVariable String roomUuid) { 33 | RoomInfoResponse roomInfoResponse = roomService.getInfo(roomUuid); 34 | return ResponseEntity.ok(roomInfoResponse); 35 | } 36 | 37 | @GetMapping("/api/v2/room/{roomUuid}") 38 | public ResponseEntity v2getInfo(@PathVariable String roomUuid) { 39 | var roomInfoResponse = roomService.v2getInfo(roomUuid); 40 | return ResponseEntity.ok(roomInfoResponse); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/application/response/TimeBlockResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.application.response; 2 | 3 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 4 | import com.dnd.modutime.core.timeblock.domain.AvailableTime; 5 | import com.fasterxml.jackson.annotation.JsonFormat; 6 | import java.time.LocalDate; 7 | import java.time.LocalDateTime; 8 | import java.time.LocalTime; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import lombok.AccessLevel; 12 | import lombok.AllArgsConstructor; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Getter 17 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 18 | @AllArgsConstructor 19 | public class TimeBlockResponse { 20 | 21 | private String name; 22 | 23 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm", timezone = "Asia/Seoul") 24 | private List availableDateTimes; 25 | 26 | public static TimeBlockResponse of(String participantName, 27 | List availableDateTimes) { 28 | List dateTimes = new ArrayList<>(); 29 | for (AvailableDateTime availableDateTime : availableDateTimes) { 30 | addDateTimes(dateTimes, availableDateTime); 31 | } 32 | return new TimeBlockResponse(participantName, dateTimes); 33 | } 34 | 35 | private static void addDateTimes(List dateTimes, 36 | AvailableDateTime availableDateTime) { 37 | LocalDate date = availableDateTime.getDate(); 38 | List timesOrNull = availableDateTime.getTimesOrNull(); 39 | if (timesOrNull.isEmpty()) { 40 | dateTimes.add(LocalDateTime.of(date, LocalTime.of(0, 0))); 41 | return; 42 | } 43 | for (AvailableTime availableTime : timesOrNull) { 44 | LocalTime time = availableTime.getTime(); 45 | dateTimes.add(LocalDateTime.of(date, time)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/room/util/CandidateDateTimeConvertorFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.util; 2 | 3 | import static com.dnd.modutime.fixture.RoomFixture.getRoom; 4 | import static com.dnd.modutime.fixture.RoomFixture.getRoomByStartEndTime; 5 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | import static org.mockito.BDDMockito.given; 8 | 9 | import com.dnd.modutime.core.adjustresult.util.convertor.CandidateDateTimeConvertor; 10 | import com.dnd.modutime.core.adjustresult.util.convertor.DateRoomConvertor; 11 | import com.dnd.modutime.core.adjustresult.util.convertor.DateTimeRoomConvertor; 12 | import com.dnd.modutime.core.room.repository.RoomRepository; 13 | import java.util.Optional; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.boot.test.mock.mockito.MockBean; 18 | 19 | @SpringBootTest 20 | class CandidateDateTimeConvertorFactoryTest { 21 | 22 | @Autowired 23 | private CandidateDateTimeConvertorFactory candidateDateTimeConvertorFactory; 24 | 25 | @MockBean 26 | private RoomRepository roomRepository; 27 | 28 | @Test 29 | void 시간이없는_방이라면_dateRoomConvertor_을_반환한다() { 30 | given(roomRepository.findByUuid(ROOM_UUID)).willReturn( 31 | Optional.of(getRoomByStartEndTime(null, null)) 32 | ); 33 | CandidateDateTimeConvertor convertor = candidateDateTimeConvertorFactory.getInstance(ROOM_UUID); 34 | assertThat(convertor).isInstanceOf(DateRoomConvertor.class); 35 | } 36 | 37 | @Test 38 | void 시간이_포함된_방이라면_dateTimeRoomConvertor_을_반환한다() { 39 | given(roomRepository.findByUuid(ROOM_UUID)).willReturn( 40 | Optional.of(getRoom()) 41 | ); 42 | CandidateDateTimeConvertor convertor = candidateDateTimeConvertorFactory.getInstance(ROOM_UUID); 43 | assertThat(convertor).isInstanceOf(DateTimeRoomConvertor.class); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/adjustresult/domain/CandidateDateTimeTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.dnd.modutime.core.adjustresult.domain; 3 | 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class CandidateDateTimeTest { 12 | 13 | @DisplayName("참여자가 모두 동일한지 확인한다") 14 | @Test 15 | void test01() { 16 | // given 17 | var candidate = new CandidateDateTime( 18 | null, 19 | LocalDateTime.now(), 20 | LocalDateTime.now(), 21 | false, 22 | List.of( 23 | new CandidateDateTimeParticipantName("alice"), 24 | new CandidateDateTimeParticipantName("bob"), 25 | new CandidateDateTimeParticipantName("carol") 26 | ) 27 | ); 28 | 29 | var participants = List.of( 30 | "carol", 31 | "alice", 32 | "bob" 33 | ); 34 | 35 | // when 36 | boolean result = candidate.containsExactly(participants); 37 | 38 | // then 39 | assertThat(result).isTrue(); 40 | } 41 | 42 | @DisplayName("참여자가 한명이라도 다르면 false를 반환한다") 43 | @Test 44 | void test02() { 45 | // given 46 | var candidate = new CandidateDateTime( 47 | null, 48 | LocalDateTime.now(), 49 | LocalDateTime.now(), 50 | false, 51 | List.of( 52 | new CandidateDateTimeParticipantName("alice"), 53 | new CandidateDateTimeParticipantName("bob"), 54 | new CandidateDateTimeParticipantName("carol") 55 | ) 56 | ); 57 | 58 | var participants = List.of("carol", "alice"); 59 | 60 | // when 61 | boolean result = candidate.containsExactly(participants); 62 | 63 | // then 64 | assertThat(result).isFalse(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/adjustresult/application/AdjustmentResultEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.command.AdjustmentResultReplaceCommand; 4 | import com.dnd.modutime.core.timetable.application.TimeTableService; 5 | import com.dnd.modutime.core.timetable.application.command.TimeTableUpdateCommand; 6 | import com.dnd.modutime.core.timetable.domain.TimeTable; 7 | import com.dnd.modutime.core.timetable.repository.TimeTableRepository; 8 | import com.dnd.modutime.util.JsonUtils; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.BDDMockito.given; 20 | import static org.mockito.Mockito.verify; 21 | 22 | @SpringBootTest 23 | class AdjustmentResultEventHandlerTest { 24 | 25 | @Autowired 26 | private TimeTableService timeTableService; 27 | 28 | @MockBean 29 | private TimeTableRepository timeTableRepository; 30 | 31 | @MockBean 32 | private AdjustmentResultReplaceService adjustmentResultReplaceService; 33 | 34 | @DisplayName("타임테이블이 변경되면 조율결과 변경이 호출된다.") 35 | @Test 36 | void test01() { 37 | //language=json 38 | var timeTableLiteral = """ 39 | { 40 | "roomUuid": "room-uuid" 41 | } 42 | """; 43 | var timeTable = JsonUtils.readValue(timeTableLiteral, TimeTable.class); 44 | given(timeTableRepository.findByRoomUuid(any())).willReturn(Optional.of(timeTable)); 45 | timeTableService.update(TimeTableUpdateCommand.of( 46 | "room-uuid", 47 | List.of(), 48 | List.of(), 49 | "참여자1" 50 | )); 51 | 52 | verify(adjustmentResultReplaceService).replace(any(AdjustmentResultReplaceCommand.class)); 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/participant/application/ParticipantFacadeTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.application; 2 | 3 | import com.dnd.modutime.core.participant.application.command.ParticipantCreateCommand; 4 | import com.dnd.modutime.core.room.application.RoomService; 5 | import com.dnd.modutime.exception.InvalidPasswordException; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | import static com.dnd.modutime.fixture.RoomRequestFixture.getRoomRequest; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 13 | 14 | @SpringBootTest 15 | class ParticipantFacadeTest { 16 | 17 | @Autowired 18 | private RoomService roomService; 19 | 20 | @Autowired 21 | private ParticipantFacade facade; 22 | 23 | @Autowired 24 | private ParticipantQueryService queryService; 25 | 26 | @Test 27 | void 방에_존재하지_않는_이름과_패스워드로_로그인요청을_하면_새로운_참여자를_생성한다() { 28 | // given 29 | var roomRequest = getRoomRequest(); 30 | var roomCreationResponse = roomService.create(roomRequest); 31 | 32 | // when 33 | var command = ParticipantCreateCommand.of(roomCreationResponse.getUuid(), "참여자1", "1234"); 34 | facade.login(command); 35 | 36 | // then 37 | var actual = queryService.getByRoomUuidAndName( 38 | roomCreationResponse.getUuid(), command.getName()); 39 | assertThat(actual.isPresent()).isTrue(); 40 | } 41 | 42 | @Test 43 | void 방에_존재하는_이름과_올바르지_않은_패스워드로_로그인요청을_하면_예외를_반환한다() { 44 | // given 45 | var roomRequest = getRoomRequest(); 46 | var roomCreationResponse = roomService.create(roomRequest); 47 | facade.login(ParticipantCreateCommand.of(roomCreationResponse.getUuid(), "참여자1", "1234")); 48 | 49 | // when & then 50 | var command = ParticipantCreateCommand.of(roomCreationResponse.getUuid(), "참여자1", "9999"); 51 | assertThatThrownBy(() -> facade.login(command)) 52 | .isInstanceOf(InvalidPasswordException.class); 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/domain/TimeInfoParticipantName.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.EntityListeners; 6 | import javax.persistence.FetchType; 7 | import javax.persistence.GeneratedValue; 8 | import javax.persistence.GenerationType; 9 | import javax.persistence.Id; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.ManyToOne; 12 | import lombok.AccessLevel; 13 | import lombok.NoArgsConstructor; 14 | import com.dnd.modutime.core.entity.Auditable; 15 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 16 | import java.time.LocalDateTime; 17 | 18 | @Entity 19 | @EntityListeners(AuditingEntityListener.class) 20 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 21 | public class TimeInfoParticipantName implements Auditable { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | private Long id; 26 | 27 | @ManyToOne(fetch = FetchType.LAZY) 28 | @JoinColumn(name = "time_info_id") 29 | private TimeInfo timeInfo; 30 | 31 | @Column(nullable = false) 32 | private String name; 33 | 34 | private String createdBy; 35 | private LocalDateTime createdAt; 36 | private String modifiedBy; 37 | private LocalDateTime modifiedAt; 38 | 39 | public TimeInfoParticipantName(TimeInfo timeInfo, 40 | String name) { 41 | this.timeInfo = timeInfo; 42 | this.name = name; 43 | } 44 | 45 | public boolean isSameName(String name) { 46 | return this.name.equals(name); 47 | } 48 | 49 | public String getName() { 50 | return name; 51 | } 52 | 53 | @Override 54 | public void setCreatedBy(String createdBy) { 55 | this.createdBy = createdBy; 56 | } 57 | 58 | @Override 59 | public void setCreatedAt(LocalDateTime createdAt) { 60 | this.createdAt = createdAt; 61 | } 62 | 63 | @Override 64 | public void setModifiedBy(String modifiedBy) { 65 | this.modifiedBy = modifiedBy; 66 | } 67 | 68 | @Override 69 | public void setModifiedAt(LocalDateTime modifiedAt) { 70 | this.modifiedAt = modifiedAt; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/common/convert/DisplayableEnumJsonConverter.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.common.convert; 2 | 3 | import com.dnd.modutime.common.DisplayableEnum; 4 | import com.fasterxml.jackson.core.JsonGenerator; 5 | import com.fasterxml.jackson.core.JsonParser; 6 | import com.fasterxml.jackson.databind.*; 7 | import com.fasterxml.jackson.databind.deser.ContextualDeserializer; 8 | import lombok.NoArgsConstructor; 9 | import org.springframework.boot.jackson.JsonComponent; 10 | 11 | import java.io.IOException; 12 | 13 | @JsonComponent 14 | public class DisplayableEnumJsonConverter { 15 | public static final String FIELD_NAME_CODE = "code"; 16 | public static final String FIELD_NAME_TEXT = "text"; 17 | 18 | public static class Serializer extends JsonSerializer { 19 | @Override 20 | public void serialize(DisplayableEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 21 | gen.writeStartObject(); 22 | gen.writeStringField(FIELD_NAME_CODE, value.getCode()); 23 | gen.writeStringField(FIELD_NAME_TEXT, value.getText()); 24 | gen.writeEndObject(); 25 | } 26 | } 27 | 28 | @NoArgsConstructor 29 | public static class Deserializer> extends JsonDeserializer implements ContextualDeserializer { 30 | private Class targetClass; 31 | 32 | public Deserializer(Class targetClass) { 33 | this.targetClass = targetClass; 34 | } 35 | 36 | @Override 37 | public Class handledType() { 38 | return targetClass; 39 | } 40 | 41 | @Override 42 | public Deserializer createContextual(DeserializationContext ctxt, BeanProperty property) { 43 | return new Deserializer(ctxt.getContextualType().getRawClass()); 44 | } 45 | 46 | @Override 47 | public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 48 | var node = (JsonNode) p.getCodec().readTree(p); 49 | if (node.get(FIELD_NAME_CODE) != null) { 50 | return T.valueOf(targetClass, node.get(FIELD_NAME_CODE).asText()); 51 | } 52 | return T.valueOf(targetClass, node.asText()); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/adjustresult/util/sorter/FastFirstSorterTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.sorter; 2 | 3 | import static com.dnd.modutime.fixture.TimeFixture._11_00; 4 | import static com.dnd.modutime.fixture.TimeFixture._12_00; 5 | import static com.dnd.modutime.fixture.TimeFixture._14_00; 6 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_08; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 10 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTimeParticipantName; 11 | import java.time.LocalDateTime; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | import org.junit.jupiter.api.Test; 16 | 17 | class FastFirstSorterTest { 18 | 19 | private final CandidateDateTimesSorter candidateDateTimesSorter = new FastFirstSorter(); 20 | 21 | @Test 22 | void 인원이_가장_많고_빠른시간_순으로_정렬된다() { 23 | List candidateDateTimes = new ArrayList<>(); 24 | candidateDateTimes.add(getCandidateTime(LocalDateTime.of(_2023_02_08, _12_00), LocalDateTime.of(_2023_02_08, _14_00), List.of("수진", "동호", "주현"))); 25 | candidateDateTimes.add(getCandidateTime(LocalDateTime.of(_2023_02_08, _11_00), LocalDateTime.of(_2023_02_08, _14_00), List.of("수진", "동호", "세희"))); 26 | candidateDateTimes.add(getCandidateTime(LocalDateTime.of(_2023_02_08, _14_00), LocalDateTime.of(_2023_02_08, _14_00), List.of("수진"))); 27 | candidateDateTimesSorter.sort(candidateDateTimes); 28 | assertThat(candidateDateTimes.stream() 29 | .map(CandidateDateTime::getStartDateTime) 30 | .collect(Collectors.toList())) 31 | .containsExactly(LocalDateTime.of(_2023_02_08, _11_00), 32 | LocalDateTime.of(_2023_02_08, _12_00), 33 | LocalDateTime.of(_2023_02_08, _14_00)); 34 | } 35 | 36 | private CandidateDateTime getCandidateTime(LocalDateTime startTime, LocalDateTime endTime, List names) { 37 | return new CandidateDateTime(null, startTime, endTime, false, 38 | names.stream() 39 | .map(CandidateDateTimeParticipantName::new) 40 | .collect(Collectors.toList())); 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/controller/AdjustmentResultController.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.controller; 2 | 3 | import com.dnd.modutime.core.Page; 4 | import com.dnd.modutime.core.adjustresult.application.AdjustmentResultService; 5 | import com.dnd.modutime.core.adjustresult.application.response.AdjustmentResultResponse; 6 | import com.dnd.modutime.core.adjustresult.application.response.CandidateDateTimeResponseV1; 7 | import com.dnd.modutime.core.adjustresult.controller.dto.AdjustmentResultRequest; 8 | import com.dnd.modutime.infrastructure.PageRequest; 9 | import java.util.List; 10 | import javax.validation.Valid; 11 | import lombok.RequiredArgsConstructor; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.PathVariable; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | @RestController 19 | @RequiredArgsConstructor 20 | public class AdjustmentResultController { 21 | 22 | private final AdjustmentResultService adjustmentResultService; 23 | 24 | @GetMapping("/api/room/{roomUuid}/adjustment-result") 25 | public ResponseEntity getAdjustmentResult(@PathVariable String roomUuid, 26 | @RequestParam(defaultValue = "fast") String sorted, 27 | @RequestParam(value = "name", defaultValue = "") List names) { 28 | AdjustmentResultResponse adjustmentResultResponse = adjustmentResultService.getByRoomUuidAndSortedAndNames( 29 | roomUuid, sorted, names 30 | ); 31 | return ResponseEntity.ok(adjustmentResultResponse); 32 | } 33 | 34 | @GetMapping("/api/v1/room/{roomUuid}/adjustment-results") 35 | public Page v1getAdjustmentResult(@PathVariable String roomUuid, 36 | @Valid AdjustmentResultRequest request, 37 | PageRequest pageRequest) { 38 | return this.adjustmentResultService.search(request.toSearchCondition(roomUuid), pageRequest); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/acceptance/ParticipantAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.acceptance; 2 | 3 | import com.dnd.modutime.core.participant.controller.dto.ParticipantsDeleteRequest; 4 | import com.dnd.modutime.core.room.application.response.RoomCreationResponse; 5 | import com.dnd.modutime.core.room.application.response.V2RoomInfoResponse; 6 | import io.restassured.response.ExtractableResponse; 7 | import io.restassured.response.Response; 8 | import org.assertj.core.api.SoftAssertions; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.http.HttpStatus; 12 | 13 | import java.time.LocalDateTime; 14 | import java.util.List; 15 | 16 | import static com.dnd.modutime.fixture.TimeFixture.*; 17 | 18 | public class ParticipantAcceptanceTest extends AcceptanceSupporter { 19 | 20 | private static final String PARTICIPANT_NAME = "참여자1"; 21 | 22 | @DisplayName("참여자를 삭제한다.") 23 | @Test 24 | void test01() { 25 | RoomCreationResponse roomCreationResponse = 방_생성(); 26 | String roomUuid = roomCreationResponse.getUuid(); 27 | 로그인_참여자_1234(roomUuid, "이채민"); 28 | 로그인_참여자_1234(roomUuid, "김주현"); 29 | 로그인_참여자_1234(roomUuid, "김동호"); 30 | 시간을_등록한다(roomUuid, PARTICIPANT_NAME, false, 31 | List.of(LocalDateTime.of(_2023_02_09, _11_00), 32 | LocalDateTime.of(_2023_02_09, _12_00)) 33 | ); 34 | var roomInfo1 = get("/api/v2/room/" + roomCreationResponse.getUuid()) 35 | .body() 36 | .as(V2RoomInfoResponse.class); 37 | var target1 = roomInfo1.getParticipants().get(0).id(); 38 | var target2 = roomInfo1.getParticipants().get(1).id(); 39 | var request = new ParticipantsDeleteRequest(List.of(target1, target2)); 40 | var response = 참여자를_삭제한다(roomUuid, request); 41 | 42 | ExtractableResponse roomInfoResponse = get("/api/v2/room/" + roomCreationResponse.getUuid()); 43 | var roomInfo = roomInfoResponse.body().as(V2RoomInfoResponse.class); 44 | 45 | var remain = roomInfo1.getParticipants().get(2); 46 | SoftAssertions.assertSoftly(softAssertions -> { 47 | softAssertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 48 | softAssertions.assertThat(roomInfo.getParticipants()).hasSize(1).containsExactly(remain); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/participant/integration/ParticipantIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.integration; 2 | 3 | import com.dnd.modutime.core.participant.application.ParticipantCommandHandler; 4 | import com.dnd.modutime.core.participant.application.ParticipantFacade; 5 | import com.dnd.modutime.core.participant.application.ParticipantQueryService; 6 | import com.dnd.modutime.core.participant.application.command.ParticipantCreateCommand; 7 | import com.dnd.modutime.core.participant.application.command.ParticipantsDeleteCommand; 8 | import com.dnd.modutime.core.participant.domain.ParticipantRemovedEvent; 9 | import org.assertj.core.api.SoftAssertions; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.test.context.event.ApplicationEvents; 15 | import org.springframework.test.context.event.RecordApplicationEvents; 16 | 17 | import java.util.List; 18 | 19 | @SpringBootTest 20 | @RecordApplicationEvents 21 | public class ParticipantIntegrationTest { 22 | 23 | @Autowired 24 | private ParticipantCommandHandler participantCommandHandler; 25 | 26 | @Autowired 27 | private ParticipantFacade participantFacade; 28 | 29 | @Autowired 30 | private ParticipantQueryService participantQueryService; 31 | 32 | @Autowired 33 | private ApplicationEvents events; 34 | 35 | @DisplayName("참여자를 삭제한다.") 36 | @Test 37 | void test01() { 38 | // given 39 | var roomUuid = "roomUuid"; 40 | var name = "name"; 41 | var password = "1234"; 42 | var participant1 = participantCommandHandler.handle(ParticipantCreateCommand.of(roomUuid, name, password)); 43 | var participant2 = participantCommandHandler.handle(ParticipantCreateCommand.of(roomUuid, "name2", password)); 44 | 45 | // when 46 | var command = ParticipantsDeleteCommand.of(roomUuid, List.of(participant1.getId(), participant2.getId())); 47 | participantFacade.delete(command); 48 | 49 | // then 50 | SoftAssertions.assertSoftly(softly -> { 51 | softly.assertThat(participantQueryService.getByRoomUuidAndName(roomUuid, name)).isEmpty(); 52 | softly.assertThat(events.stream(ParticipantRemovedEvent.class).count()).isEqualTo(2); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/AdjustmentResultService.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application; 2 | 3 | import com.dnd.modutime.core.Page; 4 | import com.dnd.modutime.core.adjustresult.application.condition.AdjustmentResultSearchCondition; 5 | import com.dnd.modutime.core.adjustresult.application.response.AdjustmentResultResponse; 6 | import com.dnd.modutime.core.adjustresult.application.response.CandidateDateTimeResponseV1; 7 | import com.dnd.modutime.core.adjustresult.domain.AdjustmentResult; 8 | import com.dnd.modutime.core.adjustresult.repository.AdjustmentResultRepository; 9 | import com.dnd.modutime.core.adjustresult.util.executor.AdjustmentResultExecutorFactory; 10 | import com.dnd.modutime.core.adjustresult.util.executor.AdjustmentResultResponseGenerator; 11 | import com.dnd.modutime.infrastructure.PageRequest; 12 | import java.util.List; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | @Service 18 | @Transactional 19 | @RequiredArgsConstructor 20 | public class AdjustmentResultService { 21 | 22 | private final AdjustmentResultRepository adjustmentResultRepository; 23 | private final AdjustmentResultExecutorFactory adjustmentResultExecutorFactory; 24 | 25 | @Transactional(readOnly = true) 26 | public AdjustmentResultResponse getByRoomUuidAndSortedAndNames(String roomUuid, 27 | String sorted, 28 | List names) { 29 | AdjustmentResultResponseGenerator adjustmentResultResponseGenerator = adjustmentResultExecutorFactory.getInstance(roomUuid, names); 30 | return adjustmentResultResponseGenerator.generate(roomUuid, CandidateDateTimeSortStandard.getByValue(sorted), names); 31 | } 32 | 33 | @Transactional(readOnly = true) 34 | public Page search(AdjustmentResultSearchCondition condition, PageRequest pageRequest) { 35 | var adjustmentResultResponseGenerator = this.adjustmentResultExecutorFactory.getInstance(condition.getRoomUuid(), condition.getParticipantNames()); 36 | return adjustmentResultResponseGenerator.v1generate(condition, pageRequest); 37 | } 38 | 39 | public void create(String roomUuid) { 40 | adjustmentResultRepository.save(new AdjustmentResult(roomUuid, List.of())); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/domain/AdjustmentResult.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.domain; 2 | 3 | import com.dnd.modutime.core.entity.Auditable; 4 | import lombok.AccessLevel; 5 | import lombok.NoArgsConstructor; 6 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 7 | 8 | import javax.persistence.*; 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | 12 | @Entity 13 | @EntityListeners(AuditingEntityListener.class) 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | public class AdjustmentResult implements Auditable { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | @Column(nullable = false, unique = true) 22 | private String roomUuid; 23 | 24 | @Column(nullable = false) 25 | private boolean confirmation; 26 | 27 | @OneToMany(mappedBy = "adjustmentResult", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) 28 | private List candidateDateTimes; 29 | 30 | private String createdBy; 31 | private LocalDateTime createdAt; 32 | private String modifiedBy; 33 | private LocalDateTime modifiedAt; 34 | 35 | public AdjustmentResult(String roomUuid, 36 | List candidateDateTimes) { 37 | this.roomUuid = roomUuid; 38 | this.candidateDateTimes = candidateDateTimes; 39 | this.confirmation = false; 40 | } 41 | 42 | public void replace(List candidateDateTimes) { 43 | this.candidateDateTimes = candidateDateTimes; 44 | } 45 | 46 | public Long getId() { 47 | return id; 48 | } 49 | 50 | public String getRoomUuid() { 51 | return roomUuid; 52 | } 53 | 54 | public boolean isConfirmation() { 55 | return confirmation; 56 | } 57 | 58 | public List getCandidateDateTimes() { 59 | return candidateDateTimes; 60 | } 61 | 62 | @Override 63 | public void setCreatedBy(String createdBy) { 64 | this.createdBy = createdBy; 65 | } 66 | 67 | @Override 68 | public void setCreatedAt(LocalDateTime createdAt) { 69 | this.createdAt = createdAt; 70 | } 71 | 72 | @Override 73 | public void setModifiedBy(String modifiedBy) { 74 | this.modifiedBy = modifiedBy; 75 | } 76 | 77 | @Override 78 | public void setModifiedAt(LocalDateTime modifiedAt) { 79 | this.modifiedAt = modifiedAt; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/participant/domain/ParticipantTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.participant.domain; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 5 | 6 | import com.dnd.modutime.core.participant.domain.Email; 7 | import com.dnd.modutime.core.participant.domain.Participant; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.ValueSource; 11 | 12 | public class ParticipantTest { 13 | 14 | @Test 15 | void 이름값이_null이면_예외가_발생한다() { 16 | assertThatThrownBy(() -> getParticipant(null, "1234")) 17 | .isInstanceOf(IllegalArgumentException.class); 18 | } 19 | 20 | @ParameterizedTest 21 | @ValueSource(strings = {"123", "12345", "abcd"}) 22 | void 비밀번호는_4자리_숫자가_아니라면_예외를_반환한다(String password) { 23 | assertThatThrownBy(() -> getParticipant("김동호", password)) 24 | .isInstanceOf(IllegalArgumentException.class); 25 | } 26 | 27 | @Test 28 | void 이메일을_추가한다() { 29 | Participant participant = getParticipant("김동호", "1234"); 30 | participant.registerEmail(new Email("participant@email.com")); 31 | assertThat(participant.hasEmail()).isTrue(); 32 | } 33 | 34 | @Test 35 | void 이메일은_null이면_이메일을_가지고_있지_않다고_판단한다() { 36 | Participant participant = getParticipant("김동호", "1234"); 37 | assertThat(participant.hasEmail()).isFalse(); 38 | } 39 | 40 | @Test 41 | void roomUuid값이_null이면_예외가_발생한다() { 42 | assertThatThrownBy(() -> getParticipant(null, "participant","1234")) 43 | .isInstanceOf(IllegalArgumentException.class); 44 | } 45 | 46 | @Test 47 | void password가_일치하면_true를_반환한다() { 48 | Participant participant = getParticipant("김동호", "1234"); 49 | assertThat(participant.matchPassword("1234")).isTrue(); 50 | } 51 | 52 | @Test 53 | void password가_일치하지_않으면_false를_반환한다() { 54 | Participant participant = getParticipant("김동호", "1234"); 55 | assertThat(participant.matchPassword("9999")).isFalse(); 56 | } 57 | 58 | private Participant getParticipant(String name, String password) { 59 | return getParticipant("7c64aa0e-6e8f-4f61-b8ee-d5a86493d3a9", name, password); 60 | } 61 | 62 | private Participant getParticipant(String roomUuid, String name, String password) { 63 | return new Participant(roomUuid, name, password); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timetable/domain/TimeTableTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.domain; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static com.dnd.modutime.fixture.TimeFixture._12_00; 5 | import static com.dnd.modutime.fixture.TimeFixture._13_00; 6 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_08; 7 | import static com.dnd.modutime.fixture.TimeFixture._2023_02_09; 8 | import static com.dnd.modutime.fixture.TimeTableFixture.getDateInfo; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.junit.jupiter.api.Assertions.assertAll; 11 | 12 | import com.dnd.modutime.core.adjustresult.application.DateTimeInfoDto; 13 | import com.dnd.modutime.core.timetable.domain.TimeInfo; 14 | import com.dnd.modutime.core.timetable.domain.TimeInfoParticipantName; 15 | import com.dnd.modutime.core.timetable.domain.TimeTable; 16 | import java.time.LocalDateTime; 17 | import java.time.LocalTime; 18 | import java.util.List; 19 | import java.util.stream.Collectors; 20 | import org.junit.jupiter.api.Test; 21 | 22 | public class TimeTableTest { 23 | 24 | @Test 25 | void 참여자이름이_모두포함된_DateInfo와_TimeInfo로_DateTimeInfosDto를_반환한다() { 26 | TimeTable timeTable = new TimeTable(ROOM_UUID); 27 | timeTable.replaceDateInfos(List.of( 28 | getDateInfo(_2023_02_08, List.of(getTimeInfo(_12_00, List.of("김동호", "이채민")), getTimeInfo(_13_00, List.of("김동호", "이수진")))), 29 | getDateInfo(_2023_02_09, List.of(getTimeInfo(_12_00, List.of("김동호", "이수진")), getTimeInfo(_13_00, List.of("이채민", "이수진")))) 30 | )); 31 | 32 | List dateTimeInfosDto = timeTable.getDateTimeInfosDtoByParticipantNames(List.of("김동호", "이수진")); 33 | DateTimeInfoDto dateTimeInfoDto1 = dateTimeInfosDto.get(0); 34 | DateTimeInfoDto dateTimeInfoDto2 = dateTimeInfosDto.get(1); 35 | assertAll( 36 | () -> assertThat(dateTimeInfoDto1.getDateTime()).isEqualTo(LocalDateTime.of(_2023_02_08, _13_00)), 37 | () -> assertThat(dateTimeInfoDto1.getParticipantNames()).hasSize(2).contains("김동호", "이수진"), 38 | () -> assertThat(dateTimeInfoDto2.getDateTime()).isEqualTo(LocalDateTime.of(_2023_02_09, _12_00)), 39 | () -> assertThat(dateTimeInfoDto2.getParticipantNames()).hasSize(2).contains("김동호", "이수진") 40 | ); 41 | } 42 | 43 | private TimeInfo getTimeInfo(LocalTime time, List names) { 44 | return new TimeInfo(time, names.stream() 45 | .map(name -> new TimeInfoParticipantName(null, name)) 46 | .collect(Collectors.toList())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/acceptance/AuthAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.acceptance; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.assertAll; 5 | 6 | import com.dnd.modutime.core.auth.application.request.LoginRequest; 7 | import com.dnd.modutime.core.auth.application.response.LoginPageResponse; 8 | import com.dnd.modutime.core.room.application.response.RoomCreationResponse; 9 | import io.restassured.response.ExtractableResponse; 10 | import io.restassured.response.Response; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.http.HttpStatus; 13 | 14 | public class AuthAcceptanceTest extends AcceptanceSupporter{ 15 | 16 | @Test 17 | void 방에_존재하지_않는_이름과_패스워드로_로그인요청을_하면_200_상태코드를_반환한다() { 18 | RoomCreationResponse roomCreationResponse = 방_생성(); 19 | ExtractableResponse response = 로그인_참여자_1234(roomCreationResponse.getUuid(), "참여자1"); 20 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 21 | } 22 | 23 | @Test 24 | void 방에_이미존재하는이름과_올바른_패스워드로_로그인요청을_하면_200_상태코드를_반환한다() { 25 | RoomCreationResponse roomCreationResponse = 방_생성(); 26 | 로그인_참여자_1234(roomCreationResponse.getUuid(), "참여자1"); 27 | 28 | LoginRequest loginRequest = new LoginRequest("참여자1", "1234"); 29 | ExtractableResponse response = post("/api/room/" + roomCreationResponse.getUuid() + "/login", loginRequest); 30 | assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); 31 | } 32 | 33 | @Test 34 | void 방에_이미존재하는이름과_올바르지_않은_패스워드로_로그인요청을_하면_401_상태코드를_반환한다() { 35 | RoomCreationResponse roomCreationResponse = 방_생성(); 36 | 로그인_참여자_1234(roomCreationResponse.getUuid(), "참여자1"); 37 | 38 | LoginRequest invalidLoginRequest = new LoginRequest("참여자1", "9999"); 39 | ExtractableResponse response = post("/api/room/" + roomCreationResponse.getUuid() + "/login", invalidLoginRequest); 40 | assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); 41 | } 42 | 43 | @Test 44 | void 로그인페이지_입장시_200_상태코드와_로그인페이지에_필요한_정보를_반환한다() { 45 | RoomCreationResponse roomCreationResponse = 방_생성(); 46 | ExtractableResponse response = get("/api/room/" + roomCreationResponse.getUuid() + "/login"); 47 | LoginPageResponse loginPageResponse = response.body().as(LoginPageResponse.class); 48 | assertAll( 49 | () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), 50 | () -> assertThat(loginPageResponse.getRoomTitle()).isEqualTo("이멤버리멤버") 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/infrastructure/PageResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.infrastructure; 2 | 3 | import com.dnd.modutime.core.Page; 4 | import com.dnd.modutime.core.Pageable; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import lombok.AccessLevel; 10 | import lombok.NoArgsConstructor; 11 | 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class PageResponse implements Page { 14 | public static final int FIRST_PAGE = 1; 15 | 16 | private final List content = new ArrayList<>(); 17 | private PageRequest pageRequest; 18 | private long total; 19 | 20 | public PageResponse(List content, Pageable pageable, long total) { 21 | this.content.addAll(content); 22 | this.pageRequest = PageRequest.of(pageable.getPage(), pageable.getSize()); 23 | this.total = 24 | Optional.of(pageable) 25 | .filter(it -> !content.isEmpty()) 26 | .filter(it -> pageable.getOffset() + it.getSize() > total) 27 | .map(it -> pageable.getOffset() + content.size()) 28 | .orElse(total); 29 | } 30 | 31 | public static PageResponse of(List content, Pageable pageable, long total) { 32 | return new PageResponse<>(content, pageable, total); 33 | } 34 | 35 | @Override 36 | public int getTotalPages() { 37 | if (this.pageRequest.getSize() == 0) { 38 | return FIRST_PAGE; 39 | } 40 | 41 | return (int) Math.ceil(getTotal() / (double) this.pageRequest.getSize()); 42 | } 43 | 44 | @JsonProperty 45 | @Override 46 | public boolean hasPrevious() { 47 | return this.pageRequest.getPage() > 0; 48 | } 49 | 50 | @JsonProperty 51 | @Override 52 | public boolean hasNext() { 53 | return this.pageRequest.getPage() + FIRST_PAGE < getTotalPages(); 54 | } 55 | 56 | @JsonProperty 57 | @Override 58 | public boolean hasContent() { 59 | return !getContent().isEmpty(); 60 | } 61 | 62 | @JsonProperty("isFirst") 63 | @Override 64 | public boolean isFirst() { 65 | return !hasPrevious(); 66 | } 67 | 68 | @JsonProperty("isLast") 69 | @Override 70 | public boolean isLast() { 71 | return !hasNext(); 72 | } 73 | 74 | @Override 75 | public List getContent() { 76 | return content; 77 | } 78 | 79 | @Override 80 | public Pageable getPageRequest() { 81 | return pageRequest; 82 | } 83 | 84 | @Override 85 | public long getTotal() { 86 | return total; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/response/AdjustmentResultResponse.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application.response; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTimeParticipantName; 5 | import com.dnd.modutime.core.participant.domain.Participants; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.NoArgsConstructor; 11 | 12 | import java.time.LocalTime; 13 | import java.time.format.TextStyle; 14 | import java.util.List; 15 | import java.util.Locale; 16 | import java.util.stream.Collectors; 17 | 18 | @Getter 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @AllArgsConstructor 21 | public class AdjustmentResultResponse { 22 | 23 | @JsonProperty(value = "candidateTimes") 24 | private List candidateDateTimeResponse; 25 | 26 | public static AdjustmentResultResponse from(List candidateDateTimes, Participants participants) { 27 | return new AdjustmentResultResponse(candidateDateTimes.stream() 28 | .map(it -> getCandidateDateTimeResponse(it, participants)) 29 | .collect(Collectors.toList())); 30 | } 31 | 32 | private static CandidateDateTimeResponse getCandidateDateTimeResponse(CandidateDateTime candidateDateTime, Participants participants) { 33 | LocalTime startTime = candidateDateTime.getStartDateTime().toLocalTime(); 34 | LocalTime endTime = candidateDateTime.getEndDateTime().toLocalTime(); 35 | if (isZeroTime(startTime, endTime)) { 36 | startTime = null; 37 | endTime = null; 38 | } 39 | var availableParticipantNames = candidateDateTime.getParticipantNames().stream() 40 | .map(CandidateDateTimeParticipantName::getName) 41 | .collect(Collectors.toList()); 42 | return new CandidateDateTimeResponse( 43 | candidateDateTime.getId(), 44 | candidateDateTime.getStartDateTime().toLocalDate(), 45 | candidateDateTime.getStartDateTime().getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.KOREAN), 46 | startTime, 47 | endTime, 48 | availableParticipantNames, 49 | participants.getExcludedParticipantNames(availableParticipantNames), 50 | candidateDateTime.isConfirmed()); 51 | } 52 | 53 | private static boolean isZeroTime(LocalTime startTime, LocalTime endTime) { 54 | return startTime.equals(endTime) && startTime.equals(LocalTime.of(0, 0)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timetable/application/TimeTableService.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application; 2 | 3 | import com.dnd.modutime.core.timetable.application.command.TimeTableUpdateCommand; 4 | import com.dnd.modutime.core.timetable.application.response.TimeTableResponse; 5 | import com.dnd.modutime.core.timetable.domain.TimeTable; 6 | import com.dnd.modutime.core.timetable.domain.TimeTableReplaceEvent; 7 | import com.dnd.modutime.core.timetable.repository.TimeInfoParticipantNameRepository; 8 | import com.dnd.modutime.core.timetable.repository.TimeTableRepository; 9 | import com.dnd.modutime.exception.NotFoundException; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.context.ApplicationEventPublisher; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.util.List; 16 | 17 | @Service 18 | @RequiredArgsConstructor 19 | public class TimeTableService { 20 | 21 | private final TimeTableRepository timeTableRepository; 22 | private final TimeTableInitializer timeTableInitializer; 23 | private final TimeInfoParticipantNameRepository timeInfoParticipantNameRepository; 24 | private final ApplicationEventPublisher eventPublisher; 25 | 26 | @Transactional 27 | public void create(String roomUuid) { 28 | TimeTable timeTable = timeTableRepository.save(new TimeTable(roomUuid)); 29 | timeTableInitializer.initialize(roomUuid, timeTable); 30 | } 31 | 32 | public TimeTableResponse getTimeTable(String roomUuid) { 33 | TimeTable timeTable = getTimeTableByRoomUuid(roomUuid); 34 | return TimeTableResponse.from(timeTable); 35 | } 36 | 37 | private TimeTable getTimeTableByRoomUuid(String roomUuid) { 38 | return timeTableRepository.findByRoomUuid(roomUuid) 39 | .orElseThrow(() -> new NotFoundException("해당하는 TimeTable을 찾을 수 없습니다.")); 40 | } 41 | 42 | @Transactional 43 | public void update(TimeTableUpdateCommand command) { 44 | TimeTable timeTable = getTimeTableByRoomUuid(command.getRoomUuid()); 45 | List timeInfoIds = timeTable.getTimeInfoIdsByAvailableDateTimes(command.getOldAvailableDateTimes()); 46 | 47 | timeTable.removeParticipantName(command.getOldAvailableDateTimes(), command.getParticipantName()); 48 | for (Long timeInfoId : timeInfoIds) { 49 | timeInfoParticipantNameRepository.deleteByTimeInfoIdAndName(timeInfoId, command.getParticipantName()); 50 | } 51 | 52 | timeTable.addParticipantName(command.getNewAvailableDateTimes(), command.getParticipantName()); 53 | timeTableRepository.save(timeTable); 54 | eventPublisher.publishEvent(new TimeTableReplaceEvent(command.getRoomUuid(), timeTable.getDateInfos())); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/adjustresult/util/convertor/DateRoomConvertorTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.convertor; 2 | 3 | import com.dnd.modutime.core.adjustresult.application.DateTimeInfoDto; 4 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 5 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTimeParticipantName; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | 10 | import java.time.LocalDateTime; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | 14 | import static com.dnd.modutime.fixture.TimeFixture.*; 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | @SpringBootTest 19 | class DateRoomConvertorTest { 20 | 21 | @Autowired 22 | private DateRoomConvertor dateRoomConvertor; 23 | 24 | @Test 25 | void 날짜만있는_방의_dateInfosDto를_candidateDateTime으로_변환한다() { 26 | List dateTimeInfosDto = List.of( 27 | new DateTimeInfoDto(LocalDateTime.of(_2023_02_09, _00_00), List.of("김동호", "이수진")), 28 | new DateTimeInfoDto(LocalDateTime.of(_2023_02_10, _00_00), List.of("이세희", "김동호")) 29 | ); 30 | 31 | List candidateDateTimes = dateRoomConvertor.convert(dateTimeInfosDto); 32 | CandidateDateTime candidateDateTimeDto1 = candidateDateTimes.get(0); 33 | CandidateDateTime candidateDateTimeDto2 = candidateDateTimes.get(1); 34 | 35 | assertAll( 36 | () -> assertThat(candidateDateTimeDto1.getStartDateTime()).isEqualTo(LocalDateTime.of(_2023_02_09, _00_00)), 37 | () -> assertThat(candidateDateTimeDto1.getEndDateTime()).isEqualTo(LocalDateTime.of(_2023_02_09, _00_00)), 38 | () -> assertThat(candidateDateTimeDto1.isConfirmed()).isNull(), 39 | () -> assertThat(candidateDateTimeDto1.getParticipantNames().stream() 40 | .map(CandidateDateTimeParticipantName::getName) 41 | .collect(Collectors.toList())).hasSize(2) 42 | .contains("김동호", "이수진"), 43 | () -> assertThat(candidateDateTimeDto2.getStartDateTime()).isEqualTo(LocalDateTime.of(_2023_02_10, _00_00)), 44 | () -> assertThat(candidateDateTimeDto2.getEndDateTime()).isEqualTo(LocalDateTime.of(_2023_02_10, _00_00)), 45 | () -> assertThat(candidateDateTimeDto2.isConfirmed()).isNull(), 46 | () -> assertThat(candidateDateTimeDto2.getParticipantNames().stream() 47 | .map(CandidateDateTimeParticipantName::getName) 48 | .collect(Collectors.toList())).hasSize(2) 49 | .contains("이세희", "김동호") 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/adjustresult/application/response/CandidateDateTimeResponseV1.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.application.response; 2 | 3 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTime; 4 | import com.dnd.modutime.core.adjustresult.domain.CandidateDateTimeParticipantName; 5 | import com.dnd.modutime.core.participant.domain.Participants; 6 | import com.fasterxml.jackson.annotation.JsonFormat; 7 | import java.time.LocalDate; 8 | import java.time.LocalTime; 9 | import java.time.format.TextStyle; 10 | import java.util.List; 11 | import java.util.Locale; 12 | import java.util.stream.Collectors; 13 | import lombok.AccessLevel; 14 | import lombok.AllArgsConstructor; 15 | import lombok.Getter; 16 | import lombok.NoArgsConstructor; 17 | 18 | @Getter 19 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 20 | @AllArgsConstructor 21 | public class CandidateDateTimeResponseV1 { 22 | 23 | private Long id; 24 | 25 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") 26 | private LocalDate date; 27 | private String dayOfWeek; 28 | 29 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 30 | private LocalTime startTime; 31 | 32 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm", timezone = "Asia/Seoul") 33 | private LocalTime endTime; 34 | private List availableParticipantNames; 35 | private List unavailableParticipantNames; 36 | 37 | public static CandidateDateTimeResponseV1 of(CandidateDateTime candidateDateTime, Participants participants) { 38 | final var response = new CandidateDateTimeResponseV1(); 39 | var startTime = candidateDateTime.getStartDateTime().toLocalTime(); 40 | var endTime = candidateDateTime.getEndDateTime().toLocalTime(); 41 | if (isZeroTime(startTime, endTime)) { 42 | startTime = null; 43 | endTime = null; 44 | } 45 | response.id = candidateDateTime.getId(); 46 | response.date = candidateDateTime.getStartDateTime() 47 | .toLocalDate(); 48 | response.dayOfWeek = candidateDateTime.getStartDateTime() 49 | .getDayOfWeek() 50 | .getDisplayName(TextStyle.SHORT, Locale.KOREAN); 51 | response.startTime = startTime; 52 | response.endTime = endTime; 53 | final List availableParticipantNames = candidateDateTime.getParticipantNames().stream() 54 | .map(CandidateDateTimeParticipantName::getName) 55 | .collect(Collectors.toList()); 56 | response.availableParticipantNames = availableParticipantNames; 57 | response.unavailableParticipantNames = participants.getExcludedParticipantNames(availableParticipantNames); 58 | return response; 59 | } 60 | 61 | private static boolean isZeroTime(LocalTime startTime, LocalTime endTime) { 62 | return startTime.equals(endTime) && startTime.equals(LocalTime.of(0, 0)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/adjustresult/util/executor/AdjustmentResultExecutorFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.adjustresult.util.executor; 2 | 3 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 6 | import static org.mockito.BDDMockito.given; 7 | 8 | import com.dnd.modutime.core.participant.application.ParticipantQueryService; 9 | import com.dnd.modutime.core.participant.domain.Participant; 10 | import java.util.List; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.params.ParameterizedTest; 14 | import org.junit.jupiter.params.provider.ValueSource; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.boot.test.mock.mockito.MockBean; 18 | 19 | @SpringBootTest 20 | public class AdjustmentResultExecutorFactoryTest { 21 | 22 | @Autowired 23 | private AdjustmentResultExecutorFactory adjustmentResultExecutorFactory; 24 | 25 | @MockBean 26 | private ParticipantQueryService participantQueryService; 27 | 28 | @BeforeEach 29 | void setUp() { 30 | given(participantQueryService.getByRoomUuid(ROOM_UUID)).willReturn( 31 | List.of(new Participant(ROOM_UUID, "김동호", "1234"), 32 | new Participant(ROOM_UUID, "이수진", "1234")) 33 | ); 34 | } 35 | 36 | @Test 37 | void 참여자이름들이_빈_리스트로_넘어오면_AdjustmentResultExecutor를_반환한다() { 38 | AdjustmentResultResponseGenerator instance = adjustmentResultExecutorFactory.getInstance(ROOM_UUID, List.of()); 39 | assertThat(instance).isInstanceOf(AdjustmentResponseGenerator.class); 40 | } 41 | 42 | @Test 43 | void 참여자이름에_null이_들어오면_AdjustmentResultExecutor를_반환한다() { 44 | final AdjustmentResultResponseGenerator instance = adjustmentResultExecutorFactory.getInstance(ROOM_UUID, null); 45 | assertThat(instance).isInstanceOf(AdjustmentResponseGenerator.class); 46 | } 47 | 48 | @Test 49 | void 참여자이름들이_전체참여자로_넘어오면_AdjustmentResultExecutor를_반환한다() { 50 | AdjustmentResultResponseGenerator instance = adjustmentResultExecutorFactory.getInstance(ROOM_UUID, List.of("김동호", "이수진")); 51 | assertThat(instance).isInstanceOf(AdjustmentResponseGenerator.class); 52 | } 53 | 54 | @ParameterizedTest 55 | @ValueSource(strings = {"김동호", "이수진"}) 56 | void 참여자이름들이_일부만_넘어오면_TimeTableResultExecutor를_반환한다(String name) { 57 | AdjustmentResultResponseGenerator instance = adjustmentResultExecutorFactory.getInstance(ROOM_UUID, List.of(name)); 58 | assertThat(instance).isInstanceOf(TimeTableResponseGenerator.class); 59 | } 60 | 61 | @Test 62 | void 방에_존재하지않는_참여자이름이_들어오면_예외를_반환한다() { 63 | assertThatThrownBy(() -> adjustmentResultExecutorFactory.getInstance(ROOM_UUID, List.of("이채민"))) 64 | .isInstanceOf(IllegalArgumentException.class); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/RoomTimeValidator.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application; 2 | 3 | import com.dnd.modutime.core.room.domain.Room; 4 | import com.dnd.modutime.core.room.domain.RoomDate; 5 | import com.dnd.modutime.core.room.repository.RoomRepository; 6 | import com.dnd.modutime.core.timeblock.application.TimeReplaceValidator; 7 | import com.dnd.modutime.core.timeblock.domain.AvailableDateTime; 8 | import com.dnd.modutime.core.timeblock.domain.AvailableTime; 9 | import com.dnd.modutime.exception.NotFoundException; 10 | import java.time.LocalTime; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.stereotype.Component; 15 | 16 | @Component 17 | @RequiredArgsConstructor 18 | public class RoomTimeValidator implements TimeReplaceValidator { 19 | 20 | private final RoomRepository roomRepository; 21 | 22 | @Override 23 | public void validate(String roomUuid, List availableDateTimes) { 24 | Room room = getRoomByRoomUuid(roomUuid); 25 | validateContainsAllDates(room, availableDateTimes); 26 | validateStartAndEndTime(room, availableDateTimes); 27 | } 28 | 29 | private void validateContainsAllDates(Room room, List availableDateTimes) { 30 | List roomDates = availableDateTimes.stream() 31 | .map(availableDateTime -> new RoomDate(availableDateTime.getDate())) 32 | .collect(Collectors.toList()); 33 | if (!room.containsAllDates(roomDates)) { 34 | throw new IllegalArgumentException(); 35 | } 36 | } 37 | 38 | private void validateStartAndEndTime(Room room, List availableDateTimes) { 39 | if (availableDateTimes.isEmpty()) { 40 | return; 41 | } 42 | if (room.hasStartAndEndTime() && !hasTime(availableDateTimes)) { 43 | throw new IllegalArgumentException("해당 방에는 시간 값이 필요합니다."); 44 | } 45 | if (!room.hasStartAndEndTime() && hasTime(availableDateTimes)) { 46 | throw new IllegalArgumentException("해당 방에는 날짜만 등록할 수 있습니다."); 47 | } 48 | if (!room.hasStartAndEndTime() && !hasTime(availableDateTimes)) { 49 | return; 50 | } 51 | availableDateTimes.forEach(it -> validateIncludeTimes(room, it.getTimesOrNull())); 52 | } 53 | 54 | private void validateIncludeTimes(Room room, List availableTimes) { 55 | availableTimes.forEach(it -> validateIncludeTime(room, it.getTime())); 56 | } 57 | 58 | private void validateIncludeTime(Room room, LocalTime time) { 59 | if (!room.includeTime(time)) { 60 | throw new IllegalArgumentException("방의 범위 밖의 시간입니다."); 61 | } 62 | } 63 | 64 | private boolean hasTime(List availableDateTimes) { 65 | return availableDateTimes.stream() 66 | .allMatch(AvailableDateTime::hasTime); 67 | } 68 | 69 | private Room getRoomByRoomUuid(String roomUuid) { 70 | return roomRepository.findByUuid(roomUuid) 71 | .orElseThrow(() -> new NotFoundException("해당하는 방이 없습니다.")); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timetable/application/TimeTableEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timetable.application; 2 | 3 | import com.dnd.modutime.core.timeblock.application.TimeBlockService; 4 | import com.dnd.modutime.core.timeblock.application.TimeReplaceValidator; 5 | import com.dnd.modutime.core.timeblock.application.request.TimeReplaceRequest; 6 | import com.dnd.modutime.core.timeblock.domain.TimeBlockRemovedEvent; 7 | import com.dnd.modutime.core.timeblock.domain.TimeBlockReplaceEvent; 8 | import com.dnd.modutime.core.timetable.application.command.TimeTableUpdateCommand; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.boot.test.mock.mockito.SpyBean; 15 | import org.springframework.test.context.event.ApplicationEvents; 16 | import org.springframework.test.context.event.RecordApplicationEvents; 17 | 18 | import java.time.LocalDateTime; 19 | import java.util.List; 20 | 21 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 22 | import static com.dnd.modutime.fixture.TimeFixture.*; 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.junit.jupiter.api.Assertions.assertAll; 25 | import static org.mockito.ArgumentMatchers.any; 26 | import static org.mockito.Mockito.doNothing; 27 | import static org.mockito.Mockito.verify; 28 | 29 | @SpringBootTest 30 | @RecordApplicationEvents 31 | class TimeTableEventHandlerTest { 32 | 33 | @Autowired 34 | private ApplicationEvents events; 35 | 36 | @Autowired 37 | private TimeBlockService timeBlockService; 38 | 39 | @SpyBean 40 | private TimeTableService timeTableService; 41 | 42 | @MockBean 43 | private TimeReplaceValidator timeReplaceValidator; 44 | 45 | @DisplayName("타임블록이 삭제되면 타임테이블이 변경이 호출된다.") 46 | @Test 47 | void test01() { 48 | var participantName = "참여자1"; 49 | timeBlockService.create(ROOM_UUID, participantName); 50 | timeBlockService.remove(ROOM_UUID, participantName); 51 | 52 | // then 53 | assertAll( 54 | () -> assertThat(events.stream(TimeBlockRemovedEvent.class).count()).isEqualTo(1), 55 | () -> verify(timeTableService).update(any(TimeTableUpdateCommand.class)) 56 | ); 57 | } 58 | 59 | @DisplayName("타임블록이 변경되면 타임테이블이 변경이 호출된다.") 60 | @Test 61 | void test02() { 62 | doNothing().when(timeReplaceValidator).validate(any(), any()); 63 | var participantName = "참여자1"; 64 | timeBlockService.create(ROOM_UUID, participantName); 65 | var request = new TimeReplaceRequest(participantName, true, List.of(LocalDateTime.of(_2023_02_10, _12_00), LocalDateTime.of(_2023_02_10, _13_00))); 66 | timeBlockService.replace(ROOM_UUID, request); 67 | 68 | // then 69 | assertAll( 70 | () -> assertThat(events.stream(TimeBlockReplaceEvent.class).count()).isEqualTo(1), 71 | () -> verify(timeTableService).update(any(TimeTableUpdateCommand.class)) 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/timeblock/domain/AvailableDateTime.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.domain; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.util.List; 6 | import javax.persistence.CascadeType; 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.EntityListeners; 10 | import javax.persistence.FetchType; 11 | import javax.persistence.ForeignKey; 12 | import javax.persistence.GeneratedValue; 13 | import javax.persistence.Id; 14 | import javax.persistence.JoinColumn; 15 | import javax.persistence.ManyToOne; 16 | import javax.persistence.OneToMany; 17 | import lombok.AccessLevel; 18 | import lombok.NoArgsConstructor; 19 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 20 | import com.dnd.modutime.core.entity.Auditable; 21 | 22 | @Entity 23 | @EntityListeners(AuditingEntityListener.class) 24 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 25 | public class AvailableDateTime implements Auditable { 26 | 27 | @Id 28 | @GeneratedValue 29 | private Long id; 30 | 31 | @ManyToOne(fetch = FetchType.LAZY) 32 | @JoinColumn(name = "time_block_id") 33 | private TimeBlock timeBlock; 34 | 35 | @Column(nullable = false) 36 | private LocalDate date; 37 | 38 | @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.EAGER) 39 | @JoinColumn( 40 | name = "available_date_time_id", nullable = false, updatable = false, 41 | foreignKey = @ForeignKey(name = "fk_at_adt_id_ref_adt_id") 42 | ) 43 | private List times; 44 | 45 | private String createdBy; 46 | private LocalDateTime createdAt; 47 | private String modifiedBy; 48 | private LocalDateTime modifiedAt; 49 | 50 | public AvailableDateTime(TimeBlock timeBlock, 51 | LocalDate date, 52 | List times) { 53 | validateDate(date); 54 | this.timeBlock = timeBlock; 55 | this.date = date; 56 | this.times = times; 57 | } 58 | 59 | private void validateDate(LocalDate date) { 60 | if (date == null) { 61 | throw new IllegalArgumentException("date는 null일 수 없습니다."); 62 | } 63 | } 64 | 65 | public boolean hasTime() { 66 | return times != null; 67 | } 68 | 69 | public Long getId() { 70 | return id; 71 | } 72 | 73 | public LocalDate getDate() { 74 | return date; 75 | } 76 | 77 | public List getTimesOrNull() { 78 | return times; 79 | } 80 | 81 | @Override 82 | public void setCreatedBy(String createdBy) { 83 | this.createdBy = createdBy; 84 | } 85 | 86 | @Override 87 | public void setCreatedAt(LocalDateTime createdAt) { 88 | this.createdAt = createdAt; 89 | } 90 | 91 | @Override 92 | public void setModifiedBy(String modifiedBy) { 93 | this.modifiedBy = modifiedBy; 94 | } 95 | 96 | @Override 97 | public void setModifiedAt(LocalDateTime modifiedAt) { 98 | this.modifiedAt = modifiedAt; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/acceptance/RoomAcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.acceptance; 2 | 3 | import static com.dnd.modutime.fixture.TimeFixture.*; 4 | import static org.assertj.core.api.Assertions.*; 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | import java.util.List; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import com.dnd.modutime.acceptance.request.RoomRequestWithNoNull; 12 | import com.dnd.modutime.core.room.application.response.RoomCreationResponse; 13 | import com.dnd.modutime.core.room.application.response.RoomInfoResponse; 14 | 15 | import io.restassured.response.ExtractableResponse; 16 | import io.restassured.response.Response; 17 | 18 | public class RoomAcceptanceTest extends AcceptanceSupporter{ 19 | 20 | @Test 21 | void 시작시간이_끝시간보다_작은_방을_생성한다() { 22 | RoomCreationResponse roomCreationResponse = 방_생성(); 23 | assertThat(roomCreationResponse.getUuid()).isNotNull(); 24 | } 25 | 26 | @Test 27 | void 시작시간이_끝시간보다_큰_방을_생성한다() { 28 | RoomCreationResponse roomCreationResponse = 시작시간이_끝시간보다_큰_방_생성(); 29 | assertThat(roomCreationResponse.getUuid()).isNotNull(); 30 | } 31 | 32 | @Test 33 | void 방_정보를_응답한다() { 34 | RoomCreationResponse roomCreationResponse = 방_생성(); 35 | ExtractableResponse response = get("/api/room/" + roomCreationResponse.getUuid()); 36 | RoomInfoResponse roomInfoResponse = response.body().as(RoomInfoResponse.class); 37 | assertAll( 38 | () -> assertThat(roomInfoResponse.getTitle()).isEqualTo("이멤버리멤버"), 39 | () -> assertThat(roomInfoResponse.getDeadLine()).isNotNull(), 40 | () -> assertThat(roomInfoResponse.getHeadCount()).isEqualTo(10), 41 | () -> assertThat(roomInfoResponse.getDates()) 42 | .hasSize(1) 43 | .contains(_2023_02_10), 44 | () -> assertThat(roomInfoResponse.getStartTime()).isEqualTo(_11_00), 45 | () -> assertThat(roomInfoResponse.getEndTime()).isEqualTo(_14_00) 46 | ); 47 | } 48 | 49 | @Test 50 | void 방_정보를_응답한다_없는_데이터는_null로_응답한다() { 51 | RoomCreationResponse roomCreationResponse = getRoomCreationResponse(); 52 | 53 | ExtractableResponse response = get("/api/room/" + roomCreationResponse.getUuid()); 54 | RoomInfoResponse roomInfoResponse = response.body().as(RoomInfoResponse.class); 55 | assertAll( 56 | () -> assertThat(roomInfoResponse.getTitle()).isEqualTo("이멤버리멤버"), 57 | () -> assertThat(roomInfoResponse.getDeadLine()).isNull(), 58 | () -> assertThat(roomInfoResponse.getHeadCount()).isNull(), 59 | () -> assertThat(roomInfoResponse.getDates()) 60 | .hasSize(1) 61 | .contains(_2023_02_10), 62 | () -> assertThat(roomInfoResponse.getStartTime()).isNull(), 63 | () -> assertThat(roomInfoResponse.getEndTime()).isNull() 64 | ); 65 | } 66 | 67 | private RoomCreationResponse getRoomCreationResponse() { 68 | ExtractableResponse response = post("/api/room", new RoomRequestWithNoNull( 69 | "이멤버리멤버", 70 | List.of(_2023_02_10))); 71 | return response.body().as(RoomCreationResponse.class); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/dnd/modutime/core/room/application/RoomTimeTableInitializer.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.room.application; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalTime; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.dnd.modutime.core.room.domain.Room; 12 | import com.dnd.modutime.core.room.domain.RoomDate; 13 | import com.dnd.modutime.core.room.repository.RoomRepository; 14 | import com.dnd.modutime.core.timetable.application.TimeTableInitializer; 15 | import com.dnd.modutime.core.timetable.domain.DateInfo; 16 | import com.dnd.modutime.core.timetable.domain.TimeInfo; 17 | import com.dnd.modutime.core.timetable.domain.TimeTable; 18 | import com.dnd.modutime.exception.NotFoundException; 19 | 20 | import lombok.RequiredArgsConstructor; 21 | 22 | @Component 23 | @RequiredArgsConstructor 24 | public class RoomTimeTableInitializer implements TimeTableInitializer { 25 | 26 | private static final int INITIAL_TIME_INFOS_CAPACITY = 50; 27 | private static final LocalTime ZERO_TIME = LocalTime.of(0, 0); 28 | 29 | private final RoomRepository roomRepository; 30 | 31 | @Override 32 | public void initialize(String roomUuid, TimeTable timeTable) { 33 | Room room = getRoomByRoomUuid(roomUuid); 34 | List dates = room.getRoomDates().stream() 35 | .map(RoomDate::getDate) 36 | .collect(Collectors.toList()); 37 | List dateInfos = new ArrayList<>(dates.size()); 38 | 39 | for (LocalDate date : dates) { 40 | List timeInfos = new ArrayList<>(INITIAL_TIME_INFOS_CAPACITY); 41 | addTimeInfos(room, timeInfos); 42 | DateInfo dateInfo = new DateInfo(timeTable, date, timeInfos); 43 | dateInfos.add(dateInfo); 44 | } 45 | timeTable.replaceDateInfos(dateInfos); 46 | } 47 | 48 | private void addTimeInfos(Room room, 49 | List timeInfos) { 50 | LocalTime startTime = room.getStartTimeOrNull(); 51 | LocalTime endTime = room.getEndTimeOrNull(); 52 | if (!room.hasStartAndEndTime()) { 53 | timeInfos.add(new TimeInfo(null, new ArrayList<>())); 54 | return; 55 | } 56 | 57 | if (startTime.isAfter(endTime)) { 58 | addTimeInfosWhenStartTimeIsAfterEndTime(timeInfos, startTime, endTime); 59 | return; 60 | } 61 | for (LocalTime time = startTime; time.isBefore(endTime); time = time.plusMinutes(30)) { 62 | timeInfos.add(new TimeInfo(time, new ArrayList<>())); 63 | } 64 | } 65 | 66 | private static void addTimeInfosWhenStartTimeIsAfterEndTime(List timeInfos, LocalTime startTime, LocalTime endTime) { 67 | 68 | for (LocalTime time = ZERO_TIME; time.isBefore(endTime); time = time.plusMinutes(30)) { 69 | timeInfos.add(new TimeInfo(time, new ArrayList<>())); 70 | } 71 | 72 | for (LocalTime time = startTime; !time.equals(ZERO_TIME); time = time.plusMinutes(30)) { 73 | timeInfos.add(new TimeInfo(time, new ArrayList<>())); 74 | } 75 | } 76 | 77 | private Room getRoomByRoomUuid(String roomUuid) { 78 | return roomRepository.findByUuid(roomUuid) 79 | .orElseThrow(() -> new NotFoundException("해당하는 방이 없습니다.")); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/dnd/modutime/core/timeblock/application/TimeBlockEventHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.dnd.modutime.core.timeblock.application; 2 | 3 | import com.dnd.modutime.core.participant.application.ParticipantCommandHandler; 4 | import com.dnd.modutime.core.participant.application.ParticipantFacade; 5 | import com.dnd.modutime.core.participant.application.command.ParticipantCreateCommand; 6 | import com.dnd.modutime.core.participant.application.command.ParticipantsDeleteCommand; 7 | import com.dnd.modutime.core.participant.domain.ParticipantRemovedEvent; 8 | import com.dnd.modutime.core.timeblock.domain.TimeBlock; 9 | import com.dnd.modutime.core.timeblock.domain.TimeBlockRemovedEvent; 10 | import com.dnd.modutime.core.timeblock.repository.TimeBlockRepository; 11 | import org.junit.jupiter.api.DisplayName; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.boot.test.mock.mockito.SpyBean; 16 | import org.springframework.test.context.event.ApplicationEvents; 17 | import org.springframework.test.context.event.RecordApplicationEvents; 18 | 19 | import java.util.List; 20 | import java.util.Optional; 21 | 22 | import static com.dnd.modutime.fixture.RoomRequestFixture.ROOM_UUID; 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.junit.jupiter.api.Assertions.assertAll; 25 | import static org.mockito.Mockito.verify; 26 | 27 | @SpringBootTest 28 | @RecordApplicationEvents 29 | class TimeBlockEventHandlerTest { 30 | 31 | @Autowired 32 | private ParticipantCommandHandler participantCommandHandler; 33 | 34 | @Autowired 35 | private ParticipantFacade participantFacade; 36 | 37 | @Autowired 38 | private TimeBlockRepository timeBlockRepository; 39 | 40 | @SpyBean 41 | private TimeBlockService timeBlockService; 42 | 43 | @Autowired 44 | private ApplicationEvents events; 45 | 46 | @DisplayName("참여자가 생성되면 참여자의 타임블록 생성이 호출된다.") 47 | @Test 48 | void 참여자가_생성되면_참여자의_TimeBlock이_생성된다() { 49 | var participantName = "참여자1"; 50 | var command = ParticipantCreateCommand.of(ROOM_UUID, participantName, "1234"); 51 | participantCommandHandler.handle(command); 52 | Optional actual = timeBlockRepository.findByRoomUuidAndParticipantName(ROOM_UUID, participantName); 53 | assertAll( 54 | () -> assertThat(actual.isPresent()).isTrue(), 55 | () -> assertThat(events.stream(ParticipantCreationEvent.class).count()).isEqualTo(1), 56 | () -> verify(timeBlockService).create(ROOM_UUID, participantName) 57 | ); 58 | } 59 | 60 | @DisplayName("참여자가 삭제되면 타임블록도 삭제가 호출된다.") 61 | @Test 62 | void test01() { 63 | var participantName = "참여자1"; 64 | var participant = participantCommandHandler.handle(ParticipantCreateCommand.of(ROOM_UUID, participantName, "1234")); 65 | 66 | // when 67 | var command = ParticipantsDeleteCommand.of(ROOM_UUID, List.of(participant.getId())); 68 | participantFacade.delete(command); 69 | 70 | // then 71 | assertAll( 72 | () -> assertThat(events.stream(ParticipantRemovedEvent.class).count()).isEqualTo(1), 73 | () -> assertThat(events.stream(TimeBlockRemovedEvent.class).count()).isEqualTo(1), 74 | () -> verify(timeBlockService).remove(ROOM_UUID, participantName) 75 | ); 76 | } 77 | } 78 | --------------------------------------------------------------------------------