diff --git a/src/main/java/com/livable/server/entity/ParkingLog.java b/src/main/java/com/livable/server/entity/ParkingLog.java index 044ea298..4bf5b9a7 100644 --- a/src/main/java/com/livable/server/entity/ParkingLog.java +++ b/src/main/java/com/livable/server/entity/ParkingLog.java @@ -17,7 +17,7 @@ public class ParkingLog extends BaseTimeEntity { private Long id; @JoinColumn(nullable = false, unique = true) - @ManyToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY) private Visitor visitor; @Column(nullable = false) diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java b/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java new file mode 100644 index 00000000..6f8df1ec --- /dev/null +++ b/src/main/java/com/livable/server/invitation/domain/InvitationValidationGuideMessage.java @@ -0,0 +1,6 @@ +package com.livable.server.invitation.domain; + +public interface InvitationValidationGuideMessage { + + String INVALID_CAR_NUMBER = "차량 번호를 올바르게 입력해 주세요."; +} diff --git a/src/main/java/com/livable/server/visitation/controller/VisitationController.java b/src/main/java/com/livable/server/visitation/controller/VisitationController.java index 33f61f8d..c413446e 100644 --- a/src/main/java/com/livable/server/visitation/controller/VisitationController.java +++ b/src/main/java/com/livable/server/visitation/controller/VisitationController.java @@ -9,6 +9,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; + @RequiredArgsConstructor @RestController @RequestMapping("/api/visitation") @@ -32,4 +34,15 @@ public ResponseEntity> validateQrCode(@RequestBody V return ApiResponse.success(HttpStatus.OK); } + + @PostMapping("/parking") + public ResponseEntity> registerParking( + @RequestBody @Valid VisitationRequest.RegisterParkingDto registerParkingDto + ) { + Long visitorId = 1L; + visitationFacadeService.registerParking(visitorId, registerParkingDto.getCarNumber()); + + return ApiResponse.success(HttpStatus.CREATED); + } + } diff --git a/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java b/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java index 43388cfb..a66bf63e 100644 --- a/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java +++ b/src/main/java/com/livable/server/visitation/domain/VisitationErrorCode.java @@ -15,7 +15,8 @@ public enum VisitationErrorCode implements ErrorCode { IO(HttpStatus.INTERNAL_SERVER_ERROR, "I/O를 진행하는 과정에서 알 수 없는 에러가 발생했습니다."), INVALID_QR_PERIOD(HttpStatus.BAD_REQUEST, "QR을 생성할 수 없는 시간입니다."), INVALID_PERIOD(HttpStatus.BAD_REQUEST, "시작 및 종료시간이 올바르지 않습니다."), - NOT_FOUND(HttpStatus.BAD_REQUEST, "존재하지 않는 정보입니다."); + NOT_FOUND(HttpStatus.BAD_REQUEST, "존재하지 않는 정보입니다."), + ALREADY_REGISTER_PARKING(HttpStatus.BAD_REQUEST, "이미 주차 등록을 완료하였습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java index aa6c5c01..6ea73d65 100644 --- a/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java +++ b/src/main/java/com/livable/server/visitation/dto/VisitationRequest.java @@ -1,9 +1,12 @@ package com.livable.server.visitation.dto; +import com.livable.server.invitation.domain.InvitationValidationGuideMessage; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import javax.validation.constraints.Pattern; + @NoArgsConstructor(access = AccessLevel.PRIVATE) public class VisitationRequest { @@ -11,4 +14,10 @@ public class VisitationRequest { public static class ValidateQrDto { private String qr; } + + @Getter + public static class RegisterParkingDto { + @Pattern(regexp = "^\\d{2,3}[가-힣]{1}\\d{4}$", message = InvitationValidationGuideMessage.INVALID_CAR_NUMBER) + private String carNumber; + } } diff --git a/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java new file mode 100644 index 00000000..019495dd --- /dev/null +++ b/src/main/java/com/livable/server/visitation/repository/ParkingLogRepository.java @@ -0,0 +1,15 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.entity.ParkingLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface ParkingLogRepository extends JpaRepository { + + @Query("select p from ParkingLog p" + + " where p.visitor.id = :visitorId") + Optional findParkingLogByVisitorId(@Param("visitorId") Long visitorId); +} diff --git a/src/main/java/com/livable/server/visitation/service/ParkingLogService.java b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java new file mode 100644 index 00000000..9382a9df --- /dev/null +++ b/src/main/java/com/livable/server/visitation/service/ParkingLogService.java @@ -0,0 +1,29 @@ +package com.livable.server.visitation.service; + +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; +import com.livable.server.visitation.repository.ParkingLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class ParkingLogService { + + private final ParkingLogRepository parkingLogRepository; + + public Optional findParkingLogByVisitorId(Long visitorId) { + return parkingLogRepository.findParkingLogByVisitorId(visitorId); + } + + public void registerParkingLog(Visitor visitor, String carNumber) { + ParkingLog parkingLog = ParkingLog.builder() + .carNumber(carNumber) + .visitor(visitor) + .build(); + + parkingLogRepository.save(parkingLog); + } +} diff --git a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java index 055aa7bd..1e0d79f4 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitationFacadeService.java @@ -1,11 +1,17 @@ package com.livable.server.visitation.service; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; import com.livable.server.invitation.service.InvitationService; +import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @RequiredArgsConstructor @Service @Transactional(readOnly = true) @@ -13,6 +19,8 @@ public class VisitationFacadeService { private final VisitationService visitationService; private final InvitationService invitationService; + private final VisitorService visitorService; + private final ParkingLogService parkingLogService; public String createQrCode(Long visitorId) { VisitationResponse.InvitationTimeDto invitationTime = invitationService.findInvitationTime(visitorId); @@ -23,4 +31,17 @@ public String createQrCode(Long visitorId) { public void validateQrCode(String qr) { visitationService.validateQrCode(qr); } + + @Transactional + public void registerParking(Long visitorId, String carNumber) { + validateDuplicationRegister(visitorId); + Visitor visitor = visitorService.findById(visitorId); + parkingLogService.registerParkingLog(visitor, carNumber); + } + + private void validateDuplicationRegister(Long visitorId) { + if (parkingLogService.findParkingLogByVisitorId(visitorId).isPresent()) { + throw new GlobalRuntimeException(VisitationErrorCode.ALREADY_REGISTER_PARKING); + } + } } diff --git a/src/main/java/com/livable/server/visitation/service/VisitorService.java b/src/main/java/com/livable/server/visitation/service/VisitorService.java index 0084daa5..da55c4c3 100644 --- a/src/main/java/com/livable/server/visitation/service/VisitorService.java +++ b/src/main/java/com/livable/server/visitation/service/VisitorService.java @@ -1,6 +1,7 @@ package com.livable.server.visitation.service; import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.Visitor; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.repository.VisitorRepository; import lombok.RequiredArgsConstructor; @@ -18,4 +19,9 @@ public Long findInvitationId(Long visitorId) { .getInvitation() .getId(); } + + public Visitor findById(Long visitorId) { + return visitorRepository.findById(visitorId) + .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND)); + } } diff --git a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java index 932044ce..237a48ce 100644 --- a/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java +++ b/src/test/java/com/livable/server/visitation/controller/VisitationControllerTest.java @@ -5,6 +5,7 @@ import com.livable.server.core.exception.GlobalRuntimeException; import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationRequest; +import com.livable.server.visitation.mock.MockRegisterParkingDto; import com.livable.server.visitation.mock.ValidateQrCodeSuccessMockRequest; import com.livable.server.visitation.service.VisitationFacadeService; import org.junit.jupiter.api.DisplayName; @@ -103,4 +104,30 @@ void validateQrCodeFail() throws Exception { then(visitationFacadeService).should(times(1)).validateQrCode(anyString()); } + + @DisplayName("[POST][/api/visitation/parking] - 차량 등록 성공") + @Test + void registerParking() throws Exception { + // given + String carNumber = "12가1234"; + MockRegisterParkingDto mockRegisterParkingDto = new MockRegisterParkingDto(carNumber); + Long visitorId = 1L; + + + willDoNothing().given(visitationFacadeService).registerParking(visitorId, mockRegisterParkingDto.getCarNumber()); + + // when + ResultActions resultActions = mockMvc.perform( + post("/api/visitation/parking") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(mockRegisterParkingDto)) + ); + + // then + resultActions.andExpect(status().isCreated()); + + then(visitationFacadeService) + .should(times(1)) + .registerParking(visitorId, mockRegisterParkingDto.getCarNumber()); + } } \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java b/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java new file mode 100644 index 00000000..fb8fd399 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/dto/VisitationRequestTest.java @@ -0,0 +1,51 @@ +package com.livable.server.visitation.dto; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +import java.util.Set; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class VisitationRequestTest { + + private static ValidatorFactory factory; + private static Validator validator; + + private static ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeAll + public static void init() { + factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @DisplayName("VisitationRequest.RegisterParkingDto carNumber 정규식 성공 테스트") + @CsvSource({"12가1234", "12흫3456", "123핡0000"}) + @ParameterizedTest(name = "[{index}] 차량번호: {0}") + void carNumberPatternSuccessTest(String carNumber) throws JsonProcessingException { + String requestDto = "{\n" + + " \"carNumber\": \"" + carNumber + "\"\n" + + " }"; + + VisitationRequest.RegisterParkingDto registerParkingDto = + objectMapper.readValue(requestDto, VisitationRequest.RegisterParkingDto.class); + + Set> validate = validator.validate(registerParkingDto); + + assertThat(validate.size()).isEqualTo(0); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java b/src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java new file mode 100644 index 00000000..fb70b815 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/mock/MockRegisterParkingDto.java @@ -0,0 +1,17 @@ +package com.livable.server.visitation.mock; + +import com.livable.server.visitation.dto.VisitationRequest; + +public class MockRegisterParkingDto extends VisitationRequest.RegisterParkingDto { + + private final String carNumber; + + public MockRegisterParkingDto(String carNumber) { + this.carNumber = carNumber; + } + + @Override + public String getCarNumber() { + return carNumber; + } +} diff --git a/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java new file mode 100644 index 00000000..3be1a726 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/repository/ParkingLogRepositoryTest.java @@ -0,0 +1,118 @@ +package com.livable.server.visitation.repository; + +import com.livable.server.core.config.QueryDslConfig; +import com.livable.server.entity.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import javax.persistence.EntityManager; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@Import(QueryDslConfig.class) +class ParkingLogRepositoryTest { + + public static final LocalDate START_DATE = LocalDate.now(); + public static final LocalTime START_TIME = LocalTime.of(1, 10); + public static final LocalTime END_TIME = LocalTime.of(1, 20); + public static final LocalDate END_DATE = LocalDate.now(); + + @Autowired + ParkingLogRepository parkingLogRepository; + + @Autowired + VisitorRepository visitorRepository; + + @Autowired + EntityManager entityManager; + + @BeforeEach + void dataInit() { + Building building = Building.builder() + .name("63빌딩") + .scale("지하 3층, 지상 63층") + .representativeImageUrl("./thumbnailImage.jpg") + .endTime(LocalTime.of(10, 30)) + .startTime(LocalTime.of(18, 30)) + .parkingCostInformation("10분당 1000원") + .longitude("10.10.10.10") + .latitude("123.123.123") + .hasCafeteria(false) + .address("서울시 강남구 서초대로 61길 7, 392") + .subwayStation("석촌역") + .build(); + + entityManager.persist(building); + + Company company = Company.builder() + .name("패스트캠퍼스") + .building(building) + .build(); + + entityManager.persist(company); + + Member member = Member.builder() + .company(company) + .contact("01012345678") + .name("김훈섭") + .email("test@naver.com") + .employeeNumber("9q0mavfdmmpoaskp") + .profileImageUrl("./profileImageUrl") + .password("1234") + .role(Role.USER) + .build(); + + entityManager.persist(member); + + Invitation invitation = Invitation.builder() + .member(member) + .endDate(END_DATE) + .startDate(START_DATE) + .startTime(START_TIME) + .endTime(END_TIME) + .description("알아서 와") + .purpose("INTERVIEW") + .officeName("패스트캠퍼스 사무실") + .build(); + + entityManager.persist(invitation); + + Visitor visitor = Visitor.builder() + .invitation(invitation) + .name("최태윤") + .contact("01034567811") + .build(); + + entityManager.persist(visitor); + + ParkingLog parkingLog = ParkingLog.builder() + .visitor(visitor) + .carNumber("12가1234") + .build(); + + entityManager.persist(parkingLog); + } + + @DisplayName("ParkingLogRepository.findParkingLogByVisitorId 쿼리 확인용 테스트") + @Test + void test() { + ParkingLog parkingLog = parkingLogRepository.findParkingLogByVisitorId(1L).get(); + Visitor visitor = visitorRepository.findById(1L).get(); + + assertAll( + () -> assertThat(parkingLog.getId()).isEqualTo(1L), + () -> assertThat(parkingLog.getVisitor()).isEqualTo(visitor), + () -> assertThat(parkingLog.getCarNumber()).isEqualTo("12가1234") + ); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java new file mode 100644 index 00000000..9d573cf0 --- /dev/null +++ b/src/test/java/com/livable/server/visitation/service/ParkingLogServiceTest.java @@ -0,0 +1,69 @@ +package com.livable.server.visitation.service; + +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; +import com.livable.server.visitation.repository.ParkingLogRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class ParkingLogServiceTest { + + @InjectMocks + ParkingLogService parkingLogService; + + @Mock + ParkingLogRepository parkingLogRepository; + + @DisplayName("ParkingLogService.findParkingLogByVisitorId 성공 테스트") + @Test + void findParkingLogByVisitorIdSuccessTest() { + + // Given + ParkingLog parkingLog = ParkingLog.builder() + .build(); + + Optional expectedOptionalParkingLog = Optional.of(parkingLog); + + given(parkingLogRepository.findParkingLogByVisitorId(anyLong())).willReturn(Optional.of(parkingLog)); + + // When + Optional optionalParkingLog = parkingLogService.findParkingLogByVisitorId(1L); + + // Then + assertThat(optionalParkingLog).isEqualTo(expectedOptionalParkingLog); + then(parkingLogRepository).should(times(1)).findParkingLogByVisitorId(anyLong()); + } + + @DisplayName("ParkingLogService.registerParkingLog 성공 테스트") + @Test + void registerParkingLogSuccessTest() { + ParkingLog parkingLog = ParkingLog.builder() + .build(); + + Visitor visitor = Visitor.builder() + .build(); + + String carNumber = "testCarNumber"; + + // Given + given(parkingLogRepository.save(any())).willReturn(parkingLog); + + // When + parkingLogService.registerParkingLog(visitor, carNumber); + + // Then + then(parkingLogRepository).should(times(1)).save(any()); + } +} \ No newline at end of file diff --git a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java index 41c7e688..f41fed85 100644 --- a/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java +++ b/src/test/java/com/livable/server/visitation/service/VisitationFacadeServiceTest.java @@ -1,6 +1,11 @@ package com.livable.server.visitation.service; +import com.livable.server.core.exception.GlobalRuntimeException; +import com.livable.server.entity.ParkingLog; +import com.livable.server.entity.Visitor; +import com.livable.server.invitation.domain.InvitationErrorCode; import com.livable.server.invitation.service.InvitationService; +import com.livable.server.visitation.domain.VisitationErrorCode; import com.livable.server.visitation.dto.VisitationResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,6 +17,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.Optional; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -30,11 +36,16 @@ class VisitationFacadeServiceTest { @Mock VisitationService visitationService; + @Mock InvitationService invitationService; + @Mock VisitorService visitorService; + @Mock + ParkingLogService parkingLogService; + @DisplayName("VisitationFacadeService.createQrCode 성공 테스트") @Test void createQrCodeSuccessTest() { @@ -71,4 +82,43 @@ void validateQrCodeSuccessTest() { // Then then(visitationService).should(times(1)).validateQrCode(anyString()); } + + @DisplayName("VisitationFacadeService.registerParking 성공 테스트") + @Test + void registerParkingSuccessTest() { + // Given + Visitor visitor = Visitor.builder() + .id(1L) + .build(); + given(visitorService.findById(anyLong())).willReturn(visitor); + given(parkingLogService.findParkingLogByVisitorId(any())).willReturn(Optional.empty()); + willDoNothing().given(parkingLogService).registerParkingLog(any(), anyString()); + + // When + visitationFacadeService.registerParking(visitor.getId(), "12가1234"); + + // Then + then(visitorService).should(times(1)).findById(anyLong()); + then(parkingLogService).should(times(1)).findParkingLogByVisitorId(any()); + then(parkingLogService).should(times(1)).registerParkingLog(any(), anyString()); + } + + @DisplayName("VisitationFacadeService.registerParking 실패 테스트") + @Test + void registerParkingFailTest() { + // Given + ParkingLog parkingLog = ParkingLog.builder() + .build(); + given(parkingLogService.findParkingLogByVisitorId(any())).willReturn(Optional.of(parkingLog)); + + // When + GlobalRuntimeException globalRuntimeException = assertThrows( + GlobalRuntimeException.class, + () -> visitationFacadeService.registerParking(any(), "12가1234") + ); + + // Then + then(parkingLogService).should(times(1)).findParkingLogByVisitorId(any()); + assertThat(globalRuntimeException.getErrorCode()).isEqualTo(VisitationErrorCode.ALREADY_REGISTER_PARKING); + } } \ No newline at end of file