diff --git a/build.gradle b/build.gradle index 375e97132..90d017dfe 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,11 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-gson:0.11.2' + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'io.rest-assured:rest-assured:5.3.1' diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 000000000..3c00c1572 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +REPOSITORY="$(cd "$(dirname "$0")" && pwd)" +PID_FILE="$REPOSITORY/app.pid" +LOG_FILE="$REPOSITORY/nohup.log" + +echo "[INFO] 최신 코드 pull" +cd "$REPOSITORY" || exit 1 +git pull + +echo "[INFO] 프로젝트 빌드" +./gradlew build +if [ $? -ne 0 ]; then + echo "[ERROR] 빌드에 실패했습니다. 배포를 중단합니다." + exit 1 +fi + +echo "[INFO] 기존에 실행 중인 애플리케이션 종료" +CURRENT_PID=$(jps -l | grep roomescape | awk '{print $1}') + +if [ -z "$CURRENT_PID" ] && [ -f "$PID_FILE" ]; then + CURRENT_PID=$(cat "$PID_FILE") + kill -0 "$CURRENT_PID" 2>/dev/null || CURRENT_PID="" +fi + +if [ -z "$CURRENT_PID" ]; then + echo "[INFO] 종료할 애플리케이션이 없습니다." +else + echo "[INFO] kill -15 $CURRENT_PID" + kill -15 "$CURRENT_PID" 2>/dev/null || taskkill //PID "$CURRENT_PID" //F + sleep 5 +fi +rm -f "$PID_FILE" + +echo "[INFO] 새 애플리케이션 실행" +JAR_NAME=$(ls "$REPOSITORY"/build/libs/*.jar | grep -v plain) +JAR_WIN=$(cygpath -w "$JAR_NAME") + +nohup java -jar "$JAR_WIN" > "$LOG_FILE" 2>&1 & +echo $! > "$PID_FILE" + +sleep 3 +echo "[INFO] 애플리케이션이 실행되었습니다. PID: $(cat "$PID_FILE")" +echo "[INFO] 로그 확인: tail -f $LOG_FILE" \ No newline at end of file diff --git a/setup.sh b/setup.sh new file mode 100644 index 000000000..deb6d6bab --- /dev/null +++ b/setup.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +REPOSITORY=/home/ubuntu/app +REPO_URL="https://github.com/htdufhc-bit/spring-basic-roomescape-playground" +PID_FILE="$REPOSITORY/app.pid" + +echo "[INFO] 시스템 패키지 업데이트" +sudo apt update + +echo "[INFO] JDK 17 설치" +sudo apt install -y openjdk-17-jdk + +echo "[INFO] 프로젝트 디렉토리 생성 및 이동" +mkdir -p $REPOSITORY +cd $REPOSITORY + +echo "[INFO] Git Repository 클론 또는 최신화" +if [ -d ".git" ]; then + echo "이미 git repository가 존재합니다. 최신 코드를 받아옵니다." + git pull +else + git clone $REPO_URL . +fi + +echo "[INFO] 초기 설정 완료" diff --git a/src/main/java/roomescape/auth/controller/AuthController.java b/src/main/java/roomescape/auth/controller/AuthController.java index 9a2d31177..4877da787 100644 --- a/src/main/java/roomescape/auth/controller/AuthController.java +++ b/src/main/java/roomescape/auth/controller/AuthController.java @@ -15,7 +15,7 @@ import roomescape.auth.service.AuthService; import roomescape.auth.web.Login; import roomescape.auth.web.TokenExtractor; -import roomescape.util.CookieUtil; +import roomescape.global.util.CookieUtil; @RestController public class AuthController { @@ -52,7 +52,7 @@ public ResponseEntity loginByRefreshToken(HttpServletRequest request, @GetMapping("/login/check") public ResponseEntity checkLogin(@Login LoginMember loginMember) { - return ResponseEntity.ok(new LoginCheckResponse(loginMember.name())); + return ResponseEntity.ok(LoginCheckResponse.from(loginMember)); } @PostMapping("/logout") diff --git a/src/main/java/roomescape/auth/dto/LoginCheckResponse.java b/src/main/java/roomescape/auth/dto/LoginCheckResponse.java index 8aa31879f..82f0e69e7 100644 --- a/src/main/java/roomescape/auth/dto/LoginCheckResponse.java +++ b/src/main/java/roomescape/auth/dto/LoginCheckResponse.java @@ -1,6 +1,12 @@ package roomescape.auth.dto; +import roomescape.auth.domain.LoginMember; + public record LoginCheckResponse( String name ) { + + public static LoginCheckResponse from(LoginMember loginMember) { + return new LoginCheckResponse(loginMember.name()); + } } diff --git a/src/main/java/roomescape/auth/dto/TokenResponse.java b/src/main/java/roomescape/auth/dto/TokenResponse.java index 57a73e871..e42195de7 100644 --- a/src/main/java/roomescape/auth/dto/TokenResponse.java +++ b/src/main/java/roomescape/auth/dto/TokenResponse.java @@ -4,4 +4,8 @@ public record TokenResponse( String accessToken, String refreshToken ) { + + public static TokenResponse of(String accessToken, String refreshToken) { + return new TokenResponse(accessToken, refreshToken); + } } diff --git a/src/main/java/roomescape/auth/entity/RefreshToken.java b/src/main/java/roomescape/auth/entity/RefreshToken.java index 661180100..376270ae2 100644 --- a/src/main/java/roomescape/auth/entity/RefreshToken.java +++ b/src/main/java/roomescape/auth/entity/RefreshToken.java @@ -8,9 +8,13 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; import roomescape.member.entity.Member; @Entity(name = "refresh_token") +@NoArgsConstructor +@Getter public class RefreshToken { @Id @@ -24,23 +28,12 @@ public class RefreshToken { @Column(nullable = false, length = 500) private String token; - public RefreshToken() { - } - - public RefreshToken(Member member, String token) { + private RefreshToken(Member member, String token) { this.member = member; this.token = token; } - public Long getId() { - return id; - } - - public Member getMember() { - return member; - } - - public String getToken() { - return token; + public static RefreshToken of(Member member, String token) { + return new RefreshToken(member, token); } } diff --git a/src/main/java/roomescape/auth/exception/AuthErrorCode.java b/src/main/java/roomescape/auth/exception/AuthErrorCode.java index 1aa8bd3b9..6d212c0e2 100644 --- a/src/main/java/roomescape/auth/exception/AuthErrorCode.java +++ b/src/main/java/roomescape/auth/exception/AuthErrorCode.java @@ -1,7 +1,7 @@ package roomescape.auth.exception; import org.springframework.http.HttpStatus; -import roomescape.exception.ErrorCode; +import roomescape.global.exception.ErrorCode; public enum AuthErrorCode implements ErrorCode { ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), diff --git a/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java b/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java deleted file mode 100644 index 59cf4bb19..000000000 --- a/src/main/java/roomescape/auth/jwt/JwtTokenProperty.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.auth.jwt; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties("roomescape.auth.jwt") -public record JwtTokenProperty( - String secretKey, - long accessTokenExpiration, - long refreshTokenExpiration -) { -} diff --git a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java index 3e916cef1..c5daeace9 100644 --- a/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java +++ b/src/main/java/roomescape/auth/jwt/JwtTokenProvider.java @@ -7,27 +7,29 @@ import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SignatureException; -import org.springframework.stereotype.Component; import roomescape.auth.exception.AuthErrorCode; -import roomescape.exception.ApplicationException; +import roomescape.global.exception.ApplicationException; import roomescape.member.entity.Member; import java.security.Key; import java.util.Base64; import java.util.Date; -@Component public class JwtTokenProvider { private final Key secretKey; private final long accessTokenExpiration; private final long refreshTokenExpiration; - public JwtTokenProvider(JwtTokenProperty tokenProperty) { + public JwtTokenProvider( + String secretKey, + long accessTokenExpiration, + long refreshTokenExpiration + ) { this.secretKey = Keys.hmacShaKeyFor( - Base64.getDecoder().decode(tokenProperty.secretKey())); - this.accessTokenExpiration = tokenProperty.accessTokenExpiration(); - this.refreshTokenExpiration = tokenProperty.refreshTokenExpiration(); + Base64.getDecoder().decode(secretKey)); + this.accessTokenExpiration = accessTokenExpiration; + this.refreshTokenExpiration = refreshTokenExpiration; } public String createAccessToken(Member member) { diff --git a/src/main/java/roomescape/auth/repository/JpaRefreshTokenRepository.java b/src/main/java/roomescape/auth/repository/JpaRefreshTokenRepository.java deleted file mode 100644 index 897cdd4a1..000000000 --- a/src/main/java/roomescape/auth/repository/JpaRefreshTokenRepository.java +++ /dev/null @@ -1,53 +0,0 @@ -package roomescape.auth.repository; - -import jakarta.persistence.EntityManager; -import org.springframework.stereotype.Repository; -import roomescape.auth.entity.RefreshToken; - -import java.util.Optional; - -@Repository -public class JpaRefreshTokenRepository implements RefreshTokenRepository { - - private final EntityManager entityManager; - - public JpaRefreshTokenRepository(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Override - public void save(RefreshToken refreshToken) { - entityManager.persist(refreshToken); - } - - @Override - public Optional findByMemberId(Long memberId) { - String jpql = "SELECT rt FROM refresh_token rt WHERE rt.member.id = :memberId"; - - return entityManager.createQuery(jpql, RefreshToken.class) - .setParameter("memberId", memberId) - .getResultStream() - .findFirst(); - } - - @Override - public Optional findByToken(String token) { - String jpql = "SELECT rt FROM refresh_token rt WHERE rt.token = :token"; - - return entityManager.createQuery(jpql, RefreshToken.class) - .setParameter("token", token) - .getResultStream() - .findFirst(); - } - - @Override - public void deleteByMemberId(Long memberId) { - findByMemberId(memberId) - .ifPresent(entityManager::remove); - } - - @Override - public void flush() { - entityManager.flush(); - } -} diff --git a/src/main/java/roomescape/auth/repository/RefreshTokenRepository.java b/src/main/java/roomescape/auth/repository/RefreshTokenRepository.java index da7f151f4..de479e0d4 100644 --- a/src/main/java/roomescape/auth/repository/RefreshTokenRepository.java +++ b/src/main/java/roomescape/auth/repository/RefreshTokenRepository.java @@ -1,18 +1,15 @@ package roomescape.auth.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import roomescape.auth.entity.RefreshToken; import java.util.Optional; -public interface RefreshTokenRepository { - - void save(RefreshToken refreshToken); +@Repository +public interface RefreshTokenRepository extends JpaRepository { Optional findByToken(String token); - Optional findByMemberId(Long memberId); - void deleteByMemberId(Long memberId); - - void flush(); } diff --git a/src/main/java/roomescape/auth/service/AuthService.java b/src/main/java/roomescape/auth/service/AuthService.java index f62e62629..767d3fcad 100644 --- a/src/main/java/roomescape/auth/service/AuthService.java +++ b/src/main/java/roomescape/auth/service/AuthService.java @@ -9,7 +9,7 @@ import roomescape.auth.exception.AuthErrorCode; import roomescape.auth.jwt.JwtTokenProvider; import roomescape.auth.repository.RefreshTokenRepository; -import roomescape.exception.ApplicationException; +import roomescape.global.exception.ApplicationException; import roomescape.member.entity.Member; import roomescape.member.exception.MemberErrorCode; import roomescape.member.repository.MemberRepository; @@ -68,8 +68,8 @@ private TokenResponse createTokens(Member member) { refreshTokenRepository.deleteByMemberId(member.getId()); refreshTokenRepository.flush(); - refreshTokenRepository.save(new RefreshToken(member, refreshToken)); + refreshTokenRepository.save(RefreshToken.of(member, refreshToken)); - return new TokenResponse(accessToken, refreshToken); + return TokenResponse.of(accessToken, refreshToken); } } diff --git a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java index 931778af1..af108c43e 100644 --- a/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java +++ b/src/main/java/roomescape/auth/web/CheckAdminInterceptor.java @@ -7,7 +7,7 @@ import roomescape.auth.domain.LoginMember; import roomescape.auth.exception.AuthErrorCode; import roomescape.auth.service.AuthService; -import roomescape.exception.ApplicationException; +import roomescape.global.exception.ApplicationException; @Component public class CheckAdminInterceptor implements HandlerInterceptor { diff --git a/src/main/java/roomescape/auth/web/TokenExtractor.java b/src/main/java/roomescape/auth/web/TokenExtractor.java index 234c48223..83a2ef245 100644 --- a/src/main/java/roomescape/auth/web/TokenExtractor.java +++ b/src/main/java/roomescape/auth/web/TokenExtractor.java @@ -3,8 +3,8 @@ import jakarta.servlet.http.Cookie; import org.springframework.stereotype.Component; import roomescape.auth.exception.AuthErrorCode; -import roomescape.exception.ApplicationException; -import roomescape.util.CookieUtil; +import roomescape.global.exception.ApplicationException; +import roomescape.global.util.CookieUtil; @Component public class TokenExtractor { diff --git a/src/main/java/roomescape/global/config/DataLoader.java b/src/main/java/roomescape/global/config/DataLoader.java new file mode 100644 index 000000000..520f314c9 --- /dev/null +++ b/src/main/java/roomescape/global/config/DataLoader.java @@ -0,0 +1,38 @@ +package roomescape.global.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import roomescape.member.entity.Member; +import roomescape.member.entity.Role; +import roomescape.member.repository.MemberRepository; + +@Component +@Profile("prod") +public class DataLoader implements CommandLineRunner { + + @Autowired + private MemberRepository memberRepository; + + @Override + @Transactional + public void run(String... args) throws Exception { + createMember("어드민", "admil@email.com", "password", Role.ADMIN); + createMember("브라운", "brown@email.com", "password", Role.USER); + } + + private void createMember(String name, String email, String password, Role role) { + if (!validateMemberExists(email)) { + memberRepository.save( + Member.of(name, email, password, role) + ); + } + } + + private boolean validateMemberExists(String email) { + return memberRepository.findByEmail(email) + .isPresent(); + } +} diff --git a/src/main/java/roomescape/global/config/SecureConfig.java b/src/main/java/roomescape/global/config/SecureConfig.java new file mode 100644 index 000000000..fac356bbe --- /dev/null +++ b/src/main/java/roomescape/global/config/SecureConfig.java @@ -0,0 +1,24 @@ +package roomescape.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import roomescape.auth.jwt.JwtTokenProvider; + +@Configuration +public class SecureConfig { + + @Value("${roomescape.auth.jwt.secret-key}") + String secretKey; + + @Value("${roomescape.auth.jwt.access-token-expiration}") + long accessTokenExpiration; + + @Value("${roomescape.auth.jwt.refresh-token-expiration}") + long refreshTokenExpiration; + + @Bean + public JwtTokenProvider jwtTokenProvider() { + return new JwtTokenProvider(secretKey, accessTokenExpiration, refreshTokenExpiration); + } +} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/global/config/WebConfig.java similarity index 97% rename from src/main/java/roomescape/config/WebConfig.java rename to src/main/java/roomescape/global/config/WebConfig.java index 44332017b..337cadf9b 100644 --- a/src/main/java/roomescape/config/WebConfig.java +++ b/src/main/java/roomescape/global/config/WebConfig.java @@ -1,4 +1,4 @@ -package roomescape.config; +package roomescape.global.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; diff --git a/src/main/java/roomescape/global/entity/BaseSoftDeleteEntity.java b/src/main/java/roomescape/global/entity/BaseSoftDeleteEntity.java new file mode 100644 index 000000000..5fb7d6acb --- /dev/null +++ b/src/main/java/roomescape/global/entity/BaseSoftDeleteEntity.java @@ -0,0 +1,23 @@ +package roomescape.global.entity; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public class BaseSoftDeleteEntity { + + private static final ZoneId KST_ZONE = ZoneId.of("Asia/Seoul"); + + private LocalDateTime deletedAt; + + public void markDeleted() { + this.deletedAt = LocalDateTime.now(KST_ZONE); + } +} diff --git a/src/main/java/roomescape/exception/ApplicationException.java b/src/main/java/roomescape/global/exception/ApplicationException.java similarity index 89% rename from src/main/java/roomescape/exception/ApplicationException.java rename to src/main/java/roomescape/global/exception/ApplicationException.java index 5177c2a3e..154101b6a 100644 --- a/src/main/java/roomescape/exception/ApplicationException.java +++ b/src/main/java/roomescape/global/exception/ApplicationException.java @@ -1,4 +1,4 @@ -package roomescape.exception; +package roomescape.global.exception; public class ApplicationException extends RuntimeException { diff --git a/src/main/java/roomescape/exception/ErrorCode.java b/src/main/java/roomescape/global/exception/ErrorCode.java similarity index 78% rename from src/main/java/roomescape/exception/ErrorCode.java rename to src/main/java/roomescape/global/exception/ErrorCode.java index 9b076f749..0a468dc94 100644 --- a/src/main/java/roomescape/exception/ErrorCode.java +++ b/src/main/java/roomescape/global/exception/ErrorCode.java @@ -1,4 +1,4 @@ -package roomescape.exception; +package roomescape.global.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/roomescape/exception/ErrorResponse.java b/src/main/java/roomescape/global/exception/ErrorResponse.java similarity index 56% rename from src/main/java/roomescape/exception/ErrorResponse.java rename to src/main/java/roomescape/global/exception/ErrorResponse.java index ebe97cc4a..be5e6a5bb 100644 --- a/src/main/java/roomescape/exception/ErrorResponse.java +++ b/src/main/java/roomescape/global/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package roomescape.exception; +package roomescape.global.exception; public record ErrorResponse(String message) { } diff --git a/src/main/java/roomescape/exception/GlobalExceptionHandler.java b/src/main/java/roomescape/global/exception/GlobalExceptionHandler.java similarity index 97% rename from src/main/java/roomescape/exception/GlobalExceptionHandler.java rename to src/main/java/roomescape/global/exception/GlobalExceptionHandler.java index ce0875b2b..8711c2797 100644 --- a/src/main/java/roomescape/exception/GlobalExceptionHandler.java +++ b/src/main/java/roomescape/global/exception/GlobalExceptionHandler.java @@ -1,4 +1,4 @@ -package roomescape.exception; +package roomescape.global.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/roomescape/util/CookieUtil.java b/src/main/java/roomescape/global/util/CookieUtil.java similarity index 98% rename from src/main/java/roomescape/util/CookieUtil.java rename to src/main/java/roomescape/global/util/CookieUtil.java index 6902f398e..a49dd81b3 100644 --- a/src/main/java/roomescape/util/CookieUtil.java +++ b/src/main/java/roomescape/global/util/CookieUtil.java @@ -1,4 +1,4 @@ -package roomescape.util; +package roomescape.global.util; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/roomescape/member/controller/MemberController.java b/src/main/java/roomescape/member/controller/MemberController.java index 52bc3d5a7..8d0696c40 100644 --- a/src/main/java/roomescape/member/controller/MemberController.java +++ b/src/main/java/roomescape/member/controller/MemberController.java @@ -22,6 +22,6 @@ public MemberController(MemberService memberService) { @PostMapping("/members") public ResponseEntity createMember(@Valid @RequestBody MemberRequest memberRequest) { MemberResponse member = memberService.create(memberRequest); - return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); + return ResponseEntity.created(URI.create("/members/" + member.id())).body(member); } } diff --git a/src/main/java/roomescape/member/dto/MemberRequest.java b/src/main/java/roomescape/member/dto/MemberRequest.java index 309b8a2b7..c0c08ecc4 100644 --- a/src/main/java/roomescape/member/dto/MemberRequest.java +++ b/src/main/java/roomescape/member/dto/MemberRequest.java @@ -3,27 +3,13 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public class MemberRequest { - - @NotBlank(message = "이름은 필수입니다.") - private String name; - - @NotBlank(message = "이메일은 필수입니다.") - @Email(message = "이메일 형식이 올바르지 않습니다.") - private String email; - - @NotBlank(message = "비밀번호는 필수입니다.") - private String password; - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } +public record MemberRequest( + @NotBlank(message = "이름은 필수입니다.") + String name, + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + String email, + @NotBlank(message = "비밀번호는 필수입니다.") + String password +) { } diff --git a/src/main/java/roomescape/member/dto/MemberResponse.java b/src/main/java/roomescape/member/dto/MemberResponse.java index b3235d2c3..420d5562e 100644 --- a/src/main/java/roomescape/member/dto/MemberResponse.java +++ b/src/main/java/roomescape/member/dto/MemberResponse.java @@ -1,25 +1,14 @@ package roomescape.member.dto; -public class MemberResponse { - private Long id; - private String name; - private String email; +import roomescape.member.entity.Member; - public MemberResponse(Long id, String name, String email) { - this.id = id; - this.name = name; - this.email = email; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } +public record MemberResponse( + Long id, + String name, + String email +) { - public String getEmail() { - return email; + public static MemberResponse from(Member member) { + return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } } diff --git a/src/main/java/roomescape/member/entity/Member.java b/src/main/java/roomescape/member/entity/Member.java index 873741f7e..33268cdc2 100644 --- a/src/main/java/roomescape/member/entity/Member.java +++ b/src/main/java/roomescape/member/entity/Member.java @@ -7,8 +7,12 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import lombok.Getter; +import lombok.NoArgsConstructor; @Entity(name = "member") +@NoArgsConstructor +@Getter public class Member { @Id @@ -27,33 +31,14 @@ public class Member { @Enumerated(EnumType.STRING) private Role role; - public Member() { - } - - public Member(String name, String email, String password, Role role) { + private Member(String name, String email, String password, Role role) { this.name = name; this.email = email; this.password = password; this.role = role; } - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public Role getRole() { - return role; + public static Member of(String name, String email, String password, Role role) { + return new Member(name, email, password, role); } } diff --git a/src/main/java/roomescape/member/exception/MemberErrorCode.java b/src/main/java/roomescape/member/exception/MemberErrorCode.java index d476aa0e5..806ac0ab2 100644 --- a/src/main/java/roomescape/member/exception/MemberErrorCode.java +++ b/src/main/java/roomescape/member/exception/MemberErrorCode.java @@ -1,7 +1,7 @@ package roomescape.member.exception; import org.springframework.http.HttpStatus; -import roomescape.exception.ErrorCode; +import roomescape.global.exception.ErrorCode; public enum MemberErrorCode implements ErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), diff --git a/src/main/java/roomescape/member/repository/JpaMemberRepository.java b/src/main/java/roomescape/member/repository/JpaMemberRepository.java deleted file mode 100644 index ac61e135c..000000000 --- a/src/main/java/roomescape/member/repository/JpaMemberRepository.java +++ /dev/null @@ -1,52 +0,0 @@ -package roomescape.member.repository; - -import jakarta.persistence.EntityManager; -import org.springframework.stereotype.Repository; -import roomescape.member.entity.Member; - -import java.util.List; -import java.util.Optional; - -@Repository -public class JpaMemberRepository implements MemberRepository { - - private final EntityManager entityManager; - - public JpaMemberRepository(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Override - public Member save(Member member) { - entityManager.persist(member); - return member; - } - - @Override - public Optional findById(Long memberId) { - Member member = entityManager.find(Member.class, memberId); - - return Optional.ofNullable(member); - } - - @Override - public Optional findByEmailAndPassword(String email, String password) { - String jpql = "SELECT m FROM member AS m WHERE m.email = :email AND m.password = :password"; - List members = entityManager.createQuery(jpql, Member.class) - .setParameter("email", email) - .setParameter("password", password) - .getResultList(); - - return members.stream().findFirst(); - } - - @Override - public Optional findByName(String name) { - String jpql = "SELECT m FROM member AS m WHERE m.name = :name"; - List members = entityManager.createQuery(jpql, Member.class) - .setParameter("name", name) - .getResultList(); - - return members.stream().findFirst(); - } -} diff --git a/src/main/java/roomescape/member/repository/MemberRepository.java b/src/main/java/roomescape/member/repository/MemberRepository.java index d475135e0..f5be3ddee 100644 --- a/src/main/java/roomescape/member/repository/MemberRepository.java +++ b/src/main/java/roomescape/member/repository/MemberRepository.java @@ -1,16 +1,16 @@ package roomescape.member.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import roomescape.member.entity.Member; import java.util.Optional; -public interface MemberRepository { - - Member save(Member member); - - Optional findById(Long memberId); - +@Repository +public interface MemberRepository extends JpaRepository { Optional findByEmailAndPassword(String email, String password); Optional findByName(String name); + + Optional findByEmail(String email); } diff --git a/src/main/java/roomescape/member/service/MemberService.java b/src/main/java/roomescape/member/service/MemberService.java index cb3f8b91f..742928830 100644 --- a/src/main/java/roomescape/member/service/MemberService.java +++ b/src/main/java/roomescape/member/service/MemberService.java @@ -11,7 +11,7 @@ @Service @Transactional(readOnly = true) public class MemberService { - private MemberRepository memberRepository; + private final MemberRepository memberRepository; public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; @@ -19,7 +19,12 @@ public MemberService(MemberRepository memberRepository) { @Transactional public MemberResponse create(MemberRequest memberRequest) { - Member member = memberRepository.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), Role.USER)); - return new MemberResponse(member.getId(), member.getName(), member.getEmail()); + Member member = memberRepository.save(Member.of( + memberRequest.name(), + memberRequest.email(), + memberRequest.password(), + Role.USER + )); + return MemberResponse.from(member); } } diff --git a/src/main/java/roomescape/reservation/controller/ReservationController.java b/src/main/java/roomescape/reservation/controller/ReservationController.java index 6c544963e..c2fb759e4 100644 --- a/src/main/java/roomescape/reservation/controller/ReservationController.java +++ b/src/main/java/roomescape/reservation/controller/ReservationController.java @@ -14,17 +14,21 @@ import roomescape.reservation.dto.ReservationRequest; import roomescape.reservation.dto.ReservationResponse; import roomescape.reservation.service.ReservationService; +import roomescape.waiting.service.WaitingService; import java.net.URI; import java.util.List; +import java.util.stream.Stream; @RestController public class ReservationController { private final ReservationService reservationService; + private final WaitingService waitingService; - public ReservationController(ReservationService reservationService) { + public ReservationController(ReservationService reservationService, WaitingService waitingService) { this.reservationService = reservationService; + this.waitingService = waitingService; } @GetMapping("/reservations") @@ -34,16 +38,20 @@ public List list() { @GetMapping("/reservations-mine") public ResponseEntity> getUserReservations(@Login LoginMember loginMember) { - List responseList = reservationService.findReservationsByMember(loginMember); - - return ResponseEntity.ok(responseList); + List responses = Stream.concat( + reservationService.findReservationsByMember(loginMember) + .stream(), + waitingService.findWaitingsByMember(loginMember) + .stream() + ).toList(); + return ResponseEntity.ok(responses); } @PostMapping("/reservations") public ResponseEntity create(@Valid @RequestBody ReservationRequest reservationRequest, @Login LoginMember loginMember) { ReservationResponse reservation = reservationService.create(reservationRequest, loginMember); - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); } @DeleteMapping("/reservations/{id}") diff --git a/src/main/java/roomescape/reservation/dto/MyReservationResponse.java b/src/main/java/roomescape/reservation/dto/MyReservationResponse.java index 3a425481e..e9612d0d3 100644 --- a/src/main/java/roomescape/reservation/dto/MyReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/MyReservationResponse.java @@ -1,37 +1,35 @@ package roomescape.reservation.dto; -public class MyReservationResponse { - private Long id; - private String theme; - private String date; - private String time; - private String status; +import roomescape.reservation.entity.Reservation; +import roomescape.waiting.dto.WaitingWithRank; +import roomescape.waiting.entity.Waiting; - public MyReservationResponse(Long id, String theme, String date, String time, String status) { - this.id = id; - this.theme = theme; - this.date = date; - this.time = time; - this.status = status; - } - - public Long getId() { - return id; - } - - public String getTheme() { - return theme; - } - - public String getDate() { - return date; - } +public record MyReservationResponse( + Long id, + String theme, + String date, + String time, + String status +) { - public String getTime() { - return time; + public static MyReservationResponse fromReservation(Reservation reservation, String status) { + return new MyReservationResponse( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate().toString(), + reservation.getTime().getTimeValue(), + status + ); } - public String getStatus() { - return status; + public static MyReservationResponse fromWaitingWithRank(WaitingWithRank waitingWithRank) { + Waiting waiting = waitingWithRank.waiting(); + return new MyReservationResponse( + waiting.getId(), + waiting.getTheme().getName(), + waiting.getDate().toString(), + waiting.getTime().getTimeValue(), + (waitingWithRank.rank() + 1) + "번째 예약대기" + ); } } diff --git a/src/main/java/roomescape/reservation/dto/ReservationRequest.java b/src/main/java/roomescape/reservation/dto/ReservationRequest.java index 56f6ec41a..9d8f6849f 100644 --- a/src/main/java/roomescape/reservation/dto/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/dto/ReservationRequest.java @@ -3,32 +3,12 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -public class ReservationRequest { - - private String name; - - @NotBlank(message = "날짜는 필수입니다.") - private String date; - - @NotNull(message = "테마는 필수입니다.") - private Long theme; - - @NotNull(message = "시간은 필수입니다.") - private Long time; - - public String getName() { - return name; - } - - public String getDate() { - return date; - } - - public Long getTheme() { - return theme; - } - - public Long getTime() { - return time; - } +public record ReservationRequest( + String name, + @NotBlank(message = "날짜는 필수입니다.") + String date, + @NotNull(message = "테마는 필수입니다.") + Long theme, + Long time +) { } diff --git a/src/main/java/roomescape/reservation/dto/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/ReservationResponse.java index 66507caf8..a3264a796 100644 --- a/src/main/java/roomescape/reservation/dto/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/ReservationResponse.java @@ -1,37 +1,22 @@ package roomescape.reservation.dto; -public class ReservationResponse { - private Long id; - private String name; - private String theme; - private String date; - private String time; +import roomescape.reservation.entity.Reservation; - public ReservationResponse(Long id, String name, String date, String time, String theme) { - this.id = id; - this.name = name; - this.date = date; - this.theme = theme; - this.time = time; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getTheme() { - return theme; - } - - public String getDate() { - return date; - } +public record ReservationResponse( + Long id, + String name, + String theme, + String date, + String time +) { - public String getTime() { - return time; + public static ReservationResponse from(Reservation reservation) { + return new ReservationResponse( + reservation.getId(), + reservation.getMember().getName(), + reservation.getTheme().getName(), + reservation.getDate().toString(), + reservation.getTime().getTimeValue() + ); } } diff --git a/src/main/java/roomescape/reservation/entity/Reservation.java b/src/main/java/roomescape/reservation/entity/Reservation.java index 7fbbf5afb..458bfb3c2 100644 --- a/src/main/java/roomescape/reservation/entity/Reservation.java +++ b/src/main/java/roomescape/reservation/entity/Reservation.java @@ -7,6 +7,12 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import roomescape.member.entity.Member; import roomescape.theme.entity.Theme; import roomescape.time.entity.Time; @@ -14,6 +20,14 @@ import java.time.LocalDate; @Entity(name = "reservation") +@Table(uniqueConstraints = { + @UniqueConstraint( + name = "uk_reservation_date_time_theme", + columnNames = {"date", "time_id", "theme_id"} + ) +}) +@NoArgsConstructor +@Getter public class Reservation { @Id @@ -24,6 +38,7 @@ public class Reservation { private LocalDate date; @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) private Member member; @ManyToOne(fetch = FetchType.LAZY) @@ -32,33 +47,19 @@ public class Reservation { @ManyToOne(fetch = FetchType.LAZY) private Theme theme; - public Reservation() { - } - - public Reservation(Member member, LocalDate date, Time time, Theme theme) { + private Reservation(Member member, LocalDate date, Time time, Theme theme) { this.member = member; this.date = date; this.time = time; this.theme = theme; } - public Long getId() { - return id; - } - - public Member getMember() { - return member; - } - - public LocalDate getDate() { - return date; - } - - public Time getTime() { - return time; - } - - public Theme getTheme() { - return theme; + public static Reservation of(Member member, LocalDate date, Time time, Theme theme) { + return new Reservation( + member, + date, + time, + theme + ); } } diff --git a/src/main/java/roomescape/reservation/exception/ReservationErrorCode.java b/src/main/java/roomescape/reservation/exception/ReservationErrorCode.java new file mode 100644 index 000000000..826531919 --- /dev/null +++ b/src/main/java/roomescape/reservation/exception/ReservationErrorCode.java @@ -0,0 +1,27 @@ +package roomescape.reservation.exception; + +import org.springframework.http.HttpStatus; +import roomescape.global.exception.ErrorCode; + +public enum ReservationErrorCode implements ErrorCode { + + DUPLICATE_RESERVATION(HttpStatus.CONFLICT, "해당 시간에 이미 예약이 존재합니다."); + + private HttpStatus httpStatus; + private String message; + + ReservationErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/roomescape/reservation/repository/JpaReservationRepository.java b/src/main/java/roomescape/reservation/repository/JpaReservationRepository.java deleted file mode 100644 index 56515d03b..000000000 --- a/src/main/java/roomescape/reservation/repository/JpaReservationRepository.java +++ /dev/null @@ -1,66 +0,0 @@ -package roomescape.reservation.repository; - -import jakarta.persistence.EntityManager; -import org.springframework.stereotype.Repository; -import roomescape.reservation.entity.Reservation; - -import java.time.LocalDate; -import java.util.List; - -@Repository -public class JpaReservationRepository implements ReservationRepository { - - private final EntityManager entityManager; - - public JpaReservationRepository(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Override - public List findAll() { - String jpql = "SELECT r FROM reservation AS r " + - "JOIN FETCH r.member " + - "JOIN FETCH r.theme " + - "JOIN FETCH r.time"; - - return entityManager.createQuery(jpql, Reservation.class) - .getResultList(); - } - - @Override - public List findAllByMemberId(Long memberId) { - String jpql = "SELECT r FROM reservation AS r " + - "JOIN FETCH r.theme " + - "JOIN FETCH r.time " + - "WHERE r.member.id = :memberId"; - - return entityManager.createQuery(jpql, Reservation.class) - .setParameter("memberId", memberId) - .getResultList(); - } - - @Override - public List findByDateAndThemeId(LocalDate date, Long themeId) { - String jpql = "SELECT r FROM reservation AS r WHERE r.date = :date AND r.theme.id = :themeId"; - - return entityManager.createQuery(jpql, Reservation.class) - .setParameter("date", date) - .setParameter("themeId", themeId) - .getResultList(); - } - - @Override - public Reservation save(Reservation reservation) { - entityManager.persist(reservation); - - return reservation; - } - - @Override - public void deleteById(Long id) { - Reservation reservation = entityManager.find(Reservation.class, id); - if (reservation != null) { - entityManager.remove(reservation); - } - } -} diff --git a/src/main/java/roomescape/reservation/repository/ReservationRepository.java b/src/main/java/roomescape/reservation/repository/ReservationRepository.java index 830455dde..45396de22 100644 --- a/src/main/java/roomescape/reservation/repository/ReservationRepository.java +++ b/src/main/java/roomescape/reservation/repository/ReservationRepository.java @@ -1,19 +1,37 @@ package roomescape.reservation.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; import roomescape.reservation.entity.Reservation; +import roomescape.theme.entity.Theme; +import roomescape.time.entity.Time; import java.time.LocalDate; import java.util.List; -public interface ReservationRepository { +@Repository +public interface ReservationRepository extends JpaRepository { + @Override + @Query("SELECT r FROM reservation r " + + "JOIN FETCH r.member " + + "JOIN FETCH r.time " + + "JOIN FETCH r.theme") List findAll(); - List findAllByMemberId(Long memberId); + @Query("SELECT r FROM reservation r " + + "JOIN FETCH r.time " + + "JOIN FETCH r.theme " + + "WHERE r.member.id = :memberId") + List findAllByMemberId(@Param("memberId") Long memberId); List findByDateAndThemeId(LocalDate date, Long themeId); - Reservation save(Reservation reservation); - - void deleteById(Long id); + boolean existsByDateAndTimeAndTheme( + @Param("date") LocalDate date, + @Param("time") Time time, + @Param("theme") Theme theme + ); } diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java index 234c48406..c726cc8d2 100644 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -3,7 +3,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import roomescape.auth.domain.LoginMember; -import roomescape.exception.ApplicationException; +import roomescape.global.exception.ApplicationException; import roomescape.member.entity.Member; import roomescape.member.exception.MemberErrorCode; import roomescape.member.repository.MemberRepository; @@ -11,6 +11,7 @@ import roomescape.reservation.dto.ReservationRequest; import roomescape.reservation.dto.ReservationResponse; import roomescape.reservation.entity.Reservation; +import roomescape.reservation.exception.ReservationErrorCode; import roomescape.reservation.repository.ReservationRepository; import roomescape.theme.entity.Theme; import roomescape.theme.repository.ThemeRepository; @@ -23,6 +24,7 @@ @Service @Transactional(readOnly = true) public class ReservationService { + private final ReservationRepository reservationRepository; private final MemberRepository memberRepository; private final TimeRepository timeRepository; @@ -37,14 +39,16 @@ public ReservationService(ReservationRepository reservationRepository, MemberRep @Transactional public ReservationResponse create(ReservationRequest request, LoginMember loginMember) { - Member member = resolveMember(request, loginMember); - LocalDate date = LocalDate.parse(request.getDate()); - Time time = timeRepository.findById(request.getTime()) + LocalDate date = LocalDate.parse(request.date()); + Time time = timeRepository.findById(request.time()) .orElseThrow(); - Theme theme = themeRepository.findById(request.getTheme()) + Theme theme = themeRepository.findById(request.theme()) .orElseThrow(); + validateDuplicateReservation(date, time, theme); + + Member member = getTargetMember(request, loginMember); - Reservation reservation = new Reservation(member, date, time, theme); + Reservation reservation = Reservation.of(member, date, time, theme); Reservation savedReservation = reservationRepository.save(reservation); return new ReservationResponse( @@ -58,24 +62,15 @@ public ReservationResponse create(ReservationRequest request, LoginMember loginM public List findAll() { return reservationRepository.findAll().stream() - .map(it -> new ReservationResponse( - it.getId(), - it.getMember().getName(), - it.getDate().toString(), - it.getTime().getTimeValue(), - it.getTheme().getName() - )) + .map(ReservationResponse::from) .toList(); } public List findReservationsByMember(LoginMember loginMember) { return reservationRepository.findAllByMemberId(loginMember.id()) .stream() - .map(reservation -> new MyReservationResponse( - reservation.getId(), - reservation.getTheme().getName(), - reservation.getDate().toString(), - reservation.getTime().getTimeValue(), + .map(reservation -> MyReservationResponse.fromReservation( + reservation, "예약" )) .toList(); @@ -86,13 +81,19 @@ public void deleteById(Long id) { reservationRepository.deleteById(id); } - private Member resolveMember(ReservationRequest reservationRequest, LoginMember loginMember) { - if (reservationRequest.getName() != null && !reservationRequest.getName().isBlank()) { - return memberRepository.findByName(reservationRequest.getName()) + private Member getTargetMember(ReservationRequest reservationRequest, LoginMember loginMember) { + if (reservationRequest.name() != null && !reservationRequest.name().isBlank()) { + return memberRepository.findByName(reservationRequest.name()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); } return memberRepository.findById(loginMember.id()) .orElseThrow(() -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); } + + private void validateDuplicateReservation(LocalDate date, Time time, Theme theme) { + if (reservationRepository.existsByDateAndTimeAndTheme(date, time, theme)) { + throw new ApplicationException(ReservationErrorCode.DUPLICATE_RESERVATION); + } + } } diff --git a/src/main/java/roomescape/theme/dto/ThemeResponse.java b/src/main/java/roomescape/theme/dto/ThemeResponse.java index e1cbb7d1d..e1a4dcdf5 100644 --- a/src/main/java/roomescape/theme/dto/ThemeResponse.java +++ b/src/main/java/roomescape/theme/dto/ThemeResponse.java @@ -1,8 +1,18 @@ package roomescape.theme.dto; +import roomescape.theme.entity.Theme; + public record ThemeResponse( Long id, String name, String description ) { + + public static ThemeResponse from(Theme theme) { + return new ThemeResponse( + theme.getId(), + theme.getName(), + theme.getDescription() + ); + } } diff --git a/src/main/java/roomescape/theme/entity/Theme.java b/src/main/java/roomescape/theme/entity/Theme.java index ef38ba4c8..dd47a9cf8 100644 --- a/src/main/java/roomescape/theme/entity/Theme.java +++ b/src/main/java/roomescape/theme/entity/Theme.java @@ -5,10 +5,14 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import org.hibernate.annotations.ColumnDefault; +import lombok.Getter; +import lombok.NoArgsConstructor; +import roomescape.global.entity.BaseSoftDeleteEntity; @Entity(name = "theme") -public class Theme { +@NoArgsConstructor +@Getter +public class Theme extends BaseSoftDeleteEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -20,27 +24,12 @@ public class Theme { @Column(nullable = false) private String description; - @Column(nullable = false) - @ColumnDefault("false") - private Boolean deleted = false; - - public Theme() { - } - - public Theme(String name, String description) { + private Theme(String name, String description) { this.name = name; this.description = description; } - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getDescription() { - return description; + public static Theme of(String name, String description) { + return new Theme(name, description); } } diff --git a/src/main/java/roomescape/theme/exception/ThemeErrorCode.java b/src/main/java/roomescape/theme/exception/ThemeErrorCode.java new file mode 100644 index 000000000..32246f835 --- /dev/null +++ b/src/main/java/roomescape/theme/exception/ThemeErrorCode.java @@ -0,0 +1,27 @@ +package roomescape.theme.exception; + +import org.springframework.http.HttpStatus; +import roomescape.global.exception.ErrorCode; + +public enum ThemeErrorCode implements ErrorCode { + + THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 시간입니다."); + + private HttpStatus httpStatus; + private String message; + + ThemeErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/roomescape/theme/repository/JpaThemeRepository.java b/src/main/java/roomescape/theme/repository/JpaThemeRepository.java deleted file mode 100644 index e70d14f55..000000000 --- a/src/main/java/roomescape/theme/repository/JpaThemeRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package roomescape.theme.repository; - -import jakarta.persistence.EntityManager; -import org.springframework.stereotype.Repository; -import roomescape.theme.entity.Theme; - -import java.util.List; -import java.util.Optional; - -@Repository -public class JpaThemeRepository implements ThemeRepository { - - private final EntityManager entityManager; - - public JpaThemeRepository(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Override - public Theme save(Theme theme) { - entityManager.persist(theme); - return theme; - } - - @Override - public List findAll() { - String jpql = "SELECT t FROM theme AS t"; - return entityManager.createQuery(jpql, Theme.class) - .getResultList(); - } - - @Override - public Optional findById(Long id) { - Theme theme = entityManager.find(Theme.class, id); - return Optional.ofNullable(theme); - } - - @Override - public void deleteById(Long id) { - findById(id) - .ifPresent(entityManager::remove); - } -} diff --git a/src/main/java/roomescape/theme/repository/ThemeRepository.java b/src/main/java/roomescape/theme/repository/ThemeRepository.java index a85cda3d5..4165b6593 100644 --- a/src/main/java/roomescape/theme/repository/ThemeRepository.java +++ b/src/main/java/roomescape/theme/repository/ThemeRepository.java @@ -1,17 +1,13 @@ package roomescape.theme.repository; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; import roomescape.theme.entity.Theme; import java.util.List; -import java.util.Optional; -public interface ThemeRepository { +@Repository +public interface ThemeRepository extends JpaRepository { - Theme save(Theme theme); - - List findAll(); - - Optional findById(Long id); - - void deleteById(Long id); + List findAllByDeletedAtNull(); } diff --git a/src/main/java/roomescape/theme/service/ThemeService.java b/src/main/java/roomescape/theme/service/ThemeService.java index 5c4f31445..f020bb4fb 100644 --- a/src/main/java/roomescape/theme/service/ThemeService.java +++ b/src/main/java/roomescape/theme/service/ThemeService.java @@ -2,9 +2,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import roomescape.global.exception.ApplicationException; import roomescape.theme.dto.ThemeRequest; import roomescape.theme.dto.ThemeResponse; import roomescape.theme.entity.Theme; +import roomescape.theme.exception.ThemeErrorCode; import roomescape.theme.repository.ThemeRepository; import java.util.List; @@ -21,29 +23,26 @@ public ThemeService(ThemeRepository themeRepository) { @Transactional public ThemeResponse create(ThemeRequest request) { - Theme theme = new Theme(request.name(), request.description()); + Theme theme = Theme.of( + request.name(), + request.description() + ); Theme savedTheme = themeRepository.save(theme); - return new ThemeResponse( - savedTheme.getId(), - savedTheme.getName(), - savedTheme.getDescription() - ); + return ThemeResponse.from(savedTheme); } public List findAll() { - return themeRepository.findAll() + return themeRepository.findAllByDeletedAtNull() .stream() - .map(theme -> new ThemeResponse( - theme.getId(), - theme.getName(), - theme.getDescription() - )) + .map(ThemeResponse::from) .toList(); } @Transactional public void deleteById(Long id) { - themeRepository.deleteById(id); + Theme theme = themeRepository.findById(id) + .orElseThrow(() -> new ApplicationException(ThemeErrorCode.THEME_NOT_FOUND)); + theme.markDeleted(); } } diff --git a/src/main/java/roomescape/time/controller/TimeController.java b/src/main/java/roomescape/time/controller/TimeController.java index f640c9d94..d6703e5af 100644 --- a/src/main/java/roomescape/time/controller/TimeController.java +++ b/src/main/java/roomescape/time/controller/TimeController.java @@ -32,12 +32,12 @@ public List list() { @PostMapping("/times") public ResponseEntity create(@RequestBody TimeRequest timeRequest) { - if (timeRequest.getValue() == null || timeRequest.getValue().isEmpty()) { + if (timeRequest.value() == null || timeRequest.value().isEmpty()) { throw new RuntimeException(); } - TimeResponse newTime = timeService.create(new Time(timeRequest.getValue())); - return ResponseEntity.created(URI.create("/times/" + newTime.getId())).body(newTime); + TimeResponse newTime = timeService.create(Time.from(timeRequest.value())); + return ResponseEntity.created(URI.create("/times/" + newTime.id())).body(newTime); } @DeleteMapping("/times/{id}") diff --git a/src/main/java/roomescape/time/dto/AvailableTime.java b/src/main/java/roomescape/time/dto/AvailableTime.java index 189a23f22..4641290d5 100644 --- a/src/main/java/roomescape/time/dto/AvailableTime.java +++ b/src/main/java/roomescape/time/dto/AvailableTime.java @@ -1,25 +1,24 @@ package roomescape.time.dto; -public class AvailableTime { - private Long timeId; - private String time; - private boolean booked; +import roomescape.reservation.entity.Reservation; +import roomescape.time.entity.Time; - public AvailableTime(Long timeId, String time, boolean booked) { - this.timeId = timeId; - this.time = time; - this.booked = booked; - } +import java.util.List; - public Long getTimeId() { - return timeId; - } +public record AvailableTime( + Long id, + String time, + boolean booked +) { - public String getTime() { - return time; - } + public static AvailableTime of(Time time, List reservations) { + boolean alreadyBooked = reservations.stream() + .anyMatch(reservation -> reservation.getTime().getId().equals(time.getId())); - public boolean isBooked() { - return booked; + return new AvailableTime( + time.getId(), + time.getTimeValue(), + alreadyBooked + ); } } diff --git a/src/main/java/roomescape/time/dto/TimeRequest.java b/src/main/java/roomescape/time/dto/TimeRequest.java index 53fda82a8..a48cc65de 100644 --- a/src/main/java/roomescape/time/dto/TimeRequest.java +++ b/src/main/java/roomescape/time/dto/TimeRequest.java @@ -1,14 +1,6 @@ package roomescape.time.dto; -public class TimeRequest { - - private final String value; - - public TimeRequest(String value) { - this.value = value; - } - - public String getValue() { - return value; - } +public record TimeRequest( + String value +) { } diff --git a/src/main/java/roomescape/time/dto/TimeResponse.java b/src/main/java/roomescape/time/dto/TimeResponse.java index 84b883334..181cf28ca 100644 --- a/src/main/java/roomescape/time/dto/TimeResponse.java +++ b/src/main/java/roomescape/time/dto/TimeResponse.java @@ -1,20 +1,16 @@ package roomescape.time.dto; -public class TimeResponse { +import roomescape.time.entity.Time; - private final Long id; - private final String value; +public record TimeResponse( + Long id, + String value +) { - public TimeResponse(Long id, String value) { - this.id = id; - this.value = value; - } - - public Long getId() { - return id; - } - - public String getValue() { - return value; + public static TimeResponse from(Time time) { + return new TimeResponse( + time.getId(), + time.getTimeValue() + ); } } diff --git a/src/main/java/roomescape/time/entity/Time.java b/src/main/java/roomescape/time/entity/Time.java index fde1aad96..1d6157c0e 100644 --- a/src/main/java/roomescape/time/entity/Time.java +++ b/src/main/java/roomescape/time/entity/Time.java @@ -5,10 +5,14 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import org.hibernate.annotations.ColumnDefault; +import lombok.Getter; +import lombok.NoArgsConstructor; +import roomescape.global.entity.BaseSoftDeleteEntity; @Entity(name = "time") -public class Time { +@NoArgsConstructor +@Getter +public class Time extends BaseSoftDeleteEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -17,21 +21,11 @@ public class Time { @Column(length = 20, nullable = false) private String timeValue; - @Column(nullable = false) - @ColumnDefault("false") - private Boolean deleted = false; - - public Time() {} - - public Time(String value) { + private Time(String value) { this.timeValue = value; } - public Long getId() { - return id; - } - - public String getTimeValue() { - return timeValue; + public static Time from(String timeValue) { + return new Time(timeValue); } } diff --git a/src/main/java/roomescape/time/exception/TimeErrorCode.java b/src/main/java/roomescape/time/exception/TimeErrorCode.java new file mode 100644 index 000000000..d710e3ecb --- /dev/null +++ b/src/main/java/roomescape/time/exception/TimeErrorCode.java @@ -0,0 +1,27 @@ +package roomescape.time.exception; + +import org.springframework.http.HttpStatus; +import roomescape.global.exception.ErrorCode; + +public enum TimeErrorCode implements ErrorCode { + + TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 시간입니다."); + + private HttpStatus httpStatus; + private String message; + + TimeErrorCode(HttpStatus httpStatus, String message) { + this.httpStatus = httpStatus; + this.message = message; + } + + @Override + public HttpStatus getHttpStatus() { + return httpStatus; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/roomescape/time/repository/JpaTimeRepository.java b/src/main/java/roomescape/time/repository/JpaTimeRepository.java deleted file mode 100644 index d0c874555..000000000 --- a/src/main/java/roomescape/time/repository/JpaTimeRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package roomescape.time.repository; - -import jakarta.persistence.EntityManager; -import org.springframework.stereotype.Repository; -import roomescape.time.entity.Time; - -import java.util.List; -import java.util.Optional; - -@Repository -public class JpaTimeRepository implements TimeRepository { - - private final EntityManager entityManager; - - public JpaTimeRepository(EntityManager entityManager) { - this.entityManager = entityManager; - } - - @Override - public List