diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..dea1a3f8 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,23 @@ +name: Deploy + +on: + push: + branches: + - Weekly + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.5.3 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Run deployment script on server + run: | + ssh -o StrictHostKeyChecking=no ubuntu@${{ secrets.SERVER_IP }} 'bash /home/ubuntu/deploy.sh' diff --git a/.gitignore b/.gitignore index de8f62d4..87e1fd0d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ out/ ### dev file ### src/main/resources/application-dev.properties + +### macOS DS_Store files ### +.DS_Store +src/.DS_Store +src/main/.DS_Store diff --git a/build.gradle b/build.gradle index b54be87e..e808707a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' implementation group: 'com.twilio.sdk', name: 'twilio', version: '10.5.0' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/sinitto/auth/controller/AuthControllerAdvice.java b/src/main/java/com/example/sinitto/auth/controller/AuthControllerAdvice.java index 4199e943..621239a4 100644 --- a/src/main/java/com/example/sinitto/auth/controller/AuthControllerAdvice.java +++ b/src/main/java/com/example/sinitto/auth/controller/AuthControllerAdvice.java @@ -1,9 +1,6 @@ package com.example.sinitto.auth.controller; -import com.example.sinitto.auth.exception.JWTExpirationException; -import com.example.sinitto.auth.exception.KakaoRefreshTokenExpirationException; -import com.example.sinitto.auth.exception.TokenNotFoundException; -import com.example.sinitto.auth.exception.UnauthorizedException; +import com.example.sinitto.auth.exception.*; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; @@ -15,11 +12,17 @@ @RestControllerAdvice(basePackages = "com.example.sinitto.auth") public class AuthControllerAdvice { + private static final String UNAUTHORIZED_ACCESS_URI = "/errors/unauthorized-access"; + private static final String JWT_EXPIRATION_URI = "/errors/unauthorized-access-by-jwt-expiration"; + private static final String TOKEN_NOT_FOUND_URI = "/errors/token-not-found"; + private static final String KAKAO_REFRESH_TOKEN_EXPIRATION_URI = "/errors/unauthorized-access-by-kakao-refresh-token-expiration"; + private static final String KAKAO_EMAIL_NOT_FOUND_URI = "/errors/kakao-email-not-found"; + @ExceptionHandler(UnauthorizedException.class) public ResponseEntity handleUnauthorizedException(UnauthorizedException ex) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getMessage()); - problemDetail.setType(URI.create("/errors/unauthorized-access")); + problemDetail.setType(URI.create(UNAUTHORIZED_ACCESS_URI)); problemDetail.setTitle("Unauthorized Access"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail); } @@ -28,7 +31,7 @@ public ResponseEntity handleUnauthorizedException(UnauthorizedExc public ResponseEntity handleJWTExpirationException(JWTExpirationException ex) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getMessage()); - problemDetail.setType(URI.create("/errors/unauthorized-access-by-jwt-expiration")); + problemDetail.setType(URI.create(JWT_EXPIRATION_URI)); problemDetail.setTitle("Unauthorized Access By JWT Expiration"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail); } @@ -37,7 +40,7 @@ public ResponseEntity handleJWTExpirationException(JWTExpirationE public ResponseEntity handleTokenNotFoundException(TokenNotFoundException ex) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); - problemDetail.setType(URI.create("/errors/token-not-found")); + problemDetail.setType(URI.create(TOKEN_NOT_FOUND_URI)); problemDetail.setTitle("Token Not Found"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); } @@ -46,8 +49,17 @@ public ResponseEntity handleTokenNotFoundException(TokenNotFoundE public ResponseEntity handleKakaoRefreshTokenExpirationException(KakaoRefreshTokenExpirationException ex) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getMessage()); - problemDetail.setType(URI.create("/errors/unauthorized-access-by-kakao-refresh-token-expiration")); + problemDetail.setType(URI.create(KAKAO_REFRESH_TOKEN_EXPIRATION_URI)); problemDetail.setTitle("Unauthorized Access By Kakao Refresh Token Expiration"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail); } + + @ExceptionHandler(KakaoEmailNotFoundException.class) + public ResponseEntity handleKakaoEmailNotFoundException(KakaoEmailNotFoundException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, + ex.getMessage()); + problemDetail.setType(URI.create(KAKAO_EMAIL_NOT_FOUND_URI)); + problemDetail.setTitle("Kakao Email Not Found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); + } } diff --git a/src/main/java/com/example/sinitto/auth/dto/KakaoTokenResponse.java b/src/main/java/com/example/sinitto/auth/dto/KakaoTokenResponse.java index e7b9b755..0cd57add 100644 --- a/src/main/java/com/example/sinitto/auth/dto/KakaoTokenResponse.java +++ b/src/main/java/com/example/sinitto/auth/dto/KakaoTokenResponse.java @@ -8,8 +8,8 @@ public record KakaoTokenResponse( String accessToken, String refreshToken, - Integer expiresIn, - Integer refreshTokenExpiresIn + int expiresIn, + int refreshTokenExpiresIn ) { diff --git a/src/main/java/com/example/sinitto/auth/dto/LoginResponse.java b/src/main/java/com/example/sinitto/auth/dto/LoginResponse.java index 6e105535..455a52c5 100644 --- a/src/main/java/com/example/sinitto/auth/dto/LoginResponse.java +++ b/src/main/java/com/example/sinitto/auth/dto/LoginResponse.java @@ -5,6 +5,7 @@ public record LoginResponse( String refreshToken, String redirectUrl, String email, - boolean isSinitto + boolean isSinitto, + boolean isMember ) { } diff --git a/src/main/java/com/example/sinitto/auth/entity/KakaoToken.java b/src/main/java/com/example/sinitto/auth/entity/KakaoToken.java index 1ac40bad..9a00aa98 100644 --- a/src/main/java/com/example/sinitto/auth/entity/KakaoToken.java +++ b/src/main/java/com/example/sinitto/auth/entity/KakaoToken.java @@ -24,28 +24,28 @@ public class KakaoToken { @NotNull private String refreshToken; @NotNull - private int expires_in; + private int expiresIn; @NotNull - private int refresh_token_expires_in; + private int refreshTokenExpiresIn; public KakaoToken(String memberEmail, String accessToken, String refreshToken, - int expires_in, int refresh_token_expires_in) { + int expiresIn, int refreshTokenExpiresIn) { this.memberEmail = memberEmail; this.accessToken = accessToken; this.refreshToken = refreshToken; - this.expires_in = expires_in; - this.refresh_token_expires_in = refresh_token_expires_in; + this.expiresIn = expiresIn; + this.refreshTokenExpiresIn = refreshTokenExpiresIn; } protected KakaoToken() { } public boolean isAccessTokenExpired() { - return LocalDateTime.now().isAfter(issuedAt.plusSeconds(expires_in)); + return LocalDateTime.now().isAfter(issuedAt.plusSeconds(expiresIn)); } public boolean isRefreshTokenExpired() { - return LocalDateTime.now().isAfter(issuedAt.plusSeconds(refresh_token_expires_in)); + return LocalDateTime.now().isAfter(issuedAt.plusSeconds(refreshTokenExpiresIn)); } public String getAccessToken() { @@ -65,11 +65,11 @@ public String getMemberEmail() { } public void updateKakaoToken(String accessToken, String refreshToken, - int expires_in, int refresh_token_expires_in) { + int expiresIn, int refreshTokenExpiresIn) { this.accessToken = accessToken; this.refreshToken = refreshToken; - this.expires_in = expires_in; - this.refresh_token_expires_in = refresh_token_expires_in; + this.expiresIn = expiresIn; + this.refreshTokenExpiresIn = refreshTokenExpiresIn; this.issuedAt = LocalDateTime.now(); } } diff --git a/src/main/java/com/example/sinitto/auth/exception/KakaoEmailNotFoundException.java b/src/main/java/com/example/sinitto/auth/exception/KakaoEmailNotFoundException.java new file mode 100644 index 00000000..3661d546 --- /dev/null +++ b/src/main/java/com/example/sinitto/auth/exception/KakaoEmailNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.auth.exception; + +public class KakaoEmailNotFoundException extends RuntimeException { + public KakaoEmailNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/auth/service/KakaoApiService.java b/src/main/java/com/example/sinitto/auth/service/KakaoApiService.java index 9efc0a24..d337e233 100644 --- a/src/main/java/com/example/sinitto/auth/service/KakaoApiService.java +++ b/src/main/java/com/example/sinitto/auth/service/KakaoApiService.java @@ -2,6 +2,7 @@ import com.example.sinitto.auth.dto.KakaoTokenResponse; import com.example.sinitto.auth.dto.KakaoUserResponse; +import com.example.sinitto.auth.exception.KakaoEmailNotFoundException; import com.example.sinitto.common.properties.KakaoProperties; import org.springframework.http.*; import org.springframework.stereotype.Service; @@ -14,6 +15,8 @@ @Service public class KakaoApiService { + private static final String KAKAO_AUTH_BASE_URL = "https://kauth.kakao.com/oauth"; + private static final String KAKAO_API_BASE_URL = "https://kapi.kakao.com/v2/user"; private final RestTemplate restTemplate; private final KakaoProperties kakaoProperties; @@ -23,14 +26,15 @@ public KakaoApiService(RestTemplate restTemplate, KakaoProperties kakaoPropertie } public String getAuthorizationUrl() { - return "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=" + return KAKAO_AUTH_BASE_URL + "/authorize?response_type=code&client_id=" + kakaoProperties.clientId() + "&redirect_uri=" + kakaoProperties.redirectUri(); } public KakaoTokenResponse getAccessToken(String authorizationCode) { - String url = "https://kauth.kakao.com/oauth/token"; + String url = KAKAO_AUTH_BASE_URL + "/token"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + LinkedMultiValueMap body = new LinkedMultiValueMap<>(); body.add("grant_type", "authorization_code"); body.add("client_id", kakaoProperties.clientId()); @@ -47,8 +51,7 @@ public KakaoTokenResponse getAccessToken(String authorizationCode) { } public KakaoTokenResponse refreshAccessToken(String refreshToken) { - String url = "https://kauth.kakao.com/oauth/token"; - + String url = KAKAO_AUTH_BASE_URL + "/token"; String body = "grant_type=refresh_token&client_id=" + kakaoProperties.clientId() + "&refresh_token=" + refreshToken; @@ -64,7 +67,7 @@ public KakaoTokenResponse refreshAccessToken(String refreshToken) { } public KakaoUserResponse getUserInfo(String accessToken) { - String url = "https://kapi.kakao.com/v2/user/me"; + String url = KAKAO_API_BASE_URL + "/me"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBearerAuth(accessToken); @@ -77,7 +80,10 @@ public KakaoUserResponse getUserInfo(String accessToken) { ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, request, KakaoUserResponse.class); + if (response.getBody().kakaoAccount().email() == null) { + throw new KakaoEmailNotFoundException("카카오 계정으로부터 전달받은 이메일이 없습니다."); + } + return response.getBody(); } - } diff --git a/src/main/java/com/example/sinitto/auth/service/TokenService.java b/src/main/java/com/example/sinitto/auth/service/TokenService.java index 2bd6f2f6..57b2cb42 100644 --- a/src/main/java/com/example/sinitto/auth/service/TokenService.java +++ b/src/main/java/com/example/sinitto/auth/service/TokenService.java @@ -6,12 +6,14 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.util.Base64; import java.util.Date; +import java.util.concurrent.TimeUnit; @Service public class TokenService { @@ -20,10 +22,12 @@ public class TokenService { private static final long REFRESH_SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7; private final Key secretKey; + private final RedisTemplate redisTemplate; - public TokenService(@Value("${jwt.secret}") String secretKey) { + public TokenService(@Value("${jwt.secret}") String secretKey, RedisTemplate redisTemplate) { byte[] decodedKey = Base64.getDecoder().decode(secretKey); this.secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256"); + this.redisTemplate = redisTemplate; } public String generateAccessToken(String email) { @@ -36,51 +40,45 @@ public String generateAccessToken(String email) { } public String generateRefreshToken(String email) { - return Jwts.builder() + String refreshToken = Jwts.builder() .setSubject(email) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + REFRESH_SEVEN_DAYS)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); + + redisTemplate.opsForValue().set(email, refreshToken, REFRESH_SEVEN_DAYS, TimeUnit.MILLISECONDS); + return refreshToken; } + public String extractEmail(String token) { - try { - var claims = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .getBody(); - - if (claims.getExpiration().before(new Date())) { - throw new JWTExpirationException("엑세스 토큰이 만료되었습니다."); - } - - return claims.getSubject(); - } catch (Exception e) { - throw new UnauthorizedException("유효하지 않은 엑세스 토큰입니다."); + var claims = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + + if (claims.getExpiration().before(new Date())) { + throw new JWTExpirationException("토큰이 만료되었습니다. 재로그인이 필요합니다."); } + + return claims.getSubject(); } public TokenResponse refreshAccessToken(String refreshToken) { - try { - var claims = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(refreshToken) - .getBody(); - - if (claims.getExpiration().before(new Date())) { - throw new JWTExpirationException("리프레쉬 토큰이 만료되었습니다."); - } - - String newAccessToken = generateAccessToken(claims.getSubject()); - String newRefreshToken = generateRefreshToken(claims.getSubject()); - - return new TokenResponse(newAccessToken, newRefreshToken); - } catch (Exception e) { - throw new UnauthorizedException("유효하지 않은 리프레쉬 토큰입니다."); + String email = extractEmail(refreshToken); + + String storedRefreshToken = redisTemplate.opsForValue().get(email); + if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) { + throw new UnauthorizedException("만료되거나 이미 한번 사용된 리프레쉬 토큰입니다. 재로그인이 필요합니다."); } - } + redisTemplate.delete(email); + + String newAccessToken = generateAccessToken(email); + String newRefreshToken = generateRefreshToken(email); + + return new TokenResponse(newAccessToken, newRefreshToken); + } } diff --git a/src/main/java/com/example/sinitto/callback/controller/CallbackController.java b/src/main/java/com/example/sinitto/callback/controller/CallbackController.java index 69025aca..e99cb5c2 100644 --- a/src/main/java/com/example/sinitto/callback/controller/CallbackController.java +++ b/src/main/java/com/example/sinitto/callback/controller/CallbackController.java @@ -31,7 +31,16 @@ public ResponseEntity> getCallbackList(@MemberId Long mem return ResponseEntity.ok(callbackService.getCallbacks(memberId, pageable)); } - @Operation(summary = "콜백 전화 완료", description = "시니또와 시니어의 연락이 끝났을 때 시니어의 요청사항을 수행 여부를 선택하여 처리합니다.") + @Operation(summary = "진행 상태인 콜백을 완료 대기 상태로 전환(시니또가)", description = "시니또가 수락한 콜백 수행을 완료했을때 이 api 호출하면 완료 대기 상태로 변합니다.") + @PutMapping("/pendingComplete/{callbackId}") + public ResponseEntity pendingCompleteCallback(@MemberId Long memberId, + @PathVariable Long callbackId) { + + callbackService.pendingComplete(memberId, callbackId); + return ResponseEntity.ok().build(); + } + + @Operation(summary = "완료 대기 상태인 콜백을 완료 상태로 전환(보호자가)", description = "보호자가 완료 대기 상태인 콜백을 완료 확정 시킵니다.") @PutMapping("/complete/{callbackId}") public ResponseEntity completeCallback(@MemberId Long memberId, @PathVariable Long callbackId) { @@ -63,4 +72,11 @@ public ResponseEntity addCallCheck(@RequestParam("From") String fromNumb return ResponseEntity.ok(callbackService.add(fromNumber)); } + + @Operation(summary = "시니또에게 현재 할당된 콜백 조회", description = "현재 시니또 본인에게 할당된 콜백을 조회합니다.") + @GetMapping("/sinitto/accepted") + public ResponseEntity getAcceptedCallback(@MemberId Long memberId) { + + return ResponseEntity.ok(callbackService.getAcceptedCallback(memberId)); + } } diff --git a/src/main/java/com/example/sinitto/callback/controller/CallbackControllerAdvice.java b/src/main/java/com/example/sinitto/callback/controller/CallbackControllerAdvice.java index acd3fda1..bc352f2a 100644 --- a/src/main/java/com/example/sinitto/callback/controller/CallbackControllerAdvice.java +++ b/src/main/java/com/example/sinitto/callback/controller/CallbackControllerAdvice.java @@ -2,7 +2,7 @@ import com.example.sinitto.callback.exception.ConflictException; import com.example.sinitto.callback.exception.ForbiddenException; -import jakarta.persistence.EntityNotFoundException; +import com.example.sinitto.callback.exception.NotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; @@ -14,15 +14,6 @@ @RestControllerAdvice(basePackages = "com.example.sinitto.callback") public class CallbackControllerAdvice { - @ExceptionHandler(EntityNotFoundException.class) - public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e) { - - ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); - problemDetail.setType(URI.create("/errors/bad-request")); - problemDetail.setTitle("Bad Request"); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); - } - @ExceptionHandler(ForbiddenException.class) public ResponseEntity handleForbiddenException(ForbiddenException e) { @@ -41,4 +32,13 @@ public ResponseEntity handleConflictException(ConflictException e return ResponseEntity.status(HttpStatus.CONFLICT).body(problemDetail); } + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(NotFoundException e) { + + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); + problemDetail.setType(URI.create("/errors/not-found")); + problemDetail.setTitle("Not Found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); + } + } diff --git a/src/main/java/com/example/sinitto/callback/entity/Callback.java b/src/main/java/com/example/sinitto/callback/entity/Callback.java index a99db6f4..c9425f57 100644 --- a/src/main/java/com/example/sinitto/callback/entity/Callback.java +++ b/src/main/java/com/example/sinitto/callback/entity/Callback.java @@ -2,6 +2,7 @@ import com.example.sinitto.callback.exception.AlreadyCompleteException; import com.example.sinitto.callback.exception.AlreadyInProgressException; +import com.example.sinitto.callback.exception.AlreadyPendingCompleteException; import com.example.sinitto.callback.exception.AlreadyWaitingException; import com.example.sinitto.member.entity.Senior; import jakarta.persistence.*; @@ -77,10 +78,19 @@ public void changeStatusToInProgress() { this.status = Status.IN_PROGRESS; } - public void changeStatusToComplete() { + public void changeStatusToPendingComplete() { if (this.status != Status.IN_PROGRESS) { - throwStatusException("완료 상태로 변경은 진행 상태에서만 가능합니다."); + throwStatusException("완료 대기 상태로 변경은 진행 상태에서만 가능합니다."); + } + + this.status = Status.PENDING_COMPLETE; + } + + public void changeStatusToComplete() { + + if (this.status != Status.PENDING_COMPLETE) { + throwStatusException("완료 상태로 변경은 완료 대기 상태에서만 가능합니다."); } this.status = Status.COMPLETE; @@ -88,6 +98,9 @@ public void changeStatusToComplete() { private void throwStatusException(String message) { + if (this.status == Status.PENDING_COMPLETE) { + throw new AlreadyPendingCompleteException("완료 대기 상태의 콜백 입니다. " + message); + } if (this.status == Status.COMPLETE) { throw new AlreadyCompleteException("완료 상태의 콜백 입니다. " + message); } @@ -111,6 +124,10 @@ public String getStatus() { return status.name(); } + public Senior getSenior() { + return senior; + } + public String getSeniorName() { return senior.getName(); } @@ -126,6 +143,7 @@ public Long getAssignedMemberId() { public enum Status { WAITING, IN_PROGRESS, + PENDING_COMPLETE, COMPLETE } diff --git a/src/main/java/com/example/sinitto/callback/exception/AlreadyPendingCompleteException.java b/src/main/java/com/example/sinitto/callback/exception/AlreadyPendingCompleteException.java new file mode 100644 index 00000000..c3b8aaa8 --- /dev/null +++ b/src/main/java/com/example/sinitto/callback/exception/AlreadyPendingCompleteException.java @@ -0,0 +1,8 @@ +package com.example.sinitto.callback.exception; + +public class AlreadyPendingCompleteException extends ConflictException { + + public AlreadyPendingCompleteException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/callback/exception/GuardMismatchException.java b/src/main/java/com/example/sinitto/callback/exception/GuardMismatchException.java new file mode 100644 index 00000000..13ff0e83 --- /dev/null +++ b/src/main/java/com/example/sinitto/callback/exception/GuardMismatchException.java @@ -0,0 +1,8 @@ +package com.example.sinitto.callback.exception; + +public class GuardMismatchException extends NotFoundException { + + public GuardMismatchException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/callback/exception/NotExistCallbackException.java b/src/main/java/com/example/sinitto/callback/exception/NotExistCallbackException.java new file mode 100644 index 00000000..3a518753 --- /dev/null +++ b/src/main/java/com/example/sinitto/callback/exception/NotExistCallbackException.java @@ -0,0 +1,8 @@ +package com.example.sinitto.callback.exception; + +public class NotExistCallbackException extends NotFoundException { + + public NotExistCallbackException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/callback/exception/NotFoundException.java b/src/main/java/com/example/sinitto/callback/exception/NotFoundException.java new file mode 100644 index 00000000..c6750b07 --- /dev/null +++ b/src/main/java/com/example/sinitto/callback/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package com.example.sinitto.callback.exception; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/callback/exception/NotMemberException.java b/src/main/java/com/example/sinitto/callback/exception/NotMemberException.java new file mode 100644 index 00000000..ff6247a7 --- /dev/null +++ b/src/main/java/com/example/sinitto/callback/exception/NotMemberException.java @@ -0,0 +1,8 @@ +package com.example.sinitto.callback.exception; + +public class NotMemberException extends NotFoundException { + + public NotMemberException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/callback/repository/CallbackRepository.java b/src/main/java/com/example/sinitto/callback/repository/CallbackRepository.java index fb3aab1d..b34aa1cb 100644 --- a/src/main/java/com/example/sinitto/callback/repository/CallbackRepository.java +++ b/src/main/java/com/example/sinitto/callback/repository/CallbackRepository.java @@ -5,8 +5,11 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface CallbackRepository extends JpaRepository { Page findAll(Pageable pageable); + Optional findByAssignedMemberIdAndStatus(Long memberId, Callback.Status status); } diff --git a/src/main/java/com/example/sinitto/callback/service/CallbackService.java b/src/main/java/com/example/sinitto/callback/service/CallbackService.java index 9ff5304b..ffced311 100644 --- a/src/main/java/com/example/sinitto/callback/service/CallbackService.java +++ b/src/main/java/com/example/sinitto/callback/service/CallbackService.java @@ -2,15 +2,13 @@ import com.example.sinitto.callback.dto.CallbackResponse; import com.example.sinitto.callback.entity.Callback; -import com.example.sinitto.callback.exception.NotAssignedException; -import com.example.sinitto.callback.exception.NotSinittoException; +import com.example.sinitto.callback.exception.*; import com.example.sinitto.callback.repository.CallbackRepository; import com.example.sinitto.callback.util.TwilioHelper; import com.example.sinitto.guard.repository.SeniorRepository; import com.example.sinitto.member.entity.Member; import com.example.sinitto.member.entity.Senior; import com.example.sinitto.member.repository.MemberRepository; -import jakarta.persistence.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -54,7 +52,7 @@ public void accept(Long memberId, Long callbackId) { } @Transactional - public void complete(Long memberId, Long callbackId) { + public void pendingComplete(Long memberId, Long callbackId) { checkAuthorization(memberId); @@ -62,6 +60,21 @@ public void complete(Long memberId, Long callbackId) { checkAssignment(memberId, callback.getAssignedMemberId()); + callback.changeStatusToPendingComplete(); + } + + @Transactional + public void complete(Long memberId, Long callbackId) { + + Callback callback = getCallbackOrThrow(callbackId); + + Senior senior = callback.getSenior(); + Long guardId = senior.getMember().getId(); + + if (!guardId.equals(memberId)) { + throw new GuardMismatchException("이 API를 요청한 보호자는 이 콜백을 요청 한 시니어의 보호자가 아닙니다."); + } + callback.changeStatusToComplete(); } @@ -94,10 +107,20 @@ public String add(String fromNumber) { return TwilioHelper.convertMessageToTwiML(SUCCESS_MESSAGE); } + public CallbackResponse getAcceptedCallback(Long memberId) { + + checkAuthorization(memberId); + + Callback callback = callbackRepository.findByAssignedMemberIdAndStatus(memberId, Callback.Status.IN_PROGRESS) + .orElseThrow(() -> new NotExistCallbackException("요청한 시니또에 할당된 콜백이 없습니다")); + + return new CallbackResponse(callback.getId(), callback.getSeniorName(), callback.getPostTime(), callback.getStatus(), callback.getSeniorId()); + } + private void checkAuthorization(Long memberId) { Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new EntityNotFoundException("멤버가 아닙니다")); + .orElseThrow(() -> new NotMemberException("멤버가 아닙니다")); if (!member.isSinitto()) { throw new NotSinittoException("시니또가 아닙니다"); @@ -107,7 +130,7 @@ private void checkAuthorization(Long memberId) { private Callback getCallbackOrThrow(Long callbackId) { return callbackRepository.findById(callbackId) - .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 콜백입니다")); + .orElseThrow(() -> new NotExistCallbackException("존재하지 않는 콜백입니다")); } private void checkAssignment(Long memberId, Long assignedMemberId) { diff --git a/src/main/java/com/example/sinitto/common/config/RestTemplateResponseErrorHandler.java b/src/main/java/com/example/sinitto/common/config/RestTemplateResponseErrorHandler.java new file mode 100644 index 00000000..98e81a56 --- /dev/null +++ b/src/main/java/com/example/sinitto/common/config/RestTemplateResponseErrorHandler.java @@ -0,0 +1,42 @@ +package com.example.sinitto.common.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResponseErrorHandler; + +import java.io.IOException; + +@Component +public class RestTemplateResponseErrorHandler implements ResponseErrorHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger( + RestTemplateResponseErrorHandler.class); + + @Override + public boolean hasError(ClientHttpResponse httpResponse) throws IOException { + var statusCode = httpResponse.getStatusCode(); + return statusCode.is4xxClientError() || statusCode.is5xxServerError(); + } + + @Override + public void handleError(ClientHttpResponse httpResponse) throws IOException { + var statusCode = httpResponse.getStatusCode(); + String responseBody = new String(httpResponse.getBody().readAllBytes()); + + if (statusCode.is4xxClientError()) { + LOGGER.error("클라이언트 에러: {} - Response body: {}", statusCode, responseBody); + throw new HttpClientErrorException(statusCode, httpResponse.getStatusText(), + httpResponse.getHeaders(), responseBody.getBytes(), null); + } + + if (statusCode.is5xxServerError()) { + LOGGER.error("서버 에러: {} - Response body: {}", statusCode, responseBody); + throw new HttpServerErrorException(statusCode, httpResponse.getStatusText(), + httpResponse.getHeaders(), responseBody.getBytes(), null); + } + } +} diff --git a/src/main/java/com/example/sinitto/common/config/WebConfig.java b/src/main/java/com/example/sinitto/common/config/WebConfig.java index f1e5aa52..0e024405 100644 --- a/src/main/java/com/example/sinitto/common/config/WebConfig.java +++ b/src/main/java/com/example/sinitto/common/config/WebConfig.java @@ -1,5 +1,6 @@ package com.example.sinitto.common.config; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @@ -8,12 +9,20 @@ import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.time.Duration; + @Configuration public class WebConfig implements WebMvcConfigurer { + private static final int TIME_OUT_DURATION = 5; + @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); + public RestTemplate restTemplate(RestTemplateBuilder builder, RestTemplateResponseErrorHandler errorHandler) { + return builder + .errorHandler(errorHandler) + .setConnectTimeout(Duration.ofSeconds(TIME_OUT_DURATION)) + .setReadTimeout(Duration.ofSeconds(TIME_OUT_DURATION)) + .build(); } @Bean diff --git a/src/main/java/com/example/sinitto/guard/controller/GuardController.java b/src/main/java/com/example/sinitto/guard/controller/GuardController.java index d13a0c51..3ee13ad0 100644 --- a/src/main/java/com/example/sinitto/guard/controller/GuardController.java +++ b/src/main/java/com/example/sinitto/guard/controller/GuardController.java @@ -11,7 +11,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; import java.util.List; @RestController @@ -20,9 +19,10 @@ public class GuardController { private final GuardService guardService; - public GuardController(GuardService guardService){ + public GuardController(GuardService guardService) { this.guardService = guardService; } + @Operation(summary = "연결된 모든 시니어 정보 조회", description = "보호자가 등록한 모든 시니어의 정보를 요청합니다.") @GetMapping("/senior") public ResponseEntity> getAllSeniors(@MemberId Long memberId) { diff --git a/src/main/java/com/example/sinitto/guard/controller/GuardControllerAdvice.java b/src/main/java/com/example/sinitto/guard/controller/GuardControllerAdvice.java index 426a8bd4..e651db7b 100644 --- a/src/main/java/com/example/sinitto/guard/controller/GuardControllerAdvice.java +++ b/src/main/java/com/example/sinitto/guard/controller/GuardControllerAdvice.java @@ -12,7 +12,7 @@ @RestControllerAdvice(basePackages = "com.example.sinitto.guard") public class GuardControllerAdvice { @ExceptionHandler(SeniorNotFoundException.class) - public ResponseEntity handleSeniorNotFoundException(SeniorNotFoundException e){ + public ResponseEntity handleSeniorNotFoundException(SeniorNotFoundException e) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); problemDetail.setType(URI.create("/error/senior-not-found")); problemDetail.setTitle("Senior Not Found"); diff --git a/src/main/java/com/example/sinitto/guard/repository/SeniorRepository.java b/src/main/java/com/example/sinitto/guard/repository/SeniorRepository.java index 7d9161dc..0f60727a 100644 --- a/src/main/java/com/example/sinitto/guard/repository/SeniorRepository.java +++ b/src/main/java/com/example/sinitto/guard/repository/SeniorRepository.java @@ -10,6 +10,8 @@ @Repository public interface SeniorRepository extends JpaRepository { List findByMemberId(Long memberId); + Optional findByIdAndMemberId(Long Id, Long memberId); + Optional findByPhoneNumber(String phoneNumber); } diff --git a/src/main/java/com/example/sinitto/guard/service/GuardService.java b/src/main/java/com/example/sinitto/guard/service/GuardService.java index e1d7e231..ecdbc7f5 100644 --- a/src/main/java/com/example/sinitto/guard/service/GuardService.java +++ b/src/main/java/com/example/sinitto/guard/service/GuardService.java @@ -8,9 +8,9 @@ import com.example.sinitto.guard.repository.SeniorRepository; import com.example.sinitto.member.entity.Member; import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.exception.MemberNotFoundException; import com.example.sinitto.member.repository.MemberRepository; import org.springframework.stereotype.Service; -import com.example.sinitto.member.exception.MemberNotFoundException; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -20,13 +20,13 @@ public class GuardService { private final MemberRepository memberRepository; private final SeniorRepository seniorRepository; - public GuardService(MemberRepository memberRepository, SeniorRepository seniorRepository){ + public GuardService(MemberRepository memberRepository, SeniorRepository seniorRepository) { this.memberRepository = memberRepository; this.seniorRepository = seniorRepository; } @Transactional - public GuardResponse readGuard(Long memberId){ + public GuardResponse readGuard(Long memberId) { Member member = memberRepository.findById(memberId).orElseThrow( () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); @@ -35,7 +35,7 @@ public GuardResponse readGuard(Long memberId){ } @Transactional - public void updateGuard(Long memberId, GuardRequest guardRequest){ + public void updateGuard(Long memberId, GuardRequest guardRequest) { Member member = memberRepository.findById(memberId).orElseThrow( () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); @@ -44,7 +44,7 @@ public void updateGuard(Long memberId, GuardRequest guardRequest){ } @Transactional - public void deleteGuard(Long memberId){ + public void deleteGuard(Long memberId) { Member member = memberRepository.findById(memberId).orElseThrow( () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); @@ -53,7 +53,7 @@ public void deleteGuard(Long memberId){ } @Transactional - public void createSenior(Long memberId, SeniorRequest seniorRequest){ + public void createSenior(Long memberId, SeniorRequest seniorRequest) { Member member = memberRepository.findById(memberId).orElseThrow( () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); @@ -64,14 +64,14 @@ public void createSenior(Long memberId, SeniorRequest seniorRequest){ } @Transactional - public List readSeniors(Long memberId){ + public List readSeniors(Long memberId) { List senior = seniorRepository.findByMemberId(memberId); return senior.stream().map(Senior::mapToResponse).toList(); } @Transactional - public SeniorResponse readOneSenior(Long memberId, Long seniorId){ + public SeniorResponse readOneSenior(Long memberId, Long seniorId) { Senior senior = seniorRepository.findByIdAndMemberId(seniorId, memberId).orElseThrow( () -> new SeniorNotFoundException("이메일에 해당하는 시니어를 찾을 수 없습니다.") ); @@ -80,7 +80,7 @@ public SeniorResponse readOneSenior(Long memberId, Long seniorId){ } @Transactional - public void updateSenior(Long memberId, Long seniorId, SeniorRequest seniorRequest){ + public void updateSenior(Long memberId, Long seniorId, SeniorRequest seniorRequest) { Senior senior = seniorRepository.findByIdAndMemberId(seniorId, memberId).orElseThrow( () -> new SeniorNotFoundException("이메일에 해당하는 시니어를 찾을 수 없습니다.") ); @@ -89,7 +89,7 @@ public void updateSenior(Long memberId, Long seniorId, SeniorRequest seniorReque } @Transactional - public void deleteSenior(Long memberId, Long seniorId){ + public void deleteSenior(Long memberId, Long seniorId) { Senior senior = seniorRepository.findByIdAndMemberId(seniorId, memberId).orElseThrow( () -> new SeniorNotFoundException("이메일에 해당하는 시니어를 찾을 수 없습니다.") ); @@ -98,7 +98,7 @@ public void deleteSenior(Long memberId, Long seniorId){ } @Transactional - public List readAllGuards(){ + public List readAllGuards() { List members = memberRepository.findByIsSinitto(false); return members.stream() diff --git a/src/main/java/com/example/sinitto/helloCall/controller/HelloCallController.java b/src/main/java/com/example/sinitto/helloCall/controller/HelloCallController.java index d5bea87c..5c29a028 100644 --- a/src/main/java/com/example/sinitto/helloCall/controller/HelloCallController.java +++ b/src/main/java/com/example/sinitto/helloCall/controller/HelloCallController.java @@ -1,76 +1,185 @@ package com.example.sinitto.helloCall.controller; -import com.example.sinitto.helloCall.dto.HelloCallDetailResponse; -import com.example.sinitto.helloCall.dto.HelloCallReportRequest; -import com.example.sinitto.helloCall.dto.HelloCallResponse; +import com.example.sinitto.common.annotation.MemberId; +import com.example.sinitto.helloCall.dto.*; +import com.example.sinitto.helloCall.service.HelloCallPriceService; +import com.example.sinitto.helloCall.service.HelloCallService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; +import java.util.List; @RestController @RequestMapping("/api/hellocalls") -@Tag(name = "[미구현]안부전화", description = "안부 전화 서비스 관련 API") +@Tag(name = "안부전화", description = "안부 전화 서비스 관련 API") public class HelloCallController { - @Operation(summary = "안부 전화 서비스 전체 리스트 보기", description = "보호자가 요청한 안부전화 신청정보를 리스트로 조회합니다. 페이징 처리 됩니다.") - @GetMapping - public ResponseEntity> getHelloCallList() { - // 임시 응답 - return ResponseEntity.ok(new PageImpl<>(new ArrayList<>())); + private final HelloCallService helloCallService; + private final HelloCallPriceService helloCallPriceService; + + public HelloCallController(HelloCallService helloCallService, HelloCallPriceService helloCallPriceService) { + this.helloCallService = helloCallService; + this.helloCallPriceService = helloCallPriceService; + } + + @Operation(summary = "[시니또용] 안부 전화 서비스 전체 리스트 보기", description = "안부전화 신청정보를 페이지로 조회합니다.") + @GetMapping("/sinittos/list") + public ResponseEntity> getHelloCallListBySinitto(@PageableDefault(size = 10, sort = "helloCallId", direction = Sort.Direction.ASC) Pageable pageable) { + + Page helloCallResponses = helloCallService.readAllWaitingHelloCallsBySinitto(pageable); + + return ResponseEntity.ok(helloCallResponses); + } + + @Operation(summary = "[보호자용] 보호자가 신청한 안부전화 리스트 보기", description = "보호자 본인이 신청한 안부전화 리스트를 조회합니다.") + @GetMapping("/guards/lists") + public ResponseEntity> getHelloCallListByGuard(@MemberId Long memberId) { + + List helloCallResponses = helloCallService.readAllHelloCallsByGuard(memberId); + + return ResponseEntity.ok(helloCallResponses); } - @Operation(summary = "선택한 안부 전화 서비스의 상세정보 보기", description = "안부전화 신청정보의 상세정보를 조회합니다.") + @Operation(summary = "[시니또, 보호자용] 선택한 안부 전화 서비스의 상세정보 보기", description = "안부전화 신청정보의 상세정보를 조회합니다.") @GetMapping("/{callId}") public ResponseEntity getHelloCallDetail(@PathVariable Long callId) { - // 임시 응답 - return ResponseEntity.ok(new HelloCallDetailResponse(null, null, null, null)); + + HelloCallDetailResponse helloCallDetailResponse = helloCallService.readHelloCallDetail(callId); + + return ResponseEntity.ok(helloCallDetailResponse); + } + + @Operation(summary = "[보호자용] 안부 전화 서비스 비용 조회", description = "안부 전화 서비스의 이용 비용을 조회합니다.") + @PostMapping("/guards/cost") + public ResponseEntity calculateHelloCallPrice(@RequestBody HelloCallPriceRequest helloCallPriceRequest) { + + HelloCallPriceResponse helloCallPriceResponse = helloCallPriceService.calculateHelloCallPrice(helloCallPriceRequest); + + return ResponseEntity.ok(helloCallPriceResponse); + } + + @Operation(summary = "[보호자용] 안부 전화 서비스 신청하기", description = "보호자가 안부 전화 서비스를 신청합니다.") + @PostMapping("/guards") + public ResponseEntity createHelloCallByGuard(@MemberId Long memberId, @RequestBody HelloCallRequest helloCallRequest) { + + helloCallService.createHelloCallByGuard(memberId, helloCallRequest); + + return ResponseEntity.ok(new StringMessageResponse("안부 전화 서비스가 신청되었습니다.")); + } + + @Operation(summary = "[보호자용] 안부 전화 서비스 수정하기", description = "보호자가 안부 전화 서비스 내용을 수정합니다.") + @PutMapping("/guards/{callId}") + public ResponseEntity updateHelloCallByGuard(@MemberId Long memberId, @PathVariable Long callId, @RequestBody HelloCallDetailUpdateRequest helloCallDetailUpdateRequest) { + + helloCallService.updateHelloCallByGuard(memberId, callId, helloCallDetailUpdateRequest); + + return ResponseEntity.ok(new StringMessageResponse("안부 전화 서비스 내용이 수정되었습니다.")); } - @Operation(summary = "서비스 수락하기", description = "시니또가 안부전화 신청을 수락합니다.") + @Operation(summary = "[보호자용] 안부 전화 서비스 삭제하기", description = "보호자가 안부 전화 서비스 신청을 취소합니다.") + @DeleteMapping("/guards/{callId}") + public ResponseEntity deleteHelloCallByGuard(@MemberId Long memberId, @PathVariable Long callId) { + + helloCallService.deleteHellCallByGuard(memberId, callId); + + return ResponseEntity.ok(new StringMessageResponse("신청된 안부전화 서비스가 삭제되었습니다.")); + } + + @Operation(summary = "[시니또용] 서비스 수락하기", description = "시니또가 안부전화 신청을 수락합니다.") @PutMapping("/accept/{callId}") - public ResponseEntity acceptHelloCall(@PathVariable Long callId) { - // 임시 응답 - return ResponseEntity.ok("안부전화 서비스가 수락되었습니다."); + public ResponseEntity acceptHelloCall(@MemberId Long memberId, @PathVariable Long callId) { + + helloCallService.acceptHelloCallBySinitto(memberId, callId); + + return ResponseEntity.ok(new StringMessageResponse("안부전화 서비스가 수락되었습니다.")); + } + + @Operation(summary = "[시니또용] 시니또가 수락한 안부전화 리스트 조회", description = "시니또가 수락한 안부전화 리스트를 조회합니다.") + @GetMapping("/own") + public ResponseEntity> readOwnHelloCallBySinitto(@MemberId Long memberId) { + + List helloCallResponses = helloCallService.readOwnHelloCallBySinitto(memberId); + + return ResponseEntity.ok(helloCallResponses); + } + + @Operation(summary = "[시니또용] 안부전화 서비스 시작시간 기록", description = "시니또가 안부전화 시작시간을 기록합니다.") + @PostMapping("/sinittos/start/{callId}") + public ResponseEntity writeHelloCallStartTimeBySinitto(@MemberId Long memberId, @PathVariable Long callId) { + + helloCallService.writeHelloCallStartTimeBySinitto(memberId, callId); + + return ResponseEntity.ok(new StringMessageResponse("안부전화 시작 시간이 기록되었습니다.")); } - @Operation(summary = "소통 보고서 작성", description = "시니또가 안부전화 후에 보고서를 작성합니다.") + @Operation(summary = "[시니또용] 안부전화 서비스 종료시간 기록", description = "시니또가 안부전화 종료시간을 기록합니다.") + @PostMapping("/sinittos/end/{callId}") + public ResponseEntity writeHelloCallEndTimeBySinitto(@MemberId Long memberId, @PathVariable Long callId) { + + helloCallService.writeHelloCallEndTimeBySinitto(memberId, callId); + + return ResponseEntity.ok(new StringMessageResponse("안부전화 종료 시간이 기록되었습니다.")); + } + + @Operation(summary = "[시니또용] 소통 보고서 작성 및 완료 대기 상태 변경", description = "시니또가 최종 안부전화 후에 보고서를 작성합니다.") @PostMapping("/reports") - public ResponseEntity createHelloCallReport(@RequestBody HelloCallReportRequest request) { - // 임시 응답 - return ResponseEntity.ok("소통 보고서가 작성되었습니다."); + public ResponseEntity createHelloCallReport(@MemberId Long memberId, @RequestBody HelloCallReportRequest request) { + + helloCallService.SendReportBySinitto(memberId, request); + + return ResponseEntity.status(HttpStatus.CREATED).body(new StringMessageResponse("소통 보고서가 작성되었습니다.")); } - @Operation(summary = "서비스 수행 완료", description = "시니또가 안부전화 수행을 완료합니다.") - @PutMapping("/complete/{callId}") - public ResponseEntity completeHelloCall(@PathVariable Long callId) { - // 임시 응답 - return ResponseEntity.ok("안부전화 서비스가 완료되었습니다."); + @Operation(summary = "[보호자용] 완료 대기 상태 안부전화 완료 처리", description = "보호자가 완료 대기 상태인 안부전화의 상태를 완료로 변경합니다.") + @PostMapping("/complete/{callId}") + public ResponseEntity completeHelloCall(@MemberId Long memberId, @PathVariable Long callId) { + + helloCallService.makeCompleteHelloCallByGuard(memberId, callId); + + return ResponseEntity.ok(new StringMessageResponse("보호자에 의해 해당 안부전화가 완료처리 되었습니다.")); } - @Operation(summary = "소통 보고서 조회", description = "보호자가 보고서 조회합니다.") - @GetMapping("/reports") - public ResponseEntity getHelloCallReports() { - // 임시 응답 - return ResponseEntity.ok(new HelloCallDetailResponse(null, null, null, null)); + @Operation(summary = "[보호자용] 안부전화 타임로그 조회", description = "보호자가 안부전화를 수행한 시니또의 전화 타임로그를 리스트로 조회합니다.") + @GetMapping("/guards/log/{callId}") + public ResponseEntity> readHelloCallTimeLogByGuard(@MemberId Long memberId, @PathVariable Long callId) { + + List helloCallTimeLogResponses = helloCallService.readHelloCallTimeLogByGuard(memberId, callId); + + return ResponseEntity.ok(helloCallTimeLogResponses); } - @Operation(summary = "소통 보고서 상세조회", description = "보호자가 보고서 상세정보를 조회합니다.") - @GetMapping("/reports/{reportId}") - public ResponseEntity getHelloCallReportDetail(@PathVariable Long reportId) { - // 임시 응답 - return ResponseEntity.ok(new HelloCallDetailResponse(null, null, null, null)); + + @Operation(summary = "[보호자용] 소통 보고서 조회", description = "보호자가 시니또가 작성한 보고서가 있다면 보고서를 조회합니다.") + @GetMapping("/reports/{callId}") + public ResponseEntity getHelloCallReportDetail(@MemberId Long memberId, @PathVariable Long callId) { + + HelloCallReportResponse helloCallReportResponse = helloCallService.readHelloCallReportByGuard(memberId, callId); + + return ResponseEntity.ok(helloCallReportResponse); } - @Operation(summary = "진행중인 안부 서비스 취소 요청", description = "시니또가 진행중인 안부전화 서비스를 취소합니다.") + @Operation(summary = "[시니또용] 진행중인 안부 서비스 취소 요청", description = "시니또가 진행중인 안부전화 서비스를 취소합니다. 취소시 포인트는 받을 수 없습니다.") @PutMapping("/cancel/{callId}") - public ResponseEntity cancelHelloCall(@PathVariable Long callId) { - // 임시 응답 - return ResponseEntity.ok("안부전화 서비스가 취소되었습니다."); + public ResponseEntity cancelHelloCall(@MemberId Long memberId, @PathVariable Long callId) { + + helloCallService.cancelHelloCallBySinitto(memberId, callId); + + return ResponseEntity.ok(new StringMessageResponse("안부전화 서비스가 취소되었습니다.")); } + @Operation(summary = "[관리자용] 모든 안부전화 리포트 조회", description = "관리자가 모든 안부전화의 리포트를 조회합니다.") + @GetMapping("/admin/reports") + public ResponseEntity> readAllHelloCallReportByAdmin() { + + List helloCallReportResponses = helloCallService.readAllHelloCallReportByAdmin(); + + return ResponseEntity.ok(helloCallReportResponses); + } } diff --git a/src/main/java/com/example/sinitto/helloCall/controller/HelloCallControllerAdvice.java b/src/main/java/com/example/sinitto/helloCall/controller/HelloCallControllerAdvice.java new file mode 100644 index 00000000..5fba1452 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/controller/HelloCallControllerAdvice.java @@ -0,0 +1,68 @@ +package com.example.sinitto.helloCall.controller; + +import com.example.sinitto.helloCall.exception.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ProblemDetail; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.net.URI; + +@RestControllerAdvice(basePackages = "com.example.sinitto.helloCall") +public class HelloCallControllerAdvice { + + @ExceptionHandler(TimeRuleException.class) + public ResponseEntity handleTimeRuleException(TimeRuleException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, + ex.getMessage()); + problemDetail.setType(URI.create("/errors/time-rule-exception")); + problemDetail.setTitle("Time Rule Exception"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); + } + + @ExceptionHandler(HelloCallAlreadyExistsException.class) + public ResponseEntity handleHelloCallAlreadyExistsException(HelloCallAlreadyExistsException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, + ex.getMessage()); + problemDetail.setType(URI.create("/errors/hello-call-already-exist")); + problemDetail.setTitle("Hello Call Already Exist"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(problemDetail); + } + + @ExceptionHandler(HelloCallNotFoundException.class) + public ResponseEntity handleHelloCallNotFoundException(HelloCallNotFoundException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, + ex.getMessage()); + problemDetail.setType(URI.create("/errors/hello-call-not-found")); + problemDetail.setTitle("Hello Call Not Found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail); + } + + @ExceptionHandler(InvalidStatusException.class) + public ResponseEntity handleInvalidStatusException(InvalidStatusException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, + ex.getMessage()); + problemDetail.setType(URI.create("/errors/invalid-status-exception")); + problemDetail.setTitle("Invalid Status Exception"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); + } + + @ExceptionHandler(CompletionConditionNotFulfilledException.class) + public ResponseEntity handleCompletionConditionNotFulfilledException(CompletionConditionNotFulfilledException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, + ex.getMessage()); + problemDetail.setType(URI.create("/errors/completion-condition-not-fulfilled")); + problemDetail.setTitle("Completion Condition Not Fulfilled"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); + } + + @ExceptionHandler(TimeLogSequenceException.class) + public ResponseEntity handleTimeLogSequenceException(TimeLogSequenceException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, + ex.getMessage()); + problemDetail.setType(URI.create("/errors/time-log-sequence-exception")); + problemDetail.setTitle("Time Log Sequence Exception"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetail); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailResponse.java index a5a177c0..0719bb6d 100644 --- a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailResponse.java +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailResponse.java @@ -1,20 +1,25 @@ package com.example.sinitto.helloCall.dto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; import java.time.LocalTime; -import java.util.Date; import java.util.List; public record HelloCallDetailResponse( - Date startDate, - Date endDate, + LocalDate startDate, + LocalDate endDate, List timeSlots, - String content + String requirement, + String seniorName, + String seniorPhoneNumber, + int price ) { - public record TimeSlot( - String day, + String dayName, + @JsonFormat(pattern = "kk:mm") LocalTime startTime, - LocalTime endTime, - int serviceTime) { + @JsonFormat(pattern = "kk:mm") + LocalTime endTime) { } } diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailUpdateRequest.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailUpdateRequest.java new file mode 100644 index 00000000..9297d4d7 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallDetailUpdateRequest.java @@ -0,0 +1,24 @@ +package com.example.sinitto.helloCall.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +public record HelloCallDetailUpdateRequest( + LocalDate startDate, + LocalDate endDate, + List timeSlots, + int price, + int serviceTime, + String requirement +) { + public record TimeSlot( + String dayName, + @JsonFormat(pattern = "kk:mm") + LocalTime startTime, + @JsonFormat(pattern = "kk:mm") + LocalTime endTime) { + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallPriceRequest.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallPriceRequest.java new file mode 100644 index 00000000..1ca35f49 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallPriceRequest.java @@ -0,0 +1,22 @@ +package com.example.sinitto.helloCall.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +public record HelloCallPriceRequest( + LocalDate startDate, + LocalDate endDate, + List timeSlots, + int serviceTime +) { + public record TimeSlot( + String dayName, + @JsonFormat(pattern = "kk:mm") + LocalTime startTime, + @JsonFormat(pattern = "kk:mm") + LocalTime endTime) { + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallPriceResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallPriceResponse.java new file mode 100644 index 00000000..0aa11951 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallPriceResponse.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.dto; + +public record HelloCallPriceResponse( + int price, + int totalServiceCount +) { +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportRequest.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportRequest.java index 23b18fad..93952c66 100644 --- a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportRequest.java +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportRequest.java @@ -1,20 +1,8 @@ package com.example.sinitto.helloCall.dto; -import java.time.LocalTime; -import java.util.Date; -import java.util.List; - public record HelloCallReportRequest( - Date startDate, - Date endDate, - List timeSlots, - String content -) { - public record TimeSlot( - String day, - LocalTime startTime, - LocalTime endTime, - int serviceTime) { - } + Long helloCallId, + String report +) { } diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportResponse.java new file mode 100644 index 00000000..06f33cd2 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallReportResponse.java @@ -0,0 +1,11 @@ +package com.example.sinitto.helloCall.dto; + +import java.time.LocalDate; + +public record HelloCallReportResponse( + LocalDate startDate, + LocalDate endDate, + String sinittoName, + String report +) { +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallRequest.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallRequest.java index ec11e618..427cf7de 100644 --- a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallRequest.java +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallRequest.java @@ -1,21 +1,25 @@ package com.example.sinitto.helloCall.dto; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDate; import java.time.LocalTime; -import java.util.Date; import java.util.List; public record HelloCallRequest( Long seniorId, - Date startDate, - Date endDate, + LocalDate startDate, + LocalDate endDate, List timeSlots, - String content + int price, + int serviceTime, + String requirement ) { - public record TimeSlot( - String day, + String dayName, + @JsonFormat(pattern = "kk:mm") LocalTime startTime, - LocalTime endTime, - int serviceTime) { + @JsonFormat(pattern = "kk:mm") + LocalTime endTime) { } } diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallResponse.java index 0c7387a6..fe710cd7 100644 --- a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallResponse.java +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallResponse.java @@ -1,7 +1,12 @@ package com.example.sinitto.helloCall.dto; +import com.example.sinitto.helloCall.entity.HelloCall; + +import java.util.List; + public record HelloCallResponse( Long helloCallId, String seniorName, - boolean[] days) { + List days, + HelloCall.Status status) { } diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallTimeLogResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallTimeLogResponse.java new file mode 100644 index 00000000..89e2bfb2 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallTimeLogResponse.java @@ -0,0 +1,10 @@ +package com.example.sinitto.helloCall.dto; + +import java.time.LocalDateTime; + +public record HelloCallTimeLogResponse( + String sinittoName, + LocalDateTime startTime, + LocalDateTime endTime +) { +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/HelloCallTimeResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallTimeResponse.java new file mode 100644 index 00000000..62c78f27 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/HelloCallTimeResponse.java @@ -0,0 +1,8 @@ +package com.example.sinitto.helloCall.dto; + +import java.time.LocalDateTime; + +public record HelloCallTimeResponse( + LocalDateTime DateAndTime +) { +} diff --git a/src/main/java/com/example/sinitto/helloCall/dto/StringMessageResponse.java b/src/main/java/com/example/sinitto/helloCall/dto/StringMessageResponse.java new file mode 100644 index 00000000..8046748b --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/dto/StringMessageResponse.java @@ -0,0 +1,6 @@ +package com.example.sinitto.helloCall.dto; + +public record StringMessageResponse( + String message +) { +} diff --git a/src/main/java/com/example/sinitto/helloCall/entity/HelloCall.java b/src/main/java/com/example/sinitto/helloCall/entity/HelloCall.java new file mode 100644 index 00000000..68baedfd --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/entity/HelloCall.java @@ -0,0 +1,207 @@ +package com.example.sinitto.helloCall.entity; + +import com.example.sinitto.auth.exception.UnauthorizedException; +import com.example.sinitto.helloCall.exception.InvalidStatusException; +import com.example.sinitto.helloCall.exception.TimeRuleException; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.entity.Sinitto; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class HelloCall { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @NotNull + private LocalDate startDate; + @NotNull + private LocalDate endDate; + @NotNull + private int price; + @NotNull + private int serviceTime; + @NotNull + private String requirement; + private String report; + @NotNull + @Enumerated(EnumType.STRING) + private HelloCall.Status status; + + @OneToOne + @JoinColumn(name = "senior_id") + @NotNull + @OnDelete(action = OnDeleteAction.CASCADE) + private Senior senior; + @OneToMany(mappedBy = "helloCall", cascade = CascadeType.REMOVE) + private List timeSlots = new ArrayList<>(); + @ManyToOne + @JoinColumn(name = "sinitto_id") + private Sinitto sinitto; + @OneToMany(mappedBy = "helloCall", cascade = CascadeType.REMOVE) + private List helloCallTimeLogs = new ArrayList<>(); + + public HelloCall(LocalDate startDate, LocalDate endDate, int price, int serviceTime, String requirement, Senior senior) { + if (startDate.isAfter(endDate)) { + throw new TimeRuleException("시작날짜가 종료날짜 이후일 수 없습니다."); + } + this.startDate = startDate; + this.endDate = endDate; + this.requirement = requirement; + this.status = Status.WAITING; + this.price = price; + this.serviceTime = serviceTime; + this.senior = senior; + } + + protected HelloCall() { + + } + + public Long getId() { + return id; + } + + public Senior getSenior() { + return senior; + } + + public List getTimeSlots() { + return timeSlots; + } + + public Status getStatus() { + return status; + } + + public LocalDate getStartDate() { + return startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public String getRequirement() { + return requirement; + } + + public int getPrice() { + return price; + } + + public int getServiceTime() { + return serviceTime; + } + + public Sinitto getSinitto() { + return sinitto; + } + + public void setSinitto(Sinitto sinitto) { + this.sinitto = sinitto; + } + + public String getReport() { + return report; + } + + public void setReport(String report) { + this.report = report; + } + + public String getSinittoName() { + return this.sinitto.getMember().getName(); + } + + public List getHelloCallTimeLogs() { + return helloCallTimeLogs; + } + + public boolean checkUnAuthorization(Member member) { + return !this.senior.getMember().equals(member); + } + + public void checkStatusIsWaiting() { + if (!this.status.equals(Status.WAITING)) { + throw new InvalidStatusException("안부전화 서비스가 수행 대기중일 때만 삭제가 가능합니다."); + } + } + + public void checkSiniitoIsSame(Sinitto sinitto) { + if (!this.sinitto.equals(sinitto)) { + throw new UnauthorizedException("안부전화 서비스 리포트를 작성할 권한이 없습니다."); + } + } + + public void checkGuardIsCorrect(Member member) { + if (!this.senior.getMember().equals(member)) { + throw new UnauthorizedException("해당 시니어의 안부전화를 신청한 보호자가 아닙니다."); + } + } + + public boolean checkIsNotAfterEndDate() { + return !(LocalDate.now().isAfter(this.endDate) || LocalDate.now().equals(this.endDate)); + } + + public boolean checkReportIsNotNull() { + return this.report != null; + } + + public void changeStatusToInProgress() { + if (!this.status.equals(Status.WAITING)) { + throw new InvalidStatusException("안부전화 서비스가 수행 대기중일 때만 진행중 상태로 변경할 수 있습니다. 현재 상태 : " + this.status); + } + this.status = Status.IN_PROGRESS; + } + + public void changeStatusToWaiting() { + if (!this.status.equals(Status.IN_PROGRESS)) { + throw new InvalidStatusException("안부전화 서비스가 수행중일 때만 진행중 상태로 변경할 수 있습니다. 현재 상태 : " + this.status); + } + this.status = Status.WAITING; + } + + public void changeStatusToPendingComplete() { + if (!this.status.equals(Status.IN_PROGRESS)) { + throw new InvalidStatusException("안부전화 서비스가 수행중일 때만 완료 대기 상태로 변경할 수 있습니다. 현재 상태 : " + this.status); + } + this.status = Status.PENDING_COMPLETE; + } + + public void changeStatusToComplete() { + if (!this.status.equals(Status.PENDING_COMPLETE)) { + throw new InvalidStatusException("안부전화 서비스가 완료 대기 일때만 완료 상태로 변경할 수 있습니다. 현재 상태 : " + this.status); + } + this.status = Status.COMPLETE; + } + + public void updateHelloCall(LocalDate startDate, LocalDate endDate, int price, int serviceTime, String requirement) { + if (!this.status.equals(Status.WAITING)) { + throw new InvalidStatusException("안부전화 서비스가 수행 대기중일 때만 수정이 가능합니다."); + } + if (startDate.isAfter(endDate)) { + throw new TimeRuleException("시작날짜가 종료날짜 이후일 수 없습니다."); + } + this.startDate = startDate; + this.endDate = endDate; + this.price = price; + this.serviceTime = serviceTime; + this.requirement = requirement; + } + + public enum Status { + WAITING, + IN_PROGRESS, + PENDING_COMPLETE, + COMPLETE + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/entity/HelloCallTimeLog.java b/src/main/java/com/example/sinitto/helloCall/entity/HelloCallTimeLog.java new file mode 100644 index 00000000..fd38b036 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/entity/HelloCallTimeLog.java @@ -0,0 +1,65 @@ +package com.example.sinitto.helloCall.entity; + +import com.example.sinitto.member.entity.Sinitto; +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +public class HelloCallTimeLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private LocalDateTime startDateAndTime; + private LocalDateTime endDateAndTime; + @ManyToOne + @JoinColumn(name = "helloCall_id") + private HelloCall helloCall; + @ManyToOne + @JoinColumn(name = "sinitto_id") + private Sinitto sinitto; + + public HelloCallTimeLog(HelloCall helloCall, Sinitto sinitto) { + this.helloCall = helloCall; + this.sinitto = sinitto; + } + + public HelloCallTimeLog(HelloCall helloCall, Sinitto sinitto, LocalDateTime startDateAndTime, LocalDateTime endDateAndTime) { + this.helloCall = helloCall; + this.startDateAndTime = startDateAndTime; + this.endDateAndTime = endDateAndTime; + this.sinitto = sinitto; + } + + protected HelloCallTimeLog() { + } + + public LocalDateTime getStartDateAndTime() { + return startDateAndTime; + } + + public void setStartDateAndTime(LocalDateTime startDateAndTime) { + this.startDateAndTime = startDateAndTime; + } + + public LocalDateTime getEndDateAndTime() { + return endDateAndTime; + } + + public void setEndDateAndTime(LocalDateTime endDateAndTime) { + this.endDateAndTime = endDateAndTime; + } + + public String getSinittoName() { + return this.sinitto.getMember().getName(); + } + + public HelloCall getHelloCall() { + return helloCall; + } + + public Sinitto getSinitto() { + return sinitto; + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/entity/TimeSlot.java b/src/main/java/com/example/sinitto/helloCall/entity/TimeSlot.java new file mode 100644 index 00000000..14c47735 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/entity/TimeSlot.java @@ -0,0 +1,62 @@ +package com.example.sinitto.helloCall.entity; + +import com.example.sinitto.helloCall.exception.TimeRuleException; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalTime; + +@Entity +public class TimeSlot { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @NotNull + private String dayName; + @NotNull + private LocalTime startTime; + @NotNull + private LocalTime endTime; + @ManyToOne + @JoinColumn(name = "hellocall_id") + private HelloCall helloCall; + + public TimeSlot(String dayName, LocalTime startTime, LocalTime endTime, HelloCall helloCall) { + if (startTime.isAfter(endTime)) { + throw new TimeRuleException("시작시간이 종료시간 이후일 수 없습니다."); + } + this.dayName = dayName; + this.startTime = startTime; + this.endTime = endTime; + this.helloCall = helloCall; + } + + protected TimeSlot() { + + } + + public String getDayName() { + return dayName; + } + + public LocalTime getStartTime() { + return startTime; + } + + public LocalTime getEndTime() { + return endTime; + } + + public HelloCall getHelloCall() { + return helloCall; + } + + public void updateTimeSlot(LocalTime startTime, LocalTime endTime) { + if (startTime.isAfter(endTime)) { + throw new TimeRuleException("시작시간이 종료시간 이후일 수 없습니다."); + } + this.startTime = startTime; + this.endTime = endTime; + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/exception/CompletionConditionNotFulfilledException.java b/src/main/java/com/example/sinitto/helloCall/exception/CompletionConditionNotFulfilledException.java new file mode 100644 index 00000000..1974b425 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/exception/CompletionConditionNotFulfilledException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.exception; + +public class CompletionConditionNotFulfilledException extends RuntimeException { + public CompletionConditionNotFulfilledException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/exception/HelloCallAlreadyExistsException.java b/src/main/java/com/example/sinitto/helloCall/exception/HelloCallAlreadyExistsException.java new file mode 100644 index 00000000..4d9d4b2f --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/exception/HelloCallAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.exception; + +public class HelloCallAlreadyExistsException extends RuntimeException { + public HelloCallAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/exception/HelloCallNotFoundException.java b/src/main/java/com/example/sinitto/helloCall/exception/HelloCallNotFoundException.java new file mode 100644 index 00000000..6f862114 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/exception/HelloCallNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.exception; + +public class HelloCallNotFoundException extends RuntimeException { + public HelloCallNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/exception/InvalidStatusException.java b/src/main/java/com/example/sinitto/helloCall/exception/InvalidStatusException.java new file mode 100644 index 00000000..013c8322 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/exception/InvalidStatusException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.exception; + +public class InvalidStatusException extends RuntimeException { + public InvalidStatusException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/exception/TimeLogSequenceException.java b/src/main/java/com/example/sinitto/helloCall/exception/TimeLogSequenceException.java new file mode 100644 index 00000000..22a1b854 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/exception/TimeLogSequenceException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.exception; + +public class TimeLogSequenceException extends RuntimeException { + public TimeLogSequenceException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/exception/TimeRuleException.java b/src/main/java/com/example/sinitto/helloCall/exception/TimeRuleException.java new file mode 100644 index 00000000..eb3756b5 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/exception/TimeRuleException.java @@ -0,0 +1,7 @@ +package com.example.sinitto.helloCall.exception; + +public class TimeRuleException extends RuntimeException { + public TimeRuleException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/repository/HelloCallRepository.java b/src/main/java/com/example/sinitto/helloCall/repository/HelloCallRepository.java new file mode 100644 index 00000000..f166ab30 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/repository/HelloCallRepository.java @@ -0,0 +1,22 @@ +package com.example.sinitto.helloCall.repository; + +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.entity.Sinitto; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface HelloCallRepository extends JpaRepository { + + Optional findBySenior(Senior senior); + + List findAllBySinitto(Sinitto sinitto); + + List findAllBySeniorIn(List seniors); + + boolean existsBySeniorAndStatusIn(Senior senior, List statuses); +} diff --git a/src/main/java/com/example/sinitto/helloCall/repository/HelloCallTimeLogRepository.java b/src/main/java/com/example/sinitto/helloCall/repository/HelloCallTimeLogRepository.java new file mode 100644 index 00000000..09c0dfb6 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/repository/HelloCallTimeLogRepository.java @@ -0,0 +1,17 @@ +package com.example.sinitto.helloCall.repository; + +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.HelloCallTimeLog; +import com.example.sinitto.member.entity.Sinitto; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface HelloCallTimeLogRepository extends JpaRepository { + Optional findBySinittoAndAndHelloCallId(Sinitto sinitto, Long helloCallId); + + List findAllByHelloCallId(Long helloCallId); + + Optional findTopBySinittoAndHelloCallOrderByStartDateAndTimeDesc(Sinitto sinitto, HelloCall helloCall); +} diff --git a/src/main/java/com/example/sinitto/helloCall/repository/TimeSlotRepository.java b/src/main/java/com/example/sinitto/helloCall/repository/TimeSlotRepository.java new file mode 100644 index 00000000..7c996143 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/repository/TimeSlotRepository.java @@ -0,0 +1,19 @@ +package com.example.sinitto.helloCall.repository; + +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.TimeSlot; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TimeSlotRepository extends JpaRepository { + + Optional findByHelloCallAndDayName(HelloCall helloCall, String dayName); + + void deleteAllByHelloCall(HelloCall helloCall); + + List findAllByHelloCall(HelloCall helloCall); +} diff --git a/src/main/java/com/example/sinitto/helloCall/service/HelloCallPriceService.java b/src/main/java/com/example/sinitto/helloCall/service/HelloCallPriceService.java new file mode 100644 index 00000000..59b5240d --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/service/HelloCallPriceService.java @@ -0,0 +1,66 @@ +package com.example.sinitto.helloCall.service; + +import com.example.sinitto.helloCall.dto.HelloCallPriceRequest; +import com.example.sinitto.helloCall.dto.HelloCallPriceResponse; +import org.springframework.stereotype.Service; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +@Service +public class HelloCallPriceService { + + private static final int PRICE_PER_ONE_MINUTE = 50; + + public HelloCallPriceResponse calculateHelloCallPrice(HelloCallPriceRequest helloCallPriceRequest) { + int totalServiceCount = calculateTotalServiceCount(helloCallPriceRequest); + int serviceTime = helloCallPriceRequest.serviceTime(); + + int price = totalServiceCount * serviceTime * PRICE_PER_ONE_MINUTE; + + return new HelloCallPriceResponse(price, totalServiceCount); + } + + public int calculateTotalServiceCount(HelloCallPriceRequest helloCallPriceRequest) { + int totalServiceCount = 0; + + LocalDate startDate = helloCallPriceRequest.startDate(); + LocalDate endDate = helloCallPriceRequest.endDate(); + + for (HelloCallPriceRequest.TimeSlot timeSlot : helloCallPriceRequest.timeSlots()) { + DayOfWeek targetDayOfWeek = convertDayStringToDayOfWeek(timeSlot.dayName()); + totalServiceCount += countOccurrencesOfDay(startDate, endDate, targetDayOfWeek); + } + + return totalServiceCount; + } + + private DayOfWeek convertDayStringToDayOfWeek(String dayName) { + return switch (dayName) { + case "월" -> DayOfWeek.MONDAY; + case "화" -> DayOfWeek.TUESDAY; + case "수" -> DayOfWeek.WEDNESDAY; + case "목" -> DayOfWeek.THURSDAY; + case "금" -> DayOfWeek.FRIDAY; + case "토" -> DayOfWeek.SATURDAY; + case "일" -> DayOfWeek.SUNDAY; + default -> throw new IllegalArgumentException("잘못된 dayName 입니다 : " + dayName); + }; + } + + private int countOccurrencesOfDay(LocalDate startDate, LocalDate endDate, DayOfWeek targetDayOfWeek) { + LocalDate firstOccurrence = findFirstOccurrenceOfDay(startDate, targetDayOfWeek); + + int occurrences = 0; + for (LocalDate date = firstOccurrence; !date.isAfter(endDate); date = date.plusWeeks(1)) { + occurrences++; + } + + return occurrences; + } + + private LocalDate findFirstOccurrenceOfDay(LocalDate startDate, DayOfWeek targetDayOfWeek) { + int daysToAdd = (targetDayOfWeek.getValue() - startDate.getDayOfWeek().getValue() + 7) % 7; + return startDate.plusDays(daysToAdd); + } +} diff --git a/src/main/java/com/example/sinitto/helloCall/service/HelloCallService.java b/src/main/java/com/example/sinitto/helloCall/service/HelloCallService.java new file mode 100644 index 00000000..307c3a61 --- /dev/null +++ b/src/main/java/com/example/sinitto/helloCall/service/HelloCallService.java @@ -0,0 +1,353 @@ +package com.example.sinitto.helloCall.service; + +import com.example.sinitto.auth.exception.UnauthorizedException; +import com.example.sinitto.guard.exception.SeniorNotFoundException; +import com.example.sinitto.guard.repository.SeniorRepository; +import com.example.sinitto.helloCall.dto.*; +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.HelloCallTimeLog; +import com.example.sinitto.helloCall.entity.TimeSlot; +import com.example.sinitto.helloCall.exception.CompletionConditionNotFulfilledException; +import com.example.sinitto.helloCall.exception.HelloCallAlreadyExistsException; +import com.example.sinitto.helloCall.exception.HelloCallNotFoundException; +import com.example.sinitto.helloCall.exception.TimeLogSequenceException; +import com.example.sinitto.helloCall.repository.HelloCallRepository; +import com.example.sinitto.helloCall.repository.HelloCallTimeLogRepository; +import com.example.sinitto.helloCall.repository.TimeSlotRepository; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.entity.Sinitto; +import com.example.sinitto.member.exception.MemberNotFoundException; +import com.example.sinitto.member.repository.MemberRepository; +import com.example.sinitto.sinitto.exception.SinittoNotFoundException; +import com.example.sinitto.sinitto.repository.SinittoRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class HelloCallService { + + private final HelloCallRepository helloCallRepository; + private final TimeSlotRepository timeSlotRepository; + private final SeniorRepository seniorRepository; + private final MemberRepository memberRepository; + private final SinittoRepository sinittoRepository; + private final HelloCallTimeLogRepository helloCallTimeLogRepository; + + + public HelloCallService(HelloCallRepository helloCallRepository, TimeSlotRepository timeSlotRepository, + SeniorRepository seniorRepository, MemberRepository memberRepository, SinittoRepository sinittoRepository, + HelloCallTimeLogRepository helloCallTimeLogRepository) { + this.helloCallRepository = helloCallRepository; + this.timeSlotRepository = timeSlotRepository; + this.seniorRepository = seniorRepository; + this.memberRepository = memberRepository; + this.sinittoRepository = sinittoRepository; + this.helloCallTimeLogRepository = helloCallTimeLogRepository; + } + + @Transactional + public void createHelloCallByGuard(Long memberId, HelloCallRequest helloCallRequest) { + Senior senior = seniorRepository.findByIdAndMemberId(helloCallRequest.seniorId(), memberId) + .orElseThrow(() -> new SeniorNotFoundException("시니어를 찾을 수 없습니다.")); + + if (helloCallRepository.existsBySeniorAndStatusIn(senior, List.of(HelloCall.Status.WAITING, HelloCall.Status.IN_PROGRESS))) { + throw new HelloCallAlreadyExistsException("이미 해당 시니어에게 할당되어 대기중 또는 진행중인 안부 전화 서비스가 존재합니다."); + } + + HelloCall helloCall = new HelloCall(helloCallRequest.startDate(), helloCallRequest.endDate(), + helloCallRequest.price(), helloCallRequest.serviceTime(), helloCallRequest.requirement(), senior); + HelloCall savedHelloCall = helloCallRepository.save(helloCall); + + for (HelloCallRequest.TimeSlot timeSlotRequest : helloCallRequest.timeSlots()) { + TimeSlot timeSlot = new TimeSlot(timeSlotRequest.dayName(), timeSlotRequest.startTime(), + timeSlotRequest.endTime(), savedHelloCall); + timeSlotRepository.save(timeSlot); + } + } + + @Transactional + public List readAllHelloCallsByGuard(Long memberId) { + List seniors = seniorRepository.findByMemberId(memberId); + + List helloCalls = helloCallRepository.findAllBySeniorIn(seniors); + + return helloCalls.stream() + .map(helloCall -> new HelloCallResponse( + helloCall.getId(), + helloCall.getSenior().getName(), + helloCall.getTimeSlots().stream().map(TimeSlot::getDayName).toList(), + helloCall.getStatus())) + .toList(); + } + + @Transactional + public Page readAllWaitingHelloCallsBySinitto(Pageable pageable) { + + List helloCalls = helloCallRepository.findAll(); + + List helloCallResponses = helloCalls.stream() + .filter(helloCall -> helloCall.getStatus().equals(HelloCall.Status.WAITING)) + .map(helloCall -> new HelloCallResponse( + helloCall.getId(), helloCall.getSenior().getName(), + helloCall.getTimeSlots().stream().map(TimeSlot::getDayName).toList(), helloCall.getStatus() + )).toList(); + + int totalElements = helloCallResponses.size(); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), totalElements); + + List pagedResponse = helloCallResponses.subList(start, end); + + return new PageImpl<>(pagedResponse, pageable, totalElements); + } + + @Transactional(readOnly = true) + public HelloCallDetailResponse readHelloCallDetail(Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + List timeSlots = helloCall.getTimeSlots().stream() + .map(timeSlot -> new HelloCallDetailResponse.TimeSlot( + timeSlot.getDayName(), timeSlot.getStartTime(), timeSlot.getEndTime())).toList(); + + return new HelloCallDetailResponse(helloCall.getStartDate(), helloCall.getEndDate(), + timeSlots, helloCall.getRequirement(), helloCall.getSenior().getName(), + helloCall.getSenior().getPhoneNumber(), helloCall.getPrice()); + } + + @Transactional + public void updateHelloCallByGuard(Long memberId, Long helloCallId, HelloCallDetailUpdateRequest helloCallDetailUpdateRequest) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException("id에 해당하는 멤버를 찾을 수 없습니다.")); + + if (helloCall.checkUnAuthorization(member)) { + throw new UnauthorizedException("안부전화 정보를 수정할 권한이 없습니다."); + } + + helloCall.updateHelloCall(helloCallDetailUpdateRequest.startDate(), helloCallDetailUpdateRequest.endDate(), + helloCallDetailUpdateRequest.price(), helloCallDetailUpdateRequest.serviceTime(), helloCallDetailUpdateRequest.requirement()); + + updateTimeSlots(helloCall, helloCallDetailUpdateRequest.timeSlots()); + } + + private void updateTimeSlots(HelloCall helloCall, List updatedTimeSlots) { + timeSlotRepository.deleteAllByHelloCall(helloCall); + helloCall.getTimeSlots().clear(); + + for (HelloCallDetailUpdateRequest.TimeSlot updatedSlot : updatedTimeSlots) { + TimeSlot newTimeSlot = new TimeSlot(updatedSlot.dayName(), updatedSlot.startTime(), updatedSlot.endTime(), helloCall); + timeSlotRepository.save(newTimeSlot); + helloCall.getTimeSlots().add(newTimeSlot); + } + } + + @Transactional + public void deleteHellCallByGuard(Long memberId, Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException("id에 해당하는 멤버를 찾을 수 없습니다.")); + + if (helloCall.checkUnAuthorization(member)) { + throw new UnauthorizedException("안부전화 신청을 취소할 권한이 없습니다."); + } + + helloCall.checkStatusIsWaiting(); + helloCallRepository.delete(helloCall); + } + + @Transactional(readOnly = true) + public List readHelloCallTimeLogByGuard(Long memberId, Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException("id에 해당하는 멤버를 찾을 수 없습니다.")); + + if (helloCall.checkUnAuthorization(member)) { + throw new UnauthorizedException("안부전화 로그를 조회할 권한이 없습니다."); + } + + List helloCallTimeLogs = helloCallTimeLogRepository.findAllByHelloCallId(helloCallId); + + List helloCallTimeLogResponses = new ArrayList<>(); + + for (HelloCallTimeLog helloCallTimeLog : helloCallTimeLogs) { + HelloCallTimeLogResponse response = new HelloCallTimeLogResponse(helloCallTimeLog.getSinittoName(), + helloCallTimeLog.getStartDateAndTime(), helloCallTimeLog.getEndDateAndTime()); + helloCallTimeLogResponses.add(response); + } + + return helloCallTimeLogResponses; + } + + @Transactional(readOnly = true) + public HelloCallReportResponse readHelloCallReportByGuard(Long memberId, Long helloCallId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException("id에 해당하는 멤버를 찾을 수 없습니다.")); + + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + helloCall.checkGuardIsCorrect(member); + + if (!helloCall.checkReportIsNotNull()) { + throw new CompletionConditionNotFulfilledException("아직 안부전화 서비스가 완료되지 않았습니다."); + } + + return new HelloCallReportResponse(helloCall.getStartDate(), + helloCall.getEndDate(), helloCall.getSinittoName(), helloCall.getReport()); + } + + @Transactional + public void makeCompleteHelloCallByGuard(Long memberId, Long helloCallId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException("id에 해당하는 멤버를 찾을 수 없습니다.")); + + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + helloCall.checkGuardIsCorrect(member); + + helloCall.changeStatusToComplete(); + + Sinitto earnedSinitto = helloCall.getSinitto(); + + //earnedSinitto에게 포인트 지급 로직 필요합니다. + } + + + @Transactional(readOnly = true) + public List readAllHelloCallReportByAdmin() { + List helloCalls = helloCallRepository.findAll(); + + List helloCallReportResponses = new ArrayList<>(); + + for (HelloCall helloCall : helloCalls) { + if (helloCall.checkReportIsNotNull()) { + HelloCallReportResponse response = new HelloCallReportResponse(helloCall.getStartDate(), + helloCall.getEndDate(), helloCall.getSinittoName(), helloCall.getReport()); + helloCallReportResponses.add(response); + } + } + return helloCallReportResponses; + } + + @Transactional + public void acceptHelloCallBySinitto(Long memberId, Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Sinitto sinitto = sinittoRepository.findByMemberId(memberId) + .orElseThrow(() -> new SinittoNotFoundException("id에 해당하는 시니또를 찾을 수 없습니다.")); + + helloCall.changeStatusToInProgress(); + helloCall.setSinitto(sinitto); + } + + @Transactional + public HelloCallTimeResponse writeHelloCallStartTimeBySinitto(Long memberId, Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Sinitto sinitto = sinittoRepository.findByMemberId(memberId) + .orElseThrow(() -> new SinittoNotFoundException("id에 해당하는 시니또를 찾을 수 없습니다.")); + + Optional recentLog = helloCallTimeLogRepository + .findTopBySinittoAndHelloCallOrderByStartDateAndTimeDesc(sinitto, helloCall); + + if (recentLog.isPresent() && recentLog.get().getEndDateAndTime() == null) { + throw new TimeLogSequenceException("이미 시작된 안부전화가 있습니다. 종료를 먼저 완료해주세요."); + } + + HelloCallTimeLog helloCallTimeLog = new HelloCallTimeLog(helloCall, sinitto); + helloCallTimeLog.setStartDateAndTime(LocalDateTime.now()); + + HelloCallTimeLog savedHelloCallTimeLog = helloCallTimeLogRepository.save(helloCallTimeLog); + return new HelloCallTimeResponse(savedHelloCallTimeLog.getStartDateAndTime()); + } + + @Transactional + public HelloCallTimeResponse writeHelloCallEndTimeBySinitto(Long memberId, Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Sinitto sinitto = sinittoRepository.findByMemberId(memberId) + .orElseThrow(() -> new SinittoNotFoundException("id에 해당하는 시니또를 찾을 수 없습니다.")); + + HelloCallTimeLog helloCallTimeLog = helloCallTimeLogRepository + .findTopBySinittoAndHelloCallOrderByStartDateAndTimeDesc(sinitto, helloCall) + .orElseThrow(() -> new HelloCallNotFoundException("안부전화 로그를 찾을 수 없습니다.")); + + if (helloCallTimeLog.getEndDateAndTime() != null) { + throw new TimeLogSequenceException("이미 종료된 안부전화입니다."); + } + + helloCallTimeLog.setEndDateAndTime(LocalDateTime.now()); + + return new HelloCallTimeResponse(helloCallTimeLog.getEndDateAndTime()); + } + + @Transactional + public void cancelHelloCallBySinitto(Long memberId, Long helloCallId) { + HelloCall helloCall = helloCallRepository.findById(helloCallId) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + if (!sinittoRepository.existsByMemberId(memberId)) { + throw new SinittoNotFoundException("id에 해당하는 시니또를 찾을 수 없습니다."); + } + + helloCall.changeStatusToWaiting(); + helloCall.setSinitto(null); + } + + @Transactional + public void SendReportBySinitto(Long memberId, HelloCallReportRequest helloCallReportRequest) { + HelloCall helloCall = helloCallRepository.findById(helloCallReportRequest.helloCallId()) + .orElseThrow(() -> new HelloCallNotFoundException("id에 해당하는 안부전화 정보를 찾을 수 없습니다.")); + + Sinitto sinitto = sinittoRepository.findByMemberId(memberId) + .orElseThrow(() -> new SinittoNotFoundException("id에 해당하는 시니또를 찾을 수 없습니다.")); + + helloCall.checkSiniitoIsSame(sinitto); + if (helloCall.checkIsNotAfterEndDate()) { + throw new CompletionConditionNotFulfilledException("서비스 종료 날짜보다 이른 날짜에 종료할 수 없습니다."); + } + + helloCall.setReport(helloCallReportRequest.report()); + helloCall.changeStatusToPendingComplete(); + } + + @Transactional(readOnly = true) + public List readOwnHelloCallBySinitto(Long memberId) { + Sinitto sinitto = sinittoRepository.findByMemberId(memberId) + .orElseThrow(() -> new SinittoNotFoundException("id에 해당하는 시니또를 찾을 수 없습니다.")); + + List helloCalls = helloCallRepository.findAllBySinitto(sinitto); + + List helloCallResponses = new ArrayList<>(); + + for (HelloCall helloCall : helloCalls) { + HelloCallResponse response = new HelloCallResponse(helloCall.getId(), helloCall.getSenior().getName(), + helloCall.getTimeSlots().stream().map(TimeSlot::getDayName).toList(), helloCall.getStatus()); + helloCallResponses.add(response); + } + + return helloCallResponses; + } + + +} diff --git a/src/main/java/com/example/sinitto/member/controller/MemberController.java b/src/main/java/com/example/sinitto/member/controller/MemberController.java index 79603825..21108fc6 100644 --- a/src/main/java/com/example/sinitto/member/controller/MemberController.java +++ b/src/main/java/com/example/sinitto/member/controller/MemberController.java @@ -1,6 +1,7 @@ package com.example.sinitto.member.controller; -import com.example.sinitto.auth.dto.RegisterResponse; +import com.example.sinitto.member.dto.RegisterResponse; +import com.example.sinitto.common.annotation.MemberId; import com.example.sinitto.member.dto.SignupRequest; import com.example.sinitto.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; @@ -8,10 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/members") @@ -39,4 +37,11 @@ public ResponseEntity guardSignup(@RequestBody SignupRequest r request.email(), request.isSinitto()); return ResponseEntity.status(HttpStatus.CREATED).body(registerResponse); } + + @Operation(summary = "멤버 로그아웃", description = "레디스에 저장되어있는 멤버의 refreshToken을 삭제합니다.") + @DeleteMapping("/logout") + public ResponseEntity memberLogout(@MemberId Long memberId) { + memberService.memberLogout(memberId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/example/sinitto/auth/dto/RegisterResponse.java b/src/main/java/com/example/sinitto/member/dto/RegisterResponse.java similarity index 75% rename from src/main/java/com/example/sinitto/auth/dto/RegisterResponse.java rename to src/main/java/com/example/sinitto/member/dto/RegisterResponse.java index 11f943da..71665400 100644 --- a/src/main/java/com/example/sinitto/auth/dto/RegisterResponse.java +++ b/src/main/java/com/example/sinitto/member/dto/RegisterResponse.java @@ -1,4 +1,4 @@ -package com.example.sinitto.auth.dto; +package com.example.sinitto.member.dto; public record RegisterResponse( String accessToken, diff --git a/src/main/java/com/example/sinitto/member/entity/Member.java b/src/main/java/com/example/sinitto/member/entity/Member.java index f567386b..29139b03 100644 --- a/src/main/java/com/example/sinitto/member/entity/Member.java +++ b/src/main/java/com/example/sinitto/member/entity/Member.java @@ -31,12 +31,12 @@ public Member(String name, String phoneNumber, String email, boolean isSinitto) protected Member() { } - public void updateMember(String name, String email, String phoneNumber){ + public void updateMember(String name, String email, String phoneNumber) { this.name = name; this.email = email; this.phoneNumber = phoneNumber; } - + public Long getId() { return id; } diff --git a/src/main/java/com/example/sinitto/member/entity/Senior.java b/src/main/java/com/example/sinitto/member/entity/Senior.java index be1ffeb3..bba956e3 100644 --- a/src/main/java/com/example/sinitto/member/entity/Senior.java +++ b/src/main/java/com/example/sinitto/member/entity/Senior.java @@ -22,18 +22,21 @@ public class Senior { @OnDelete(action = OnDeleteAction.CASCADE) private Member member; - public Senior(String name, String phoneNumber, Member member){ + public Senior(String name, String phoneNumber, Member member) { this.name = name; this.phoneNumber = phoneNumber; this.member = member; } - public void updateSenior(String name, String phoneNumber){ + protected Senior() { + } + + public void updateSenior(String name, String phoneNumber) { this.name = name; this.phoneNumber = phoneNumber; } - public SeniorResponse mapToResponse(){ + public SeniorResponse mapToResponse() { return new SeniorResponse(this.id, this.name, this.phoneNumber); } diff --git a/src/main/java/com/example/sinitto/member/repository/MemberRepository.java b/src/main/java/com/example/sinitto/member/repository/MemberRepository.java index e4a9a165..63870e1e 100644 --- a/src/main/java/com/example/sinitto/member/repository/MemberRepository.java +++ b/src/main/java/com/example/sinitto/member/repository/MemberRepository.java @@ -11,6 +11,8 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + boolean existsByEmail(String email); + List findByIsSinitto(boolean isSinitto); -} \ No newline at end of file +} diff --git a/src/main/java/com/example/sinitto/member/service/MemberService.java b/src/main/java/com/example/sinitto/member/service/MemberService.java index 62b497e2..324f0a6f 100644 --- a/src/main/java/com/example/sinitto/member/service/MemberService.java +++ b/src/main/java/com/example/sinitto/member/service/MemberService.java @@ -3,15 +3,16 @@ import com.example.sinitto.auth.dto.KakaoTokenResponse; import com.example.sinitto.auth.dto.KakaoUserResponse; import com.example.sinitto.auth.dto.LoginResponse; -import com.example.sinitto.auth.dto.RegisterResponse; import com.example.sinitto.auth.service.KakaoApiService; import com.example.sinitto.auth.service.KakaoTokenService; import com.example.sinitto.auth.service.TokenService; import com.example.sinitto.common.resolver.MemberIdProvider; +import com.example.sinitto.member.dto.RegisterResponse; import com.example.sinitto.member.entity.Member; import com.example.sinitto.member.exception.MemberNotFoundException; import com.example.sinitto.member.exception.NotUniqueException; import com.example.sinitto.member.repository.MemberRepository; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.Optional; @@ -23,12 +24,16 @@ public class MemberService implements MemberIdProvider { private final TokenService tokenService; private final KakaoApiService kakaoApiService; private final KakaoTokenService kakaoTokenService; + private final RedisTemplate redisTemplate; - public MemberService(MemberRepository memberRepository, TokenService tokenService, KakaoApiService kakaoApiService, KakaoTokenService kakaoTokenService) { + + public MemberService(MemberRepository memberRepository, TokenService tokenService, KakaoApiService kakaoApiService, KakaoTokenService kakaoTokenService + ,RedisTemplate redisTemplate) { this.memberRepository = memberRepository; this.tokenService = tokenService; this.kakaoApiService = kakaoApiService; this.kakaoTokenService = kakaoTokenService; + this.redisTemplate = redisTemplate; } @Override @@ -51,14 +56,14 @@ public LoginResponse kakaoLogin(String authorizationCode) { Optional optionalMember = memberRepository.findByEmail(email); if (optionalMember.isEmpty()) { - return new LoginResponse(null, null, "/signup", email, false); + return new LoginResponse(null, null, "/signup", email, false, false); } Member member = optionalMember.get(); String accessToken = tokenService.generateAccessToken(email); String refreshToken = tokenService.generateRefreshToken(email); - return new LoginResponse(accessToken, refreshToken, null, null, member.isSinitto()); + return new LoginResponse(accessToken, refreshToken, null, null, member.isSinitto(), true); } public RegisterResponse registerNewMember(String name, String phoneNumber, String email, boolean isSinitto) { @@ -75,4 +80,15 @@ public RegisterResponse registerNewMember(String name, String phoneNumber, Strin return new RegisterResponse(accessToken, refreshToken, isSinitto); } + + public void memberLogout(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException("id에 해당하는 멤버가 없습니다.")); + + String storedRefreshToken = redisTemplate.opsForValue().get(member.getEmail()); + + if (storedRefreshToken != null) { + redisTemplate.delete(member.getEmail()); + } + } } diff --git a/src/main/java/com/example/sinitto/review/controller/ReviewController.java b/src/main/java/com/example/sinitto/review/controller/ReviewController.java index 326a6880..ad35d427 100644 --- a/src/main/java/com/example/sinitto/review/controller/ReviewController.java +++ b/src/main/java/com/example/sinitto/review/controller/ReviewController.java @@ -1,7 +1,9 @@ package com.example.sinitto.review.controller; +import com.example.sinitto.common.annotation.MemberId; import com.example.sinitto.review.dto.ReviewRequest; import com.example.sinitto.review.dto.ReviewResponse; +import com.example.sinitto.review.service.ReviewService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; @@ -12,20 +14,24 @@ @RestController @RequestMapping("/api/reviews") -@Tag(name = "[미구현]리뷰", description = "서비스 리뷰 관련 API") +@Tag(name = "리뷰", description = "서비스 리뷰 관련 API") public class ReviewController { + private final ReviewService reviewService; + + public ReviewController(ReviewService reviewService) { + this.reviewService = reviewService; + } @Operation(summary = "서비스 리뷰 리스트 보기", description = "서비스 리뷰를 리스트 형태로 보여줍니다.") @GetMapping public ResponseEntity> getReviewList() { - // 임시 응답 - return ResponseEntity.ok(new ArrayList<>()); + return ResponseEntity.ok(reviewService.readAllReviews()); } @Operation(summary = "서비스 리뷰 및 평가 제출", description = "시니또에 대한 별점과 작성한 평가 내용(선택사항)을 제출합니다.") @PostMapping - public ResponseEntity submitReview(@RequestBody ReviewRequest request) { - // 임시 응답 + public ResponseEntity submitReview(@MemberId Long memberId, @RequestBody ReviewRequest reviewRequest) { + reviewService.createReview(memberId, reviewRequest); return ResponseEntity.ok("리뷰가 성공적으로 제출되었습니다."); } } diff --git a/src/main/java/com/example/sinitto/review/dto/ReviewRequest.java b/src/main/java/com/example/sinitto/review/dto/ReviewRequest.java index 335422df..f6c6139c 100644 --- a/src/main/java/com/example/sinitto/review/dto/ReviewRequest.java +++ b/src/main/java/com/example/sinitto/review/dto/ReviewRequest.java @@ -1,6 +1,8 @@ package com.example.sinitto.review.dto; public record ReviewRequest( - int starCount, + int starCountForRequest, + int starCountForService, + int starCountForSatisfaction, String content) { } diff --git a/src/main/java/com/example/sinitto/review/dto/ReviewResponse.java b/src/main/java/com/example/sinitto/review/dto/ReviewResponse.java index 0fe0e2d6..0e9fb759 100644 --- a/src/main/java/com/example/sinitto/review/dto/ReviewResponse.java +++ b/src/main/java/com/example/sinitto/review/dto/ReviewResponse.java @@ -1,10 +1,10 @@ package com.example.sinitto.review.dto; -import java.util.Date; +import java.time.LocalDate; public record ReviewResponse( String name, - int starCount, - Date postDate, + double averageStarCount, + LocalDate postDate, String content) { } diff --git a/src/main/java/com/example/sinitto/review/entity/Review.java b/src/main/java/com/example/sinitto/review/entity/Review.java new file mode 100644 index 00000000..1c1f69fb --- /dev/null +++ b/src/main/java/com/example/sinitto/review/entity/Review.java @@ -0,0 +1,86 @@ +package com.example.sinitto.review.entity; + +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.review.dto.ReviewResponse; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDate; + +@Entity +@EntityListeners(AuditingEntityListener.class) +public class Review { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + int starCountForRequest; + + @NotNull + int starCountForService; + + @NotNull + int starCountForSatisfaction; + + @CreatedDate + LocalDate postDate; + + String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + @NotNull + @OnDelete(action = OnDeleteAction.CASCADE) + private Member member; + + public Review(int starCountForRequest, int starCountForService, int starCountForSatisfaction, String content, Member member) { + this.starCountForRequest = starCountForRequest; + this.starCountForService = starCountForService; + this.starCountForSatisfaction = starCountForSatisfaction; + this.content = content; + this.member = member; + } + + public Review() { + } + + public ReviewResponse mapToResponse() { + double averageStarCount = (double) Math.round((double) ((starCountForRequest + + starCountForSatisfaction + + starCountForRequest) / 3) * 100) / 100; + return new ReviewResponse(this.member.getName(), averageStarCount, this.postDate, this.content); + } + + public Long getId() { + return id; + } + + public int getStarCountForRequest() { + return starCountForRequest; + } + + public int getStarCountForService() { + return starCountForService; + } + + public int getStarCountForSatisfaction() { + return starCountForSatisfaction; + } + + public LocalDate getPostDate() { + return postDate; + } + + public String getContent() { + return content; + } + + public Member getMember() { + return member; + } +} diff --git a/src/main/java/com/example/sinitto/review/repository/ReviewRepository.java b/src/main/java/com/example/sinitto/review/repository/ReviewRepository.java new file mode 100644 index 00000000..5ec682c2 --- /dev/null +++ b/src/main/java/com/example/sinitto/review/repository/ReviewRepository.java @@ -0,0 +1,9 @@ +package com.example.sinitto.review.repository; + +import com.example.sinitto.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReviewRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/sinitto/review/service/ReviewService.java b/src/main/java/com/example/sinitto/review/service/ReviewService.java new file mode 100644 index 00000000..4db11a55 --- /dev/null +++ b/src/main/java/com/example/sinitto/review/service/ReviewService.java @@ -0,0 +1,45 @@ +package com.example.sinitto.review.service; + +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.exception.MemberNotFoundException; +import com.example.sinitto.member.repository.MemberRepository; +import com.example.sinitto.review.dto.ReviewRequest; +import com.example.sinitto.review.dto.ReviewResponse; +import com.example.sinitto.review.entity.Review; +import com.example.sinitto.review.repository.ReviewRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class ReviewService { + private final ReviewRepository reviewRepository; + private final MemberRepository memberRepository; + + public ReviewService(ReviewRepository reviewRepository, MemberRepository memberRepository) { + this.reviewRepository = reviewRepository; + this.memberRepository = memberRepository; + } + + @Transactional + public List readAllReviews() { + List reviewList = reviewRepository.findAll(); + + return reviewList.stream().map(Review::mapToResponse).toList(); + } + + @Transactional + public void createReview(Long memberId, ReviewRequest reviewRequest) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") + ); + Review review = new Review(reviewRequest.starCountForRequest(), + reviewRequest.starCountForService(), + reviewRequest.starCountForSatisfaction(), + reviewRequest.content(), + member); + + reviewRepository.save(review); + } +} diff --git a/src/main/java/com/example/sinitto/sinitto/controller/SinittoController.java b/src/main/java/com/example/sinitto/sinitto/controller/SinittoController.java index d523c559..6c0a8a6a 100644 --- a/src/main/java/com/example/sinitto/sinitto/controller/SinittoController.java +++ b/src/main/java/com/example/sinitto/sinitto/controller/SinittoController.java @@ -1,7 +1,6 @@ package com.example.sinitto.sinitto.controller; import com.example.sinitto.common.annotation.MemberId; -import com.example.sinitto.guard.dto.SeniorRequest; import com.example.sinitto.sinitto.dto.SinittoBankRequest; import com.example.sinitto.sinitto.dto.SinittoRequest; import com.example.sinitto.sinitto.dto.SinittoResponse; diff --git a/src/main/java/com/example/sinitto/sinitto/controller/SinittoControllerAdvice.java b/src/main/java/com/example/sinitto/sinitto/controller/SinittoControllerAdvice.java index cb5e488e..6fee49bb 100644 --- a/src/main/java/com/example/sinitto/sinitto/controller/SinittoControllerAdvice.java +++ b/src/main/java/com/example/sinitto/sinitto/controller/SinittoControllerAdvice.java @@ -6,6 +6,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; + import java.net.URI; @RestControllerAdvice(basePackages = "com.example.sinitto.sinitto") diff --git a/src/main/java/com/example/sinitto/sinitto/exception/SinittoNotFoundException.java b/src/main/java/com/example/sinitto/sinitto/exception/SinittoNotFoundException.java index a5af05c5..50923b49 100644 --- a/src/main/java/com/example/sinitto/sinitto/exception/SinittoNotFoundException.java +++ b/src/main/java/com/example/sinitto/sinitto/exception/SinittoNotFoundException.java @@ -1,5 +1,7 @@ package com.example.sinitto.sinitto.exception; -public class SinittoNotFoundException extends RuntimeException{ - public SinittoNotFoundException(String message){super(message);} +public class SinittoNotFoundException extends RuntimeException { + public SinittoNotFoundException(String message) { + super(message); + } } diff --git a/src/main/java/com/example/sinitto/sinitto/repository/SinittoRepository.java b/src/main/java/com/example/sinitto/sinitto/repository/SinittoRepository.java index 4c6531fb..1c211fcd 100644 --- a/src/main/java/com/example/sinitto/sinitto/repository/SinittoRepository.java +++ b/src/main/java/com/example/sinitto/sinitto/repository/SinittoRepository.java @@ -1,9 +1,8 @@ package com.example.sinitto.sinitto.repository; -import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Sinitto; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import com.example.sinitto.member.entity.Sinitto; import java.util.Optional; @@ -12,4 +11,6 @@ public interface SinittoRepository extends JpaRepository { Optional findByMemberId(Long memberId); + boolean existsByMemberId(Long memberId); + } diff --git a/src/main/java/com/example/sinitto/sinitto/service/SinittoService.java b/src/main/java/com/example/sinitto/sinitto/service/SinittoService.java index 2ddb80e9..c423177f 100644 --- a/src/main/java/com/example/sinitto/sinitto/service/SinittoService.java +++ b/src/main/java/com/example/sinitto/sinitto/service/SinittoService.java @@ -9,9 +9,8 @@ import com.example.sinitto.sinitto.dto.SinittoResponse; import com.example.sinitto.sinitto.exception.SinittoNotFoundException; import com.example.sinitto.sinitto.repository.SinittoRepository; -import org.springframework.transaction.annotation.Transactional; - import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -28,7 +27,7 @@ public SinittoService(MemberRepository memberRepository, SinittoRepository sinit @Transactional - public void createSinittoBankInfo(Long memberId, SinittoBankRequest sinittoBankRequest){ + public void createSinittoBankInfo(Long memberId, SinittoBankRequest sinittoBankRequest) { Member member = memberRepository.findById(memberId).orElseThrow( () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); @@ -37,7 +36,7 @@ public void createSinittoBankInfo(Long memberId, SinittoBankRequest sinittoBankR } @Transactional(readOnly = true) - public SinittoResponse readSinitto(Long memberId){ + public SinittoResponse readSinitto(Long memberId) { Member member = memberRepository.findById(memberId).orElseThrow( () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); @@ -48,43 +47,43 @@ public SinittoResponse readSinitto(Long memberId){ } @Transactional - public void updateSinitto(Long memberId, SinittoRequest sinittoRequest){ + public void updateSinitto(Long memberId, SinittoRequest sinittoRequest) { Member member = memberRepository.findById(memberId).orElseThrow( - ()->new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") + () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); member.updateMember(sinittoRequest.name(), sinittoRequest.email(), sinittoRequest.phoneNumber()); } @Transactional - public void updateSinittoBankInfo(Long memberId, SinittoBankRequest sinittoBankRequest){ + public void updateSinittoBankInfo(Long memberId, SinittoBankRequest sinittoBankRequest) { Sinitto sinitto = sinittoRepository.findByMemberId(memberId).orElseThrow( - ()-> new SinittoNotFoundException("이메일에 해당하는 멤버의 계좌정보를 찾을 수 없습니다.") + () -> new SinittoNotFoundException("이메일에 해당하는 멤버의 계좌정보를 찾을 수 없습니다.") ); sinitto.updateSinitto(sinittoBankRequest.bankName(), sinittoBankRequest.accountNumber()); } @Transactional - public void deleteSinitto(Long memberId){ + public void deleteSinitto(Long memberId) { Member member = memberRepository.findById(memberId).orElseThrow( - ()->new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") + () -> new MemberNotFoundException("이메일에 해당하는 멤버를 찾을 수 없습니다.") ); memberRepository.delete(member); } @Transactional - public void deleteSinittoBankInfo(Long memberId){ + public void deleteSinittoBankInfo(Long memberId) { Sinitto sinitto = sinittoRepository.findByMemberId(memberId).orElseThrow( - ()->new SinittoNotFoundException("이메일에 해당하는 멤버의 계좌정보를 찾을 수 없습니다.") + () -> new SinittoNotFoundException("이메일에 해당하는 멤버의 계좌정보를 찾을 수 없습니다.") ); sinittoRepository.delete(sinitto); } @Transactional - public List readAllSinitto(){ + public List readAllSinitto() { List sinittos = sinittoRepository.findAll(); return sinittos.stream() - .map(m -> new SinittoResponse(m.getMember().getName(), m.getMember().getPhoneNumber(), m.getMember().getEmail(), m.getAccountNumber(),m.getBankName())) + .map(m -> new SinittoResponse(m.getMember().getName(), m.getMember().getPhoneNumber(), m.getMember().getEmail(), m.getAccountNumber(), m.getBankName())) .toList(); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7263f9a6..bfa68ed5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,6 +2,7 @@ spring.application.name=sinnitto-example spring.profiles.include=dev spring.h2.console.enabled=true spring.h2.console.path=/h2-console +spring.h2.console.settings.web-allow-others=true spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.username=sa spring.datasource.driver-class-name=org.h2.Driver @@ -9,3 +10,6 @@ spring.jpa.properties.hibernate.format_sql=true spring.jpa.show-sql=true logging.level.org.hibernate.orm.jdbc.bind=TRACE spring.profiles.active=dev +spring.data.redis.host=localhost +spring.data.redis.port=6379 +spring.data.redis.password=sinitto diff --git a/src/test/java/com/example/sinitto/callback/entity/CallbackTest.java b/src/test/java/com/example/sinitto/callback/entity/CallbackTest.java index 6b674460..d2fca9be 100644 --- a/src/test/java/com/example/sinitto/callback/entity/CallbackTest.java +++ b/src/test/java/com/example/sinitto/callback/entity/CallbackTest.java @@ -2,6 +2,7 @@ import com.example.sinitto.callback.exception.AlreadyCompleteException; import com.example.sinitto.callback.exception.AlreadyInProgressException; +import com.example.sinitto.callback.exception.AlreadyPendingCompleteException; import com.example.sinitto.callback.exception.AlreadyWaitingException; import com.example.sinitto.callback.repository.CallbackRepository; import com.example.sinitto.guard.repository.SeniorRepository; @@ -49,14 +50,14 @@ void changeStatusToInProgress() { } @Test - @DisplayName("Complete -> In Progress - 예외 발생") + @DisplayName("Pending Complete -> In Progress - 예외 발생") void changeStatusToInProgress_fail1() { //given testCallback.changeStatusToInProgress(); - testCallback.changeStatusToComplete(); + testCallback.changeStatusToPendingComplete(); //when then - assertThrows(AlreadyCompleteException.class, () -> testCallback.changeStatusToInProgress()); + assertThrows(AlreadyPendingCompleteException.class, () -> testCallback.changeStatusToInProgress()); } @Test @@ -70,34 +71,34 @@ void changeStatusToInProgress_fail2() { } @Test - @DisplayName("In Progress -> Complete - 성공") - void changeStatusToComplete() { + @DisplayName("In Progress -> Pending Complete - 성공") + void changeStatusToPendingComplete() { //given testCallback.changeStatusToInProgress(); //when - testCallback.changeStatusToComplete(); + testCallback.changeStatusToPendingComplete(); //then - assertEquals(testCallback.getStatus(), Callback.Status.COMPLETE.name()); + assertEquals(testCallback.getStatus(), Callback.Status.PENDING_COMPLETE.name()); } @Test - @DisplayName("Waiting -> Complete - 예외 발생") - void changeStatusToComplete_fail1() { + @DisplayName("Waiting -> Pending Complete - 예외 발생") + void changeStatusToPendingComplete_fail1() { //when then - assertThrows(AlreadyWaitingException.class, () -> testCallback.changeStatusToComplete()); + assertThrows(AlreadyWaitingException.class, () -> testCallback.changeStatusToPendingComplete()); } @Test - @DisplayName("Complete -> Complete - 예외 발생") - void changeStatusToComplete_fail2() { + @DisplayName("Pending Complete -> Pending Complete - 예외 발생") + void changeStatusToPendingComplete_fail2() { //given testCallback.changeStatusToInProgress(); - testCallback.changeStatusToComplete(); + testCallback.changeStatusToPendingComplete(); //when then - assertThrows(AlreadyCompleteException.class, () -> testCallback.changeStatusToComplete()); + assertThrows(AlreadyPendingCompleteException.class, () -> testCallback.changeStatusToPendingComplete()); } @Test @@ -121,14 +122,14 @@ void changeStatusToWaiting_fain1() { } @Test - @DisplayName("Complete -> Waiting - 예외 발생") + @DisplayName("Pending Complete -> Waiting - 예외 발생") void changeStatusToWaiting_fail2() { //given testCallback.changeStatusToInProgress(); - testCallback.changeStatusToComplete(); + testCallback.changeStatusToPendingComplete(); //when then - assertThrows(AlreadyCompleteException.class, () -> testCallback.changeStatusToWaiting()); + assertThrows(AlreadyPendingCompleteException.class, () -> testCallback.changeStatusToWaiting()); } @Test @@ -155,10 +156,22 @@ void checkAssignedMemberId_fail1() { } @Test - @DisplayName("Complete 상태에서 멤버Id 할당 - 예외 발생") + @DisplayName("Pending Complete 상태에서 멤버Id 할당 - 예외 발생") void checkAssignedMemberId_fail2() { //given testCallback.changeStatusToInProgress(); + testCallback.changeStatusToPendingComplete(); + + //when then + assertThrows(AlreadyPendingCompleteException.class, () -> testCallback.assignMember(3L)); + } + + @Test + @DisplayName("Complete 상태에서 멤버Id 할당 - 예외 발생") + void checkAssignedMemberId_fail3() { + //given + testCallback.changeStatusToInProgress(); + testCallback.changeStatusToPendingComplete(); testCallback.changeStatusToComplete(); //when then @@ -181,14 +194,14 @@ void cancelAssignment() { } @Test - @DisplayName("Complete 상태에서 할당 취소 - 예외 발생") + @DisplayName("Pending Complete 상태에서 할당 취소 - 예외 발생") void cancelAssignment_fail1() { //given testCallback.changeStatusToInProgress(); - testCallback.changeStatusToComplete(); + testCallback.changeStatusToPendingComplete(); //when then - assertThrows(AlreadyCompleteException.class, () -> testCallback.cancelAssignment()); + assertThrows(AlreadyPendingCompleteException.class, () -> testCallback.cancelAssignment()); } @Test @@ -197,4 +210,58 @@ void cancelAssignment_fail2() { //when then assertThrows(AlreadyWaitingException.class, () -> testCallback.cancelAssignment()); } + + @Test + @DisplayName("Complete 상태에서 할당 취소 - 예외 발생") + void cancelAssignment_fail3() { + //given + testCallback.changeStatusToInProgress(); + testCallback.changeStatusToPendingComplete(); + testCallback.changeStatusToComplete(); + + //when then + assertThrows(AlreadyCompleteException.class, () -> testCallback.cancelAssignment()); + } + + @Test + @DisplayName("Pending Complete -> Complete - 성공") + void changeStatusToComplete() { + //when + testCallback.changeStatusToInProgress(); + testCallback.changeStatusToPendingComplete(); + testCallback.changeStatusToComplete(); + + //then + assertEquals(testCallback.getStatus(), Callback.Status.COMPLETE.name()); + } + + @Test + @DisplayName("Complete -> Complete - 예외 발생") + void changeStatusToComplete_fail() { + //when + testCallback.changeStatusToInProgress(); + testCallback.changeStatusToPendingComplete(); + testCallback.changeStatusToComplete(); + + //then + assertThrows(AlreadyCompleteException.class, () -> testCallback.changeStatusToComplete()); + } + + @Test + @DisplayName("Waiting -> Complete - 예외 발생") + void changeStatusToComplete_fail1() { + //when then + assertThrows(AlreadyWaitingException.class, () -> testCallback.changeStatusToComplete()); + } + + @Test + @DisplayName("In Progress -> Complete - 예외 발생") + void changeStatusToComplete_fail2() { + //given + testCallback.changeStatusToInProgress(); + + //when then + assertThrows(AlreadyInProgressException.class, () -> testCallback.changeStatusToComplete()); + } + } diff --git a/src/test/java/com/example/sinitto/callback/repository/CallbackRepositoryTest.java b/src/test/java/com/example/sinitto/callback/repository/CallbackRepositoryTest.java new file mode 100644 index 00000000..8de07246 --- /dev/null +++ b/src/test/java/com/example/sinitto/callback/repository/CallbackRepositoryTest.java @@ -0,0 +1,100 @@ +package com.example.sinitto.callback.repository; + +import com.example.sinitto.callback.entity.Callback; +import com.example.sinitto.guard.repository.SeniorRepository; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.repository.MemberRepository; +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 java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class CallbackRepositoryTest { + + @Autowired + private CallbackRepository callbackRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private SeniorRepository seniorRepository; + private Callback testCallback; + private Long memberId; + + @BeforeEach + void setUp() { + Member testMember = new Member("member", "01043214321", "tjdgns5506@gmai.com", true); + memberRepository.save(testMember); + + memberId = testMember.getId(); + + Senior testSenior = new Senior("senior", "01012341234", testMember); + seniorRepository.save(testSenior); + + testCallback = callbackRepository.save(new Callback(Callback.Status.WAITING, testSenior)); + } + + @Test + @DisplayName("할당된 멤버id 조회 테스트 - 성공") + void findByAssignedMemberId() { + //given + testCallback.assignMember(memberId); + testCallback.changeStatusToInProgress(); + + //when + Callback result = callbackRepository.findByAssignedMemberIdAndStatus(memberId, Callback.Status.IN_PROGRESS).get(); + + //then + assertNotNull(result); + assertEquals(memberId, result.getAssignedMemberId()); + assertEquals("senior", result.getSeniorName()); + } + + @Test + @DisplayName("할당된 멤버id 조회 테스트 - 실패(멤버ID 불일치)") + void findByAssignedMemberId_fail() { + //given + testCallback.assignMember(memberId); + testCallback.changeStatusToInProgress(); + + //when + Optional result = callbackRepository.findByAssignedMemberIdAndStatus(100L, Callback.Status.IN_PROGRESS); + + //then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("할당된 멤버id 조회 테스트 - 실패(콜백 상태가 InProgress 아닐 경우)") + void findByAssignedMemberId_fail2() { + //given + //현재 Waiting 상태 + + //when + Optional result = callbackRepository.findByAssignedMemberIdAndStatus(memberId, Callback.Status.IN_PROGRESS); + + //then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("할당된 멤버id 조회 테스트 - 실패(멤버Id 불일치 +콜백 상태가 InProgress 아닐 경우)") + void findByAssignedMemberId_fail3() { + //given + testCallback.assignMember(memberId); + testCallback.changeStatusToInProgress(); + testCallback.changeStatusToPendingComplete(); + + //when + Optional result = callbackRepository.findByAssignedMemberIdAndStatus(100L, Callback.Status.IN_PROGRESS); + + //then + assertTrue(result.isEmpty()); + } +} diff --git a/src/test/java/com/example/sinitto/callback/service/CallbackServiceTest.java b/src/test/java/com/example/sinitto/callback/service/CallbackServiceTest.java index 86a6c96e..51be815d 100644 --- a/src/test/java/com/example/sinitto/callback/service/CallbackServiceTest.java +++ b/src/test/java/com/example/sinitto/callback/service/CallbackServiceTest.java @@ -2,6 +2,9 @@ import com.example.sinitto.callback.dto.CallbackResponse; import com.example.sinitto.callback.entity.Callback; +import com.example.sinitto.callback.exception.GuardMismatchException; +import com.example.sinitto.callback.exception.NotExistCallbackException; +import com.example.sinitto.callback.exception.NotMemberException; import com.example.sinitto.callback.exception.NotSinittoException; import com.example.sinitto.callback.repository.CallbackRepository; import com.example.sinitto.callback.util.TwilioHelper; @@ -9,7 +12,6 @@ import com.example.sinitto.member.entity.Member; import com.example.sinitto.member.entity.Senior; import com.example.sinitto.member.repository.MemberRepository; -import jakarta.persistence.EntityNotFoundException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -77,7 +79,7 @@ void getCallbacks_Fail_WhenNotMember() { when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); //when then - assertThrows(EntityNotFoundException.class, () -> callbackService.getCallbacks(memberId, pageable)); + assertThrows(NotMemberException.class, () -> callbackService.getCallbacks(memberId, pageable)); } @Test @@ -118,7 +120,7 @@ void accept() { } @Test - @DisplayName("콜백 완료 - 성공") + @DisplayName("콜백 완료 대기 - 성공") void complete() { //given Long memberId = 1L; @@ -134,10 +136,10 @@ void complete() { when(callback.getAssignedMemberId()).thenReturn(assignedMemberId); //when - callbackService.complete(memberId, callbackId); + callbackService.pendingComplete(memberId, callbackId); //then - verify(callback).changeStatusToComplete(); + verify(callback).changeStatusToPendingComplete(); } @Test @@ -197,4 +199,82 @@ void addCallback_fail() { verify(callbackRepository, times(0)).save(any()); assertNotNull(result); } + + @Test + @DisplayName("보호자가 콜백 대기 상태를 완료 생태로 변경 - 성공") + void completeByGuard() { + //given + Long memberId = 1L; + Long callbackId = 1L; + Member member = mock(Member.class); + Callback callback = mock(Callback.class); + Senior senior = mock(Senior.class); + + when(member.getId()).thenReturn(1L); + when(callbackRepository.findById(callbackId)).thenReturn(Optional.of(callback)); + when(callback.getSenior()).thenReturn(senior); + when(senior.getMember()).thenReturn(member); + when(senior.getMember().getId()).thenReturn(1L); + + //when + callbackService.complete(memberId, callbackId); + + //then + verify(callback).changeStatusToComplete(); + } + + @Test + @DisplayName("보호자가 콜백 대기 상태를 완료 생태로 변경 - 일치하는 보호자 ID가 아니어서 실패") + void completeByGuard_fail() { + //given + Long memberId = 10L; + Long callbackId = 1L; + Member member = mock(Member.class); + Callback callback = mock(Callback.class); + Senior senior = mock(Senior.class); + + when(member.getId()).thenReturn(1L); + when(callbackRepository.findById(callbackId)).thenReturn(Optional.of(callback)); + when(callback.getSenior()).thenReturn(senior); + when(senior.getMember()).thenReturn(member); + when(senior.getMember().getId()).thenReturn(1L); + + //when then + assertThrows(GuardMismatchException.class, () -> callbackService.complete(memberId, callbackId)); + } + + @Test + @DisplayName("콜백이 할당된 시니또 조회에 성공") + void getAcceptedCallback() { + //given + Long memberId = 1L; + Callback callback = mock(Callback.class); + Member member = mock(Member.class); + + //when + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(member.isSinitto()).thenReturn(true); + when(callback.getId()).thenReturn(10L); + + when(callbackRepository.findByAssignedMemberIdAndStatus(memberId, Callback.Status.IN_PROGRESS)).thenReturn(Optional.of(callback)); + CallbackResponse result = callbackService.getAcceptedCallback(memberId); + + //then + assertEquals(10L, result.callbackId()); + } + + @Test + @DisplayName("콜백이 할당된 시니또 조회에 실패") + void getAcceptedCallback_fail() { + //given + Long memberId = 1L; + Member member = mock(Member.class); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(member.isSinitto()).thenReturn(true); + when(callbackRepository.findByAssignedMemberIdAndStatus(memberId, Callback.Status.IN_PROGRESS)).thenReturn(Optional.empty()); + + //when then + assertThrows(NotExistCallbackException.class, () -> callbackService.getAcceptedCallback(memberId)); + } } diff --git a/src/test/java/com/example/sinitto/guard/entity/SeniorTest.java b/src/test/java/com/example/sinitto/guard/entity/SeniorTest.java index 869fa2a6..69ee7a99 100644 --- a/src/test/java/com/example/sinitto/guard/entity/SeniorTest.java +++ b/src/test/java/com/example/sinitto/guard/entity/SeniorTest.java @@ -14,7 +14,7 @@ public class SeniorTest { private Senior senior; @BeforeEach - void setup(){ + void setup() { member = new Member( "test", "01012345678", @@ -26,7 +26,7 @@ void setup(){ @Test @DisplayName("Senior 엔티티 생성자 테스트") - void counstructorTest(){ + void counstructorTest() { assertThat(senior.getId()).isNull(); assertThat(senior.getName()).isEqualTo("testSenior"); assertThat(senior.getPhoneNumber()).isEqualTo("01000000000"); @@ -35,7 +35,7 @@ void counstructorTest(){ @Test @DisplayName("updateSenior 메소드 테스트") - void updateSeniorTest(){ + void updateSeniorTest() { senior.updateSenior("updateSenior", "01011111111"); assertThat(senior.getName()).isEqualTo("updateSenior"); assertThat(senior.getPhoneNumber()).isEqualTo("01011111111"); @@ -43,7 +43,7 @@ void updateSeniorTest(){ @Test @DisplayName("mapToResponse 메소드 테스트") - void mapToResponseTest(){ + void mapToResponseTest() { SeniorResponse response = senior.mapToResponse(); assertThat(response.seniorId()).isNull(); assertThat(response.seniorName()).isEqualTo(senior.getName()); diff --git a/src/test/java/com/example/sinitto/guard/repository/SeniorRepositoryTest.java b/src/test/java/com/example/sinitto/guard/repository/SeniorRepositoryTest.java index b55f8975..060e5340 100644 --- a/src/test/java/com/example/sinitto/guard/repository/SeniorRepositoryTest.java +++ b/src/test/java/com/example/sinitto/guard/repository/SeniorRepositoryTest.java @@ -19,7 +19,7 @@ public class SeniorRepositoryTest { @Test @DisplayName("저장 테스트") - void saveSenior(){ + void saveSenior() { //given Member member = memberRepository.save(new Member("test", "01012345678", "test@test.com", false)); Senior senior = new Senior("testSenior", "01000000000", member); @@ -31,7 +31,7 @@ void saveSenior(){ @Test @DisplayName("조회 확인") - void readSenior(){ + void readSenior() { //given Member member = memberRepository.save(new Member("test", "01012345678", "test@test.com", false)); Senior senior = seniorRepository.save(new Senior("testSenior", "01000000000", member)); @@ -45,7 +45,7 @@ void readSenior(){ @Test @DisplayName("findByMemberId 메소드 테스트") - void findByMemberIdTest(){ + void findByMemberIdTest() { //given Member member = memberRepository.save(new Member("test", "01012345678", "test@test.com", false)); Senior senior = seniorRepository.save(new Senior("testSenior", "01000000000", member)); @@ -59,7 +59,7 @@ void findByMemberIdTest(){ @Test @DisplayName("findByIdAndMemberId 메소드 테스트") - void findByIdAndMemberIdTest(){ + void findByIdAndMemberIdTest() { //given Member member = memberRepository.save(new Member("test", "01012345678", "test@test.com", false)); Senior senior = seniorRepository.save(new Senior("testSenior", "01000000000", member)); diff --git a/src/test/java/com/example/sinitto/hellocall/entity/HelloCallTest.java b/src/test/java/com/example/sinitto/hellocall/entity/HelloCallTest.java new file mode 100644 index 00000000..a9c7b69a --- /dev/null +++ b/src/test/java/com/example/sinitto/hellocall/entity/HelloCallTest.java @@ -0,0 +1,124 @@ +package com.example.sinitto.hellocall.entity; + +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.exception.InvalidStatusException; +import com.example.sinitto.helloCall.exception.TimeRuleException; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class HelloCallTest { + + private HelloCall helloCall; + private Senior senior; + + @BeforeEach + void setup() { + Member member = new Member("보호자", "01033334444", "test@test.coom", false); + senior = new Senior("시니어", "01011112222", member); + helloCall = new HelloCall( + LocalDate.of(2024, 10, 1), + LocalDate.of(2024, 10, 10), + 10000, + 30, + "Hello Call Requirement", + senior + ); + } + + @Test + @DisplayName("HelloCall 생성자 테스트") + void constructorTest() { + assertThat(helloCall.getId()).isNull(); + assertThat(helloCall.getStartDate()).isEqualTo(LocalDate.of(2024, 10, 1)); + assertThat(helloCall.getEndDate()).isEqualTo(LocalDate.of(2024, 10, 10)); + assertThat(helloCall.getPrice()).isEqualTo(10000); + assertThat(helloCall.getServiceTime()).isEqualTo(30); + assertThat(helloCall.getRequirement()).isEqualTo("Hello Call Requirement"); + assertThat(helloCall.getStatus()).isEqualTo(HelloCall.Status.WAITING); + assertThat(helloCall.getSenior()).isEqualTo(senior); + } + + @Test + @DisplayName("시작날짜가 종료날짜 이후일 때 예외 발생 테스트") + void constructorThrowsTimeRuleException() { + assertThatThrownBy(() -> new HelloCall( + LocalDate.of(2024, 10, 11), + LocalDate.of(2024, 10, 10), + 10000, + 30, + "Invalid Requirement", + senior + )).isInstanceOf(TimeRuleException.class) + .hasMessage("시작날짜가 종료날짜 이후일 수 없습니다."); + } + + @Test + @DisplayName("상태 변경 테스트") + void changeStatusTest() { + helloCall.changeStatusToInProgress(); + assertThat(helloCall.getStatus()).isEqualTo(HelloCall.Status.IN_PROGRESS); + + helloCall.changeStatusToPendingComplete(); + assertThat(helloCall.getStatus()).isEqualTo(HelloCall.Status.PENDING_COMPLETE); + + helloCall.changeStatusToComplete(); + assertThat(helloCall.getStatus()).isEqualTo(HelloCall.Status.COMPLETE); + } + + @Test + @DisplayName("상태 변경 예외 발생 테스트") + void changeStatusThrowsInvalidStatusException() { + helloCall.changeStatusToInProgress(); + + assertThatThrownBy(() -> helloCall.changeStatusToInProgress()) + .isInstanceOf(InvalidStatusException.class) + .hasMessage("안부전화 서비스가 수행 대기중일 때만 진행중 상태로 변경할 수 있습니다. 현재 상태 : " + HelloCall.Status.IN_PROGRESS); + + helloCall.changeStatusToPendingComplete(); + + assertThatThrownBy(() -> helloCall.changeStatusToWaiting()) + .isInstanceOf(InvalidStatusException.class) + .hasMessage("안부전화 서비스가 수행중일 때만 진행중 상태로 변경할 수 있습니다. 현재 상태 : " + HelloCall.Status.PENDING_COMPLETE); + } + + @Test + @DisplayName("Update HelloCall 테스트") + void updateHelloCallTest() { + helloCall.updateHelloCall( + LocalDate.of(2024, 10, 2), + LocalDate.of(2024, 10, 12), + 12000, + 45, + "Updated Requirement" + ); + + assertThat(helloCall.getStartDate()).isEqualTo(LocalDate.of(2024, 10, 2)); + assertThat(helloCall.getEndDate()).isEqualTo(LocalDate.of(2024, 10, 12)); + assertThat(helloCall.getPrice()).isEqualTo(12000); + assertThat(helloCall.getServiceTime()).isEqualTo(45); + assertThat(helloCall.getRequirement()).isEqualTo("Updated Requirement"); + } + + @Test + @DisplayName("Update HelloCall 예외 발생 테스트") + void updateHelloCallThrowsInvalidStatusException() { + helloCall.changeStatusToInProgress(); + + assertThatThrownBy(() -> helloCall.updateHelloCall( + LocalDate.of(2024, 10, 2), + LocalDate.of(2024, 10, 12), + 12000, + 45, + "Updated Requirement" + )).isInstanceOf(InvalidStatusException.class) + .hasMessage("안부전화 서비스가 수행 대기중일 때만 수정이 가능합니다."); + } +} diff --git a/src/test/java/com/example/sinitto/hellocall/entity/HelloCallTimeLogTest.java b/src/test/java/com/example/sinitto/hellocall/entity/HelloCallTimeLogTest.java new file mode 100644 index 00000000..ebbfa9f4 --- /dev/null +++ b/src/test/java/com/example/sinitto/hellocall/entity/HelloCallTimeLogTest.java @@ -0,0 +1,57 @@ +package com.example.sinitto.hellocall.entity; + +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.HelloCallTimeLog; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Sinitto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class HelloCallTimeLogTest { + + private HelloCall helloCall; + private Sinitto sinitto; + private HelloCallTimeLog helloCallTimeLog; + + @BeforeEach + void setup() { + helloCall = new HelloCall(LocalDateTime.now().toLocalDate(), LocalDateTime.now().toLocalDate(), 10000, 30, "Test Requirement", null); + Member member = new Member("Sinitto Test", "01012345678", "sinitto@test.com", true); + sinitto = new Sinitto("테스트은행", "1111222233334444", member); + + helloCallTimeLog = new HelloCallTimeLog(helloCall, sinitto); + } + + @Test + @DisplayName("HelloCallTimeLog 생성자 테스트") + void constructorTest() { + assertThat(helloCallTimeLog).isNotNull(); + assertThat(helloCallTimeLog.getHelloCall()).isEqualTo(helloCall); + assertThat(helloCallTimeLog.getSinitto()).isEqualTo(sinitto); + } + + @Test + @DisplayName("시작 및 종료 시간 설정 테스트") + void setStartAndEndDateTimeTest() { + LocalDateTime startDateAndTime = LocalDateTime.now(); + LocalDateTime endDateAndTime = startDateAndTime.plusHours(1); + + helloCallTimeLog.setStartDateAndTime(startDateAndTime); + helloCallTimeLog.setEndDateAndTime(endDateAndTime); + + assertThat(helloCallTimeLog.getStartDateAndTime()).isEqualTo(startDateAndTime); + assertThat(helloCallTimeLog.getEndDateAndTime()).isEqualTo(endDateAndTime); + } + + @Test + @DisplayName("Sinitto 이름 가져오기 테스트") + void getSinittoNameTest() { + String expectedName = sinitto.getMember().getName(); + assertThat(helloCallTimeLog.getSinittoName()).isEqualTo(expectedName); + } +} diff --git a/src/test/java/com/example/sinitto/hellocall/entity/TimeSlotTest.java b/src/test/java/com/example/sinitto/hellocall/entity/TimeSlotTest.java new file mode 100644 index 00000000..384d0849 --- /dev/null +++ b/src/test/java/com/example/sinitto/hellocall/entity/TimeSlotTest.java @@ -0,0 +1,67 @@ +package com.example.sinitto.hellocall.entity; + +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.TimeSlot; +import com.example.sinitto.helloCall.exception.TimeRuleException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TimeSlotTest { + + private HelloCall helloCall; + private TimeSlot timeSlot; + + @BeforeEach + void setup() { + helloCall = new HelloCall(LocalDate.now(), LocalDate.now().plusDays(1), 10000, 30, "Test Requirement", null); + + timeSlot = new TimeSlot("Monday", LocalTime.of(9, 0), LocalTime.of(10, 0), helloCall); + } + + @Test + @DisplayName("TimeSlot 생성자 테스트 - 유효한 입력") + void constructorTest_ValidInput() { + assertThat(timeSlot.getDayName()).isEqualTo("Monday"); + assertThat(timeSlot.getStartTime()).isEqualTo(LocalTime.of(9, 0)); + assertThat(timeSlot.getEndTime()).isEqualTo(LocalTime.of(10, 0)); + assertThat(timeSlot.getHelloCall()).isEqualTo(helloCall); + } + + @Test + @DisplayName("TimeSlot 생성자 테스트 - 잘못된 시간 순서") + void constructorTest_InvalidTimeOrder() { + assertThatThrownBy(() -> new TimeSlot("Monday", LocalTime.of(10, 0), LocalTime.of(9, 0), helloCall)) + .isInstanceOf(TimeRuleException.class) + .hasMessage("시작시간이 종료시간 이후일 수 없습니다."); + } + + @Test + @DisplayName("TimeSlot 시간 업데이트 테스트 - 유효한 시간") + void updateTimeSlot_ValidTime() { + LocalTime newStartTime = LocalTime.of(10, 0); + LocalTime newEndTime = LocalTime.of(11, 0); + + timeSlot.updateTimeSlot(newStartTime, newEndTime); + + assertThat(timeSlot.getStartTime()).isEqualTo(newStartTime); + assertThat(timeSlot.getEndTime()).isEqualTo(newEndTime); + } + + @Test + @DisplayName("TimeSlot 시간 업데이트 테스트 - 잘못된 시간 순서") + void updateTimeSlot_InvalidTimeOrder() { + LocalTime newStartTime = LocalTime.of(11, 0); + LocalTime newEndTime = LocalTime.of(10, 0); + + assertThatThrownBy(() -> timeSlot.updateTimeSlot(newStartTime, newEndTime)) + .isInstanceOf(TimeRuleException.class) + .hasMessage("시작시간이 종료시간 이후일 수 없습니다."); + } +} diff --git a/src/test/java/com/example/sinitto/hellocall/repository/HelloCallRepositoryTest.java b/src/test/java/com/example/sinitto/hellocall/repository/HelloCallRepositoryTest.java new file mode 100644 index 00000000..8fc626a2 --- /dev/null +++ b/src/test/java/com/example/sinitto/hellocall/repository/HelloCallRepositoryTest.java @@ -0,0 +1,178 @@ +package com.example.sinitto.hellocall.repository; + +import com.example.sinitto.guard.repository.SeniorRepository; +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.repository.HelloCallRepository; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.entity.Sinitto; +import com.example.sinitto.member.repository.MemberRepository; +import com.example.sinitto.sinitto.repository.SinittoRepository; +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 java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class HelloCallRepositoryTest { + + @Autowired + private HelloCallRepository helloCallRepository; + + @Autowired + private SeniorRepository seniorRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private SinittoRepository sinittoRepository; + + private Member sinittoMember1; + private Member sinittoMember2; + private Member seniorMember; + private Sinitto sinitto1; + private Sinitto sinitto2; + private Senior senior; + + @BeforeEach + void setUp() { + sinittoMember1 = new Member("test1", "01011111111", "test1@test.com", true); + memberRepository.save(sinittoMember1); + + sinitto1 = new Sinitto("sinittoBank1", "BankAccount1", sinittoMember1); + sinittoRepository.save(sinitto1); + + sinittoMember2 = new Member("test2", "01022222222", "test2@test.com", true); + memberRepository.save(sinittoMember2); + + sinitto2 = new Sinitto("SinittoBank2", "BankAccount2", sinittoMember2); + sinittoRepository.save(sinitto2); + + seniorMember = new Member("test3", "01033333333", "test3@test.com", false); + memberRepository.save(seniorMember); + + senior = new Senior("SeniorName", "01044444444", seniorMember); + seniorRepository.save(senior); + } + + @Test + @DisplayName("Senior 기반으로 HelloCall 찾기 테스트") + void findBySeniorTest() { + HelloCall helloCall = new HelloCall(LocalDate.of(2024, 1, 1), + LocalDate.of(2024, 1, 2), 10000, 60, + "요구사항", senior); + helloCallRepository.save(helloCall); + + Optional foundHelloCall = helloCallRepository.findBySenior(senior); + + assertThat(foundHelloCall).isPresent(); + assertThat(foundHelloCall.get().getSenior()).isEqualTo(senior); + } + + @Test + @DisplayName("Sinitto 기반으로 HelloCall 리스트 찾기 테스트") + void findAllBySinittoTest() { + Senior senior1 = new Senior("Senior1", "01012345678", sinittoMember2); + seniorRepository.save(senior1); + Senior senior2 = new Senior("Senior2", "01098765432", sinittoMember2); + seniorRepository.save(senior2); + + HelloCall helloCall1 = new HelloCall(LocalDate.of(2024, 1, 1), + LocalDate.of(2024, 1, 2), 10000, 60, + "요구사항1", senior1); + HelloCall helloCall2 = new HelloCall(LocalDate.of(2024, 1, 3), + LocalDate.of(2024, 1, 4), 15000, 90, + "요구사항2", senior2); + + helloCall1.setSinitto(sinitto1); + helloCall2.setSinitto(sinitto1); + + helloCallRepository.save(helloCall1); + helloCallRepository.save(helloCall2); + + List helloCalls = helloCallRepository.findAllBySinitto(sinitto1); + + assertThat(helloCalls).hasSize(2); + assertThat(helloCalls.get(0).getSinitto()).isEqualTo(sinitto1); + assertThat(helloCalls.get(1).getSinitto()).isEqualTo(sinitto1); + } + + @Test + @DisplayName("Senior 존재 여부 테스트") + void existsBySeniorTest() { + HelloCall helloCall = new HelloCall(LocalDate.of(2024, 1, 1), + LocalDate.of(2024, 1, 2), 10000, 60, + "요구사항", senior); + helloCallRepository.save(helloCall); + + boolean exists = helloCallRepository.existsBySeniorAndStatusIn(senior, List.of(HelloCall.Status.WAITING)); + + assertThat(exists).isTrue(); + } + + @Test + @DisplayName("Senior가 안부전화에 할당되지 않았을 경우 테스트") + void doesNotExistBySeniorTest() { + Senior notExistingSenior = new Senior("NonExistent", "01099999999", seniorMember); + seniorRepository.save(notExistingSenior); + + boolean exists = helloCallRepository.existsBySeniorAndStatusIn(notExistingSenior, List.of(HelloCall.Status.WAITING)); + + assertThat(exists).isFalse(); + } + + @Test + @DisplayName("안부전화 담당 Sinitto가 변경되었을 경우, 변경이 잘 적용되는지 테스트") + void testChangeSinittoInHelloCall() { + HelloCall helloCall = new HelloCall(LocalDate.of(2024, 1, 1), + LocalDate.of(2024, 1, 2), 10000, 60, + "요구사항", senior); + + helloCall.setSinitto(sinitto1); + helloCallRepository.save(helloCall); + + Optional savedHelloCall = helloCallRepository.findById(helloCall.getId()); + assertThat(savedHelloCall).isPresent(); + assertThat(savedHelloCall.get().getSinitto()).isEqualTo(sinitto1); + + savedHelloCall.get().setSinitto(sinitto2); + helloCallRepository.save(savedHelloCall.get()); + + Optional updatedHelloCall = helloCallRepository.findById(helloCall.getId()); + assertThat(updatedHelloCall).isPresent(); + assertThat(updatedHelloCall.get().getSinitto()).isEqualTo(sinitto2); + } + + @Test + @DisplayName("Senior 리스트 기반으로 HelloCall 찾기 테스트") + void findAllBySeniorInTest() { + Senior senior1 = new Senior("Senior1", "01055555555", seniorMember); + Senior senior2 = new Senior("Senior2", "01066666666", seniorMember); + seniorRepository.save(senior1); + seniorRepository.save(senior2); + + HelloCall helloCall1 = new HelloCall(LocalDate.of(2024, 1, 1), + LocalDate.of(2024, 1, 2), 10000, 60, + "요구사항1", senior1); + HelloCall helloCall2 = new HelloCall(LocalDate.of(2024, 1, 3), + LocalDate.of(2024, 1, 4), 15000, 90, + "요구사항2", senior2); + + helloCallRepository.save(helloCall1); + helloCallRepository.save(helloCall2); + + List helloCalls = helloCallRepository.findAllBySeniorIn(List.of(senior1, senior2)); + + assertThat(helloCalls).hasSize(2); + assertThat(helloCalls.get(0).getSenior()).isEqualTo(senior1); + assertThat(helloCalls.get(1).getSenior()).isEqualTo(senior2); + } +} diff --git a/src/test/java/com/example/sinitto/hellocall/repository/HelloCallTimeLogRepositoryTest.java b/src/test/java/com/example/sinitto/hellocall/repository/HelloCallTimeLogRepositoryTest.java new file mode 100644 index 00000000..ae2068c4 --- /dev/null +++ b/src/test/java/com/example/sinitto/hellocall/repository/HelloCallTimeLogRepositoryTest.java @@ -0,0 +1,119 @@ +package com.example.sinitto.hellocall.repository; + +import com.example.sinitto.guard.repository.SeniorRepository; +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.HelloCallTimeLog; +import com.example.sinitto.helloCall.repository.HelloCallRepository; +import com.example.sinitto.helloCall.repository.HelloCallTimeLogRepository; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.entity.Sinitto; +import com.example.sinitto.member.repository.MemberRepository; +import com.example.sinitto.sinitto.repository.SinittoRepository; +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 java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class HelloCallTimeLogRepositoryTest { + + @Autowired + private HelloCallTimeLogRepository helloCallTimeLogRepository; + + @Autowired + private HelloCallRepository helloCallRepository; + + @Autowired + private SinittoRepository sinittoRepository; + + @Autowired + private SeniorRepository seniorRepository; + + @Autowired + private MemberRepository memberRepository; + + private Member sinittoMember; + private Member seniorMember; + private Sinitto sinitto; + private Senior senior; + private HelloCall helloCall; + + @BeforeEach + void setUp() { + sinittoMember = new Member("testSinitto", "01011112222", "sinitto@test.com", true); + memberRepository.save(sinittoMember); + + sinitto = new Sinitto("sinittoBank", "sinittoAccount", sinittoMember); + sinittoRepository.save(sinitto); + + seniorMember = new Member("testSenior", "01033334444", "senior@test.com", false); + memberRepository.save(seniorMember); + + senior = new Senior("SeniorName", "01055556666", seniorMember); + seniorRepository.save(senior); + + helloCall = new HelloCall(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 2), 10000, 60, "Test requirements", senior); + helloCallRepository.save(helloCall); + } + + @Test + @DisplayName("로그 저장 및 Sinitto와 HelloCall ID 기반으로 특정 HelloCallTimeLog 찾기 테스트") + void findBySinittoAndHelloCallIdTest() { + HelloCallTimeLog timeLog = new HelloCallTimeLog(helloCall, sinitto, LocalDateTime.of(2024, 1, 1, 10, 0), LocalDateTime.of(2024, 1, 1, 11, 0)); + helloCallTimeLogRepository.save(timeLog); + + Optional foundTimeLog = helloCallTimeLogRepository.findBySinittoAndAndHelloCallId(sinitto, helloCall.getId()); + + assertThat(foundTimeLog).isPresent(); + assertThat(foundTimeLog.get().getHelloCall()).isEqualTo(helloCall); + assertThat(foundTimeLog.get().getSinitto()).isEqualTo(sinitto); + assertThat(foundTimeLog.get().getStartDateAndTime()).isEqualTo(LocalDateTime.of(2024, 1, 1, 10, 0)); + assertThat(foundTimeLog.get().getEndDateAndTime()).isEqualTo(LocalDateTime.of(2024, 1, 1, 11, 0)); + } + + @Test + @DisplayName("HelloCall ID 기반으로 모든 HelloCallTimeLog 리스트 찾기 테스트") + void findAllByHelloCallIdTest() { + HelloCallTimeLog timeLog1 = new HelloCallTimeLog(helloCall, sinitto, LocalDateTime.of(2024, 1, 1, 10, 0), LocalDateTime.of(2024, 1, 1, 11, 0)); + HelloCallTimeLog timeLog2 = new HelloCallTimeLog(helloCall, sinitto, LocalDateTime.of(2024, 1, 1, 12, 0), LocalDateTime.of(2024, 1, 1, 13, 0)); + + helloCallTimeLogRepository.save(timeLog1); + helloCallTimeLogRepository.save(timeLog2); + + List timeLogs = helloCallTimeLogRepository.findAllByHelloCallId(helloCall.getId()); + + assertThat(timeLogs).hasSize(2); + assertThat(timeLogs.get(0).getHelloCall()).isEqualTo(helloCall); + assertThat(timeLogs.get(1).getHelloCall()).isEqualTo(helloCall); + assertThat(timeLogs.get(0).getStartDateAndTime()).isEqualTo(LocalDateTime.of(2024, 1, 1, 10, 0)); + assertThat(timeLogs.get(1).getStartDateAndTime()).isEqualTo(LocalDateTime.of(2024, 1, 1, 12, 0)); + } + + @Test + @DisplayName("Sinitto와 HelloCall 기반으로 가장 최근의 HelloCallTimeLog 찾기 테스트") + void findTopBySinittoAndHelloCallOrderByStartDateAndTimeDescTest() { + HelloCallTimeLog timeLog1 = new HelloCallTimeLog(helloCall, sinitto, LocalDateTime.of(2024, 1, 1, 10, 0), LocalDateTime.of(2024, 1, 1, 11, 0)); + HelloCallTimeLog timeLog2 = new HelloCallTimeLog(helloCall, sinitto, LocalDateTime.of(2024, 1, 1, 12, 0), LocalDateTime.of(2024, 1, 1, 13, 0)); + + helloCallTimeLogRepository.save(timeLog1); + helloCallTimeLogRepository.save(timeLog2); + + Optional recentTimeLog = helloCallTimeLogRepository + .findTopBySinittoAndHelloCallOrderByStartDateAndTimeDesc(sinitto, helloCall); + + assertThat(recentTimeLog).isPresent(); + assertThat(recentTimeLog.get().getHelloCall()).isEqualTo(helloCall); + assertThat(recentTimeLog.get().getSinitto()).isEqualTo(sinitto); + assertThat(recentTimeLog.get().getStartDateAndTime()).isEqualTo(LocalDateTime.of(2024, 1, 1, 12, 0)); + assertThat(recentTimeLog.get().getEndDateAndTime()).isEqualTo(LocalDateTime.of(2024, 1, 1, 13, 0)); + } +} diff --git a/src/test/java/com/example/sinitto/hellocall/repository/TimeSlotRepositoryTest.java b/src/test/java/com/example/sinitto/hellocall/repository/TimeSlotRepositoryTest.java new file mode 100644 index 00000000..04eaf40d --- /dev/null +++ b/src/test/java/com/example/sinitto/hellocall/repository/TimeSlotRepositoryTest.java @@ -0,0 +1,89 @@ +package com.example.sinitto.hellocall.repository; + +import com.example.sinitto.guard.repository.SeniorRepository; +import com.example.sinitto.helloCall.entity.HelloCall; +import com.example.sinitto.helloCall.entity.TimeSlot; +import com.example.sinitto.helloCall.repository.HelloCallRepository; +import com.example.sinitto.helloCall.repository.TimeSlotRepository; +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.entity.Senior; +import com.example.sinitto.member.repository.MemberRepository; +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 java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +class TimeSlotRepositoryTest { + + @Autowired + private TimeSlotRepository timeSlotRepository; + + @Autowired + private HelloCallRepository helloCallRepository; + + @Autowired + private SeniorRepository seniorRepository; + + @Autowired + private MemberRepository memberRepository; + + private HelloCall helloCall; + private Senior senior; + private Member seniorMember; + + @BeforeEach + void setUp() { + seniorMember = new Member("testSenior", "01033334444", "senior@test.com", false); + memberRepository.save(seniorMember); + + senior = new Senior("SeniorName", "01055556666", seniorMember); + seniorRepository.save(senior); + + helloCall = new HelloCall(LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 2), 10000, 60, "테스트 요구사항", senior); + helloCallRepository.save(helloCall); + } + + @Test + @DisplayName("HelloCall 및 DayName을 기반으로 TimeSlot 찾기 테스트") + void findByHelloCallAndDayTest() { + TimeSlot timeSlot = new TimeSlot("금", LocalTime.of(10, 0), LocalTime.of(11, 0), helloCall); + timeSlotRepository.save(timeSlot); + + Optional foundTimeSlot = timeSlotRepository.findByHelloCallAndDayName(helloCall, "금"); + + assertThat(foundTimeSlot).isPresent(); + assertThat(foundTimeSlot.get().getHelloCall()).isEqualTo(helloCall); + assertThat(foundTimeSlot.get().getDayName()).isEqualTo("금"); + } + + @Test + @DisplayName("존재하지 않는 DayName으로 TimeSlot 찾기 테스트") + void findByHelloCallAndNonExistentDayTest() { + Optional foundTimeSlot = timeSlotRepository.findByHelloCallAndDayName(helloCall, "월"); + + assertThat(foundTimeSlot).isNotPresent(); + } + + @Test + @DisplayName("HelloCall 기반으로 모든 TimeSlot 삭제 테스트") + void deleteAllByHelloCallTest() { + TimeSlot timeSlot1 = new TimeSlot("월", LocalTime.of(9, 0), LocalTime.of(10, 0), helloCall); + TimeSlot timeSlot2 = new TimeSlot("화", LocalTime.of(10, 0), LocalTime.of(11, 0), helloCall); + timeSlotRepository.save(timeSlot1); + timeSlotRepository.save(timeSlot2); + + assertThat(timeSlotRepository.findAllByHelloCall(helloCall)).hasSize(2); + + timeSlotRepository.deleteAllByHelloCall(helloCall); + + assertThat(timeSlotRepository.findAllByHelloCall(helloCall)).isEmpty(); + } +} diff --git a/src/test/java/com/example/sinitto/review/entity/ReviewTest.java b/src/test/java/com/example/sinitto/review/entity/ReviewTest.java new file mode 100644 index 00000000..fce3ad1c --- /dev/null +++ b/src/test/java/com/example/sinitto/review/entity/ReviewTest.java @@ -0,0 +1,46 @@ +package com.example.sinitto.review.entity; + +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.review.dto.ReviewResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class ReviewTest { + private Member member; + private Review review; + + @BeforeEach + public void setup() { + member = new Member( + "test", + "01012345678", + "test@test.com", + true); + review = new Review(5, 4, 3, "testContent", member); + } + + @Test + @DisplayName("Review 엔티티 생성자 테스트") + void counstructorTest() { + assertThat(review.getId()).isNull(); + assertThat(review.getStarCountForRequest()).isEqualTo(5); + assertThat(review.getStarCountForService()).isEqualTo(4); + assertThat(review.getStarCountForSatisfaction()).isEqualTo(3); + assertThat(review.getPostDate()).isNull(); + assertThat(review.getContent()).isEqualTo("testContent"); + assertThat(review.getMember()).isEqualTo(member); + } + + @Test + @DisplayName("mapToResponse 메소드 테스트") + void mapToResponseTest() { + ReviewResponse response = review.mapToResponse(); + assertThat(response.name()).isEqualTo(member.getName()); + assertThat(response.averageStarCount()).isEqualTo(4.0); + assertThat(response.postDate()).isNull(); + assertThat(response.content()).isEqualTo(review.getContent()); + } +} diff --git a/src/test/java/com/example/sinitto/review/service/ReviewServiceTest.java b/src/test/java/com/example/sinitto/review/service/ReviewServiceTest.java new file mode 100644 index 00000000..df1fd9a7 --- /dev/null +++ b/src/test/java/com/example/sinitto/review/service/ReviewServiceTest.java @@ -0,0 +1,70 @@ +package com.example.sinitto.review.service; + +import com.example.sinitto.member.entity.Member; +import com.example.sinitto.member.repository.MemberRepository; +import com.example.sinitto.review.dto.ReviewRequest; +import com.example.sinitto.review.dto.ReviewResponse; +import com.example.sinitto.review.entity.Review; +import com.example.sinitto.review.repository.ReviewRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@MockitoSettings +public class ReviewServiceTest { + @Mock + ReviewRepository reviewRepository; + @Mock + MemberRepository memberRepository; + @InjectMocks + ReviewService reviewService; + + @Test + @DisplayName("readAllReviews 메소드 테스트") + public void readAllReviewsTest() { + //given + Member member = new Member("testName", "01000000000", "test@test.com", true); + List reviewList = new ArrayList<>(); + reviewList.add(new Review(5, 4, 3, "testContent", member)); + when(reviewRepository.findAll()).thenReturn(reviewList); + //when + List reviewResponseList = reviewService.readAllReviews(); + //then + assertEquals(1, reviewResponseList.size()); + assertEquals("testName", reviewResponseList.getFirst().name()); + assertEquals("testContent", reviewResponseList.getFirst().content()); + assertEquals(4.0, reviewResponseList.getFirst().averageStarCount(), 0.01); + } + + @Test + @DisplayName("createReview 메소드 테스트") + public void createReviewTest() { + //given + Member member = new Member("testName", "01000000000", "test@test.com", true); + Long memberId = 1L; + int starCountForRequest = 5; + int starCountForService = 4; + int starCountForSatisfaction = 3; + String content = "testContent"; + ReviewRequest reviewRequest = new ReviewRequest(starCountForRequest, + starCountForService, + starCountForSatisfaction, + content); + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + + //when + reviewService.createReview(memberId, reviewRequest); + + //then + verify(reviewRepository, times(1)).save(any()); + } +}