diff --git a/.github/workflows/ci-dev.yml b/.github/workflows/ci-dev.yml index 345d7f2f4..e7c55af50 100644 --- a/.github/workflows/ci-dev.yml +++ b/.github/workflows/ci-dev.yml @@ -28,7 +28,7 @@ jobs: run: chmod +x ./gradlew - name: Gradle 빌드 - run: ./gradlew clean build -x test + run: ./gradlew clean build - name: DockerHub 로그인 uses: docker/login-action@v1 diff --git a/backend/src/main/java/mouda/backend/common/RestResponse.java b/backend/src/main/java/mouda/backend/common/RestResponse.java index 2b646d0af..ccc36be6f 100644 --- a/backend/src/main/java/mouda/backend/common/RestResponse.java +++ b/backend/src/main/java/mouda/backend/common/RestResponse.java @@ -6,5 +6,6 @@ @Getter @AllArgsConstructor public class RestResponse { + private T data; } diff --git a/backend/src/main/java/mouda/backend/config/CorsConfig.java b/backend/src/main/java/mouda/backend/config/CorsConfig.java index 887ac79f7..f7906baa8 100644 --- a/backend/src/main/java/mouda/backend/config/CorsConfig.java +++ b/backend/src/main/java/mouda/backend/config/CorsConfig.java @@ -4,13 +4,17 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + @Configuration public class CorsConfig implements WebMvcConfigurer { + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("*") - .allowedMethods("*") - .allowedHeaders("*"); + .allowedOrigins("http://localhost:8080") + .allowedMethods("GET", "POST", "DELETE", "OPTIONS") + .allowedHeaders("Authorization", "Content-Type") + .allowCredentials(true) + .maxAge(3600); } } diff --git a/backend/src/main/java/mouda/backend/config/JacksonConfig.java b/backend/src/main/java/mouda/backend/config/JacksonConfig.java new file mode 100644 index 000000000..d9a4fc542 --- /dev/null +++ b/backend/src/main/java/mouda/backend/config/JacksonConfig.java @@ -0,0 +1,32 @@ +package mouda.backend.config; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +@Configuration +public class JacksonConfig { + + @Bean + public JavaTimeModule javaTimeModule() { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + DateTimeFormatter dateFormat = DateTimeFormatter.ISO_LOCAL_DATE; + DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm"); + + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormat)) + .addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormat)) + .addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormat)) + .addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormat)); + + return javaTimeModule; + } +} diff --git a/backend/src/main/java/mouda/backend/exception/ErrorResponse.java b/backend/src/main/java/mouda/backend/exception/ErrorResponse.java new file mode 100644 index 000000000..d56d951dd --- /dev/null +++ b/backend/src/main/java/mouda/backend/exception/ErrorResponse.java @@ -0,0 +1,6 @@ +package mouda.backend.exception; + +public record ErrorResponse( + String message +) { +} diff --git a/backend/src/main/java/mouda/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/mouda/backend/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..35dff91a0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/exception/GlobalExceptionHandler.java @@ -0,0 +1,21 @@ +package mouda.backend.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(MoudaException.class) + public ResponseEntity handleMoudaException(MoudaException exception) { + return ResponseEntity.status(exception.getHttpStatus()).body(new ErrorResponse(exception.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException() { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new ErrorResponse("서버 오류가 발생했습니다.")); + } +} diff --git a/backend/src/main/java/mouda/backend/exception/MoudaException.java b/backend/src/main/java/mouda/backend/exception/MoudaException.java new file mode 100644 index 000000000..0819250f7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/exception/MoudaException.java @@ -0,0 +1,14 @@ +package mouda.backend.exception; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MoudaException extends RuntimeException { + + private HttpStatus httpStatus; + private String message; +} diff --git a/backend/src/main/java/mouda/backend/member/domain/Member.java b/backend/src/main/java/mouda/backend/member/domain/Member.java new file mode 100644 index 000000000..ad183d9f9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/domain/Member.java @@ -0,0 +1,36 @@ +package mouda.backend.member.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.moim.domain.Moim; + +@Entity +@Getter +@NoArgsConstructor +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nickname; + + @ManyToOne(fetch = FetchType.LAZY) + private Moim moim; + + @Builder + public Member(String nickname) { + this.nickname = nickname; + } + + public void joinMoim(Moim moim) { + this.moim = moim; + } +} diff --git a/backend/src/main/java/mouda/backend/member/repository/MemberRepository.java b/backend/src/main/java/mouda/backend/member/repository/MemberRepository.java new file mode 100644 index 000000000..17762f592 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/repository/MemberRepository.java @@ -0,0 +1,12 @@ +package mouda.backend.member.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.member.domain.Member; + +public interface MemberRepository extends JpaRepository { + + List findAllByMoimId(long moimId); +} diff --git a/backend/src/main/java/mouda/backend/moim/controller/MoimController.java b/backend/src/main/java/mouda/backend/moim/controller/MoimController.java index 6ff48247f..c56a535c0 100644 --- a/backend/src/main/java/mouda/backend/moim/controller/MoimController.java +++ b/backend/src/main/java/mouda/backend/moim/controller/MoimController.java @@ -9,9 +9,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; import mouda.backend.common.RestResponse; import mouda.backend.moim.domain.Moim; @@ -24,54 +21,47 @@ @RestController @RequestMapping("/v1/moim") @RequiredArgsConstructor -public class MoimController { +public class MoimController implements MoimSwagger { private final MoimService moimService; - @Operation(summary = "모임 생성", description = "모임을 생성한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "모임 생성 성공!"), - }) + @Override @PostMapping public ResponseEntity> createMoim(@RequestBody MoimCreateRequest moimCreateRequest) { Moim moim = moimService.createMoim(moimCreateRequest); + return ResponseEntity.ok().body(new RestResponse<>(moim.getId())); } - @Operation(summary = "모임 전체 조회", description = "모든 모임을 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "모임 조회 성공!"), - }) + @Override @GetMapping public ResponseEntity> findAllMoim() { - return ResponseEntity.ok().body(new RestResponse<>(moimService.findAllMoim())); + MoimFindAllResponses moimFindAllResponses = moimService.findAllMoim(); + + return ResponseEntity.ok().body(new RestResponse<>(moimFindAllResponses)); } - @Operation(summary = "모임 상세 조회", description = "모임 상세 조회한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "모임 상세 조회 성공!"), - }) + @Override @GetMapping("/{moimId}") - public ResponseEntity> findMoimDetails(@PathVariable long moimId) { - return ResponseEntity.ok().body(new RestResponse<>(moimService.findMoimDetails(moimId))); + public ResponseEntity> findMoimDetails(@PathVariable Long moimId) { + MoimDetailsFindResponse moimDetailsFindResponse = moimService.findMoimDetails(moimId); + + return ResponseEntity.ok().body(new RestResponse<>(moimDetailsFindResponse)); } - @Operation(summary = "모임 참여", description = "모임에 참여한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "모임 참여 성공!") - }) + @Override @PostMapping("/join") - public ResponseEntity> joinMoim(@RequestBody MoimJoinRequest moimJoinRequest) { + public ResponseEntity joinMoim(@RequestBody MoimJoinRequest moimJoinRequest) { moimService.joinMoim(moimJoinRequest); + return ResponseEntity.ok().build(); } - @Operation(summary = "모임 삭제", description = "해당하는 id의 모임을 삭제한다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "모임 삭제 성공!"), - }) + @Override @DeleteMapping("/{moimId}") - public void deleteMoim(@PathVariable long moimId) { + public ResponseEntity deleteMoim(@PathVariable Long moimId) { moimService.deleteMoim(moimId); + + return ResponseEntity.ok().build(); } } diff --git a/backend/src/main/java/mouda/backend/moim/controller/MoimSwagger.java b/backend/src/main/java/mouda/backend/moim/controller/MoimSwagger.java new file mode 100644 index 000000000..90bf816ea --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/controller/MoimSwagger.java @@ -0,0 +1,47 @@ +package mouda.backend.moim.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.RestResponse; +import mouda.backend.moim.dto.request.MoimCreateRequest; +import mouda.backend.moim.dto.request.MoimJoinRequest; +import mouda.backend.moim.dto.response.MoimDetailsFindResponse; +import mouda.backend.moim.dto.response.MoimFindAllResponses; + +public interface MoimSwagger { + + @Operation(summary = "모임 생성", description = "모임을 생성한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 생성 성공!"), + }) + ResponseEntity> createMoim(@RequestBody MoimCreateRequest moimCreateRequest); + + @Operation(summary = "모임 전체 조회", description = "모든 모임을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 조회 성공!"), + }) + ResponseEntity> findAllMoim(); + + @Operation(summary = "모임 상세 조회", description = "모임 상세 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 상세 조회 성공!"), + }) + ResponseEntity> findMoimDetails(@PathVariable Long moimId); + + @Operation(summary = "모임 참여", description = "모임에 참여한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 참여 성공!") + }) + ResponseEntity joinMoim(@RequestBody MoimJoinRequest moimJoinRequest); + + @Operation(summary = "모임 삭제", description = "해당하는 id의 모임을 삭제한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 삭제 성공!"), + }) + ResponseEntity deleteMoim(@PathVariable Long moimId); +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/Moim.java b/backend/src/main/java/mouda/backend/moim/domain/Moim.java index e1ec28e0f..ee82804ec 100644 --- a/backend/src/main/java/mouda/backend/moim/domain/Moim.java +++ b/backend/src/main/java/mouda/backend/moim/domain/Moim.java @@ -3,21 +3,23 @@ import java.time.LocalDate; import java.time.LocalTime; +import org.springframework.http.HttpStatus; + import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; @Entity @Getter -@Builder @NoArgsConstructor -@AllArgsConstructor public class Moim { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,18 +32,34 @@ public class Moim { private String place; - private int currentPeople; - private int maxPeople; private String authorNickname; private String description; - public void join() { + @Builder + public Moim( + String title, + LocalDate date, + LocalTime time, + String place, + int maxPeople, + String authorNickname, + String description + ) { + this.title = title; + this.date = date; + this.time = time; + this.place = place; + this.maxPeople = maxPeople; + this.authorNickname = authorNickname; + this.description = description; + } + + public void validateAlreadyFullMoim(int currentPeople) { if (currentPeople + 1 > maxPeople) { - throw new IllegalArgumentException(); + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MAX_PEOPLE); } - currentPeople++; } } diff --git a/backend/src/main/java/mouda/backend/moim/dto/request/MoimCreateRequest.java b/backend/src/main/java/mouda/backend/moim/dto/request/MoimCreateRequest.java index 4465b84ee..b36992caa 100644 --- a/backend/src/main/java/mouda/backend/moim/dto/request/MoimCreateRequest.java +++ b/backend/src/main/java/mouda/backend/moim/dto/request/MoimCreateRequest.java @@ -10,10 +10,11 @@ public record MoimCreateRequest( LocalDate date, LocalTime time, String place, - int maxPeople, + Integer maxPeople, String authorNickname, String description ) { + public Moim toEntity() { return Moim.builder() .title(title) diff --git a/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java b/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java index cdaa6e296..54b80007b 100644 --- a/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java +++ b/backend/src/main/java/mouda/backend/moim/dto/request/MoimJoinRequest.java @@ -1,6 +1,7 @@ package mouda.backend.moim.dto.request; public record MoimJoinRequest( - Long moimId + Long moimId, + String nickname ) { } diff --git a/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java b/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java index e62ba6132..b70af876d 100644 --- a/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java +++ b/backend/src/main/java/mouda/backend/moim/dto/response/MoimDetailsFindResponse.java @@ -2,6 +2,7 @@ import java.time.LocalDate; import java.time.LocalTime; +import java.util.List; import lombok.Builder; import mouda.backend.moim.domain.Moim; @@ -15,18 +16,21 @@ public record MoimDetailsFindResponse( int currentPeople, int maxPeople, String authorNickname, - String description + String description, + List participants ) { - public static MoimDetailsFindResponse toResponse(Moim moim) { + + public static MoimDetailsFindResponse toResponse(Moim moim, List participants) { return MoimDetailsFindResponse.builder() .title(moim.getTitle()) .date(moim.getDate()) .time(moim.getTime()) .place(moim.getPlace()) - .currentPeople(moim.getCurrentPeople()) + .currentPeople(participants.size()) .maxPeople(moim.getMaxPeople()) .authorNickname(moim.getAuthorNickname()) .description(moim.getDescription()) + .participants(participants) .build(); } } diff --git a/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java b/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java index 04978b651..10236f5ee 100644 --- a/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java +++ b/backend/src/main/java/mouda/backend/moim/dto/response/MoimFindAllResponse.java @@ -8,7 +8,7 @@ @Builder public record MoimFindAllResponse( - long moimId, + Long moimId, String title, LocalDate date, LocalTime time, @@ -18,14 +18,15 @@ public record MoimFindAllResponse( String authorNickname, String description ) { - public static MoimFindAllResponse toResponse(Moim moim) { + + public static MoimFindAllResponse toResponse(Moim moim, int currentPeople) { return MoimFindAllResponse.builder() .moimId(moim.getId()) .title(moim.getTitle()) .date(moim.getDate()) .time(moim.getTime()) .place(moim.getPlace()) - .currentPeople(moim.getCurrentPeople()) + .currentPeople(currentPeople) .maxPeople(moim.getMaxPeople()) .authorNickname(moim.getAuthorNickname()) .description(moim.getDescription()) diff --git a/backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java b/backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java new file mode 100644 index 000000000..d62d84e87 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java @@ -0,0 +1,15 @@ +package mouda.backend.moim.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MoimErrorMessage { + + NOT_FOUND("모임이 존재하지 않습니다."), + MAX_PEOPLE("모임 최대 인원 수를 초과합니다."), + ; + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/MoimException.java b/backend/src/main/java/mouda/backend/moim/exception/MoimException.java new file mode 100644 index 000000000..030fdd04b --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/MoimException.java @@ -0,0 +1,12 @@ +package mouda.backend.moim.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.exception.MoudaException; + +public class MoimException extends MoudaException { + + public MoimException(HttpStatus httpStatus, MoimErrorMessage moimErrorMessage) { + super(httpStatus, moimErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/service/MoimService.java b/backend/src/main/java/mouda/backend/moim/service/MoimService.java index 9f54fdc8b..6cdeb2a70 100644 --- a/backend/src/main/java/mouda/backend/moim/service/MoimService.java +++ b/backend/src/main/java/mouda/backend/moim/service/MoimService.java @@ -2,16 +2,21 @@ import java.util.List; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.member.repository.MemberRepository; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.dto.request.MoimCreateRequest; import mouda.backend.moim.dto.request.MoimJoinRequest; import mouda.backend.moim.dto.response.MoimDetailsFindResponse; import mouda.backend.moim.dto.response.MoimFindAllResponse; import mouda.backend.moim.dto.response.MoimFindAllResponses; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; import mouda.backend.moim.repository.MoimRepository; @Transactional @@ -21,8 +26,15 @@ public class MoimService { private final MoimRepository moimRepository; + private final MemberRepository memberRepository; + public Moim createMoim(MoimCreateRequest moimCreateRequest) { - return moimRepository.save(moimCreateRequest.toEntity()); + Member author = new Member(moimCreateRequest.authorNickname()); + Moim moim = moimRepository.save(moimCreateRequest.toEntity()); + author.joinMoim(moim); + memberRepository.save(author); + + return moim; } @Transactional(readOnly = true) @@ -30,7 +42,10 @@ public MoimFindAllResponses findAllMoim() { List moims = moimRepository.findAll(); return new MoimFindAllResponses( moims.stream() - .map(MoimFindAllResponse::toResponse) + .map(moim -> { + List participants = memberRepository.findAllByMoimId(moim.getId()); + return MoimFindAllResponse.toResponse(moim, participants.size()); + }) .toList() ); } @@ -38,20 +53,34 @@ public MoimFindAllResponses findAllMoim() { @Transactional(readOnly = true) public MoimDetailsFindResponse findMoimDetails(long id) { Moim moim = moimRepository.findById(id) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.NOT_FOUND)); + + List participants = memberRepository.findAllByMoimId(id).stream() + .map(Member::getNickname) + .toList(); - return MoimDetailsFindResponse.toResponse(moim); + return MoimDetailsFindResponse.toResponse(moim, participants); } public void joinMoim(MoimJoinRequest moimJoinRequest) { + Member member = new Member(moimJoinRequest.nickname()); Moim moim = moimRepository.findById(moimJoinRequest.moimId()) - .orElseThrow(() -> new IllegalArgumentException("모임이 존재하지 않습니다.")); - moim.join(); + .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.NOT_FOUND)); + + member.joinMoim(moim); + memberRepository.save(member); + List participants = memberRepository.findAllByMoimId(moim.getId()); + + moim.validateAlreadyFullMoim(participants.size()); } public void deleteMoim(long id) { Moim moim = moimRepository.findById(id) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.NOT_FOUND)); + List participants = memberRepository.findAllByMoimId(moim.getId()); + for (Member participant : participants) { + memberRepository.deleteById(participant.getId()); + } moimRepository.delete(moim); } diff --git a/backend/src/test/java/mouda/backend/config/DatabaseCleaner.java b/backend/src/test/java/mouda/backend/config/DatabaseCleaner.java index f78e879e6..a797ba45f 100644 --- a/backend/src/test/java/mouda/backend/config/DatabaseCleaner.java +++ b/backend/src/test/java/mouda/backend/config/DatabaseCleaner.java @@ -1,26 +1,29 @@ package mouda.backend.config; -import java.util.List; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; @Component public class DatabaseCleaner { - @Autowired - private JdbcTemplate jdbcTemplate; - - public DatabaseCleaner(final JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public void cleanUp() { - String sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'PUBLIC' AND table_type='BASE TABLE'"; - List tables = jdbcTemplate.queryForList(sql, String.class); - for (String tableName : tables) { - jdbcTemplate.update("DELETE FROM " + tableName); - jdbcTemplate.update("ALTER TABLE " + tableName + " alter column id restart with 1"); - } - } + @Autowired + private EntityManager entityManager; + + public DatabaseCleaner(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Transactional + public void cleanUp() { + entityManager.createNativeQuery("DELETE FROM MEMBER").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE MEMBER alter column id restart with 1").executeUpdate(); + + entityManager.createNativeQuery("DELETE FROM MOIM").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE MOIM alter column id restart with 1").executeUpdate(); + + entityManager.clear(); + } } diff --git a/backend/src/test/java/mouda/backend/config/JacksonConfigTest.java b/backend/src/test/java/mouda/backend/config/JacksonConfigTest.java new file mode 100644 index 000000000..4440a39cd --- /dev/null +++ b/backend/src/test/java/mouda/backend/config/JacksonConfigTest.java @@ -0,0 +1,176 @@ +package mouda.backend.config; + +import static org.hamcrest.Matchers.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import io.restassured.RestAssured; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.repository.MoimRepository; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class JacksonConfigTest { + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private DatabaseCleaner databaseCleaner; + + @LocalServerPort + private int port; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @AfterEach + void tearDown() { + databaseCleaner.cleanUp(); + } + + @DisplayName("날짜 형식에 따른 직렬화 및 역직렬화 테스트") + @Nested + class DateFormatTest { + + @DisplayName("yyyy-MM-dd 형식의 날짜를 역직렬화한다.") + @Test + void deserialize() { + Map params = Map.of( + "title", "title", + "date", "2024-07-19", + "time", "12:30", + "place", "place", + "maxPeople", 10, + "authorNickname", "안나", + "description", "설명" + ); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/v1/moim") + .then().statusCode(is(HttpStatus.OK.value())); + } + + @DisplayName("yyyy-MM-dd 형식이 아닌 날짜가 입력되면 예외가 발생한다.") + @Test + void deserialize_when_invalidDateFormat() { + Map params = Map.of( + "title", "title", + "date", "2024/07/19", + "time", "12:30", + "place", "place", + "maxPeople", 10, + "authorNickname", "안나", + "description", "설명" + ); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/v1/moim") + .then().statusCode(is(HttpStatus.BAD_REQUEST.value())); + } + + @DisplayName("날짜는 yyyy-MM-dd 형식으로 직렬화된다.") + @Test + void serialize() { + Moim moim = Moim.builder() + .title("title") + .date(LocalDate.parse("2024-07-19")) + .time(LocalTime.parse("12:30")) + .place("place") + .maxPeople(10) + .authorNickname("안나") + .description("설명") + .build(); + + Moim saved = moimRepository.save(moim); + + RestAssured.given() + .when().get("/v1/moim/" + saved.getId()) + .then().statusCode(is(HttpStatus.OK.value())) + .body("data.date", is("2024-07-19")); + } + } + + @DisplayName("시간 형식에 따른 직렬화 및 역직렬화 테스트") + @Nested + class TimeFormatTest { + + @DisplayName("HH:mm 형식의 시간을 역직렬화한다.") + @Test + void deserialize() { + Map params = Map.of( + "title", "title", + "date", "2024-07-19", + "time", "12:30", + "place", "place", + "maxPeople", 10, + "authorNickname", "안나", + "description", "설명" + ); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/v1/moim") + .then().statusCode(is(HttpStatus.OK.value())); + } + + @DisplayName("HH:mm 형식이 아닌 시간이 입력되면 예외가 발생한다.") + @Test + void deserialize_when_invalidTimeFormat() { + Map params = Map.of( + "title", "title", + "date", "2024-07-19", + "time", "12-30", + "place", "place", + "maxPeople", 10, + "authorNickname", "안나", + "description", "설명" + ); + + RestAssured.given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(params) + .when().post("/v1/moim") + .then().statusCode(is(HttpStatus.BAD_REQUEST.value())); + } + + @DisplayName("시간은 HH:mm 형식으로 직렬화된다.") + @Test + void serialize() { + Moim moim = Moim.builder() + .title("title") + .date(LocalDate.now().plusDays(1)) + .time(LocalTime.parse("12:30")) + .place("place") + .maxPeople(10) + .authorNickname("안나") + .description("설명") + .build(); + Moim saved = moimRepository.save(moim); + + RestAssured.given() + .when().get("/v1/moim/" + saved.getId()) + .then().statusCode(is(HttpStatus.OK.value())) + .body("data.time", is("12:30")); + } + } +} diff --git a/backend/src/test/java/mouda/backend/member/repository/MemberRepositoryTest.java b/backend/src/test/java/mouda/backend/member/repository/MemberRepositoryTest.java new file mode 100644 index 000000000..7128727e5 --- /dev/null +++ b/backend/src/test/java/mouda/backend/member/repository/MemberRepositoryTest.java @@ -0,0 +1,48 @@ +package mouda.backend.member.repository; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.config.DatabaseCleaner; +import mouda.backend.member.domain.Member; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.repository.MoimRepository; + +@SpringBootTest +class MemberRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private DatabaseCleaner databaseCleaner; + + @AfterEach + void cleanUp() { + databaseCleaner.cleanUp(); + } + + @DisplayName("모임에 가입된 참여자의 수를 반환한다.") + @Test + void findNickNamesByMoimId() { + Member member = new Member("tehah"); + Moim moim = Moim.builder() + .build(); + Moim saveMoim = moimRepository.save(moim); + member.joinMoim(saveMoim); + memberRepository.save(member); + + List participants = memberRepository.findAllByMoimId(saveMoim.getId()); + + Assertions.assertThat(participants.size()).isEqualTo(1); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java b/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java index 66ca9ca59..bce390403 100644 --- a/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java +++ b/backend/src/test/java/mouda/backend/moim/service/MoimServiceTest.java @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -13,6 +14,8 @@ import org.springframework.boot.test.context.SpringBootTest; import mouda.backend.config.DatabaseCleaner; +import mouda.backend.member.domain.Member; +import mouda.backend.member.repository.MemberRepository; import mouda.backend.moim.domain.Moim; import mouda.backend.moim.dto.request.MoimCreateRequest; import mouda.backend.moim.dto.request.MoimJoinRequest; @@ -29,6 +32,9 @@ class MoimServiceTest { @Autowired private MoimRepository moimRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired private DatabaseCleaner databaseCleaner; @@ -44,7 +50,9 @@ void createMoim() { "title", LocalDate.now(), LocalTime.now(), "place", 10, "안나", "설명" ); + Moim moim = moimService.createMoim(moimCreateRequest); + assertThat(moim.getId()).isEqualTo(1L); } @@ -60,7 +68,6 @@ void findAllMoim() { MoimFindAllResponses moimResponses = moimService.findAllMoim(); - assertThat(moimResponses).isNotNull(); assertThat(moimResponses.moims()).hasSize(2); } @@ -85,13 +92,15 @@ void joinMoim() { "title", LocalDate.now(), LocalTime.now(), "place", 10, "안나", "설명" ); - moimService.createMoim(moimCreateRequest); + Moim moim = moimService.createMoim(moimCreateRequest); - MoimJoinRequest moimJoinRequest = new MoimJoinRequest(1L); + MoimJoinRequest moimJoinRequest = new MoimJoinRequest(moim.getId(), "호기"); moimService.joinMoim(moimJoinRequest); + List participants = memberRepository.findAllByMoimId(moim.getId()); - MoimDetailsFindResponse moimDetails = moimService.findMoimDetails(1L); - assertThat(moimDetails.currentPeople()).isEqualTo(1); + Optional moimOptional = moimRepository.findById(moim.getId()); + assertThat(moimOptional).isNotEmpty(); + assertThat(participants.size()).isEqualTo(2); } @DisplayName("모임을 삭제한다.") @@ -104,8 +113,8 @@ void deleteMoim() { moimService.createMoim(moimCreateRequest); moimService.deleteMoim(1L); - List moims = moimRepository.findAll(); + List moims = moimRepository.findAll(); assertThat(moims).hasSize(0); } } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 026ce36af..3ba0d1773 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -4,7 +4,8 @@ spring: properties: hibernate: format_sql: true - ddl-auto: create-drop + hibernate: + ddl-auto: create-drop defer-datasource-initialization: true sql: init: