diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 00000000..8bfbf95f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# 프로젝트 경로 설정 +REPOSITORY=/home/ubuntu/app +PROJECT_NAME=spring-basic-roomescape-playground + +# 프로젝트 디렉토리로 이동 +cd $REPOSITORY/$PROJECT_NAME +echo "> Git Pull" +git pull origin main + +echo "> 프로젝트 빌드 시작" +./gradlew clean build + +echo "> 빌드 파일 복사" +cp $REPOSITORY/$PROJECT_NAME/build/libs/*.jar $REPOSITORY/ + +echo "> 현재 구동 중인 애플리케이션 PID 확인" +CURRENT_PID=$(lsof -i tcp:8080 | awk 'NR!=1 {print $2}' | sort -u) + +if [ -z "$CURRENT_PID" ]; then + echo "> 현재 구동 중인 애플리케이션이 없으므로 종료하지 않습니다." +else + echo "> kill -9 $CURRENT_PID" + kill -9 $CURRENT_PID + sleep 3 +fi + +echo "> 새 애플리케이션 배포" +JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1) +echo "> JAR Name: $JAR_NAME" + +# 애플리케이션 실행 +nohup java -jar \ + -Dspring.config.location=classpath:/application.yml,/home/ubuntu/app/application-prod.yml \ + -Dspring.profiles.active=prod \ + $JAR_NAME 2>&1 & diff --git a/src/main/java/jwt/AuthConfig.java b/src/main/java/jwt/AuthConfig.java new file mode 100644 index 00000000..7753004a --- /dev/null +++ b/src/main/java/jwt/AuthConfig.java @@ -0,0 +1,15 @@ +package jwt; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(JwtProperties.class) +public class AuthConfig { + + @Bean + public JwtUtils jwtUtils(JwtProperties properties) { + return new JwtUtils(properties.secret(), properties.expiration()); + } +} diff --git a/src/main/java/jwt/JwtProperties.java b/src/main/java/jwt/JwtProperties.java new file mode 100644 index 00000000..b5cac7fc --- /dev/null +++ b/src/main/java/jwt/JwtProperties.java @@ -0,0 +1,7 @@ +package jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "roomescape.auth.jwt") +public record JwtProperties(String secret, long expiration) { +} diff --git a/src/main/java/roomescape/auth/JwtTokenProvider.java b/src/main/java/jwt/JwtUtils.java similarity index 73% rename from src/main/java/roomescape/auth/JwtTokenProvider.java rename to src/main/java/jwt/JwtUtils.java index 8d1cdc70..462194f3 100644 --- a/src/main/java/roomescape/auth/JwtTokenProvider.java +++ b/src/main/java/jwt/JwtUtils.java @@ -1,21 +1,20 @@ -package roomescape.auth; +package jwt; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets; import java.util.Date; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; import roomescape.domain.member.Member; -@Component -public class JwtTokenProvider { +public class JwtUtils { - @Value("${roomescape.auth.jwt.secret}") - private String secretKey; - @Value("${roomescape.auth.jwt.expiration}") - private long expirationTime; + private final String secretKey; + private final long expirationTime; + public JwtUtils(String secretKey, long expirationTime) { + this.secretKey = secretKey; + this.expirationTime = expirationTime; + } public String createToken(Member member) { return Jwts.builder() .setSubject(member.getId().toString()) diff --git a/src/main/java/roomescape/DataLoader.java b/src/main/java/roomescape/DataLoader.java new file mode 100644 index 00000000..124f4a82 --- /dev/null +++ b/src/main/java/roomescape/DataLoader.java @@ -0,0 +1,23 @@ +package roomescape; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import roomescape.domain.member.Member; +import roomescape.domain.member.MemberRepository; + +@Component +@Profile("dev") +@RequiredArgsConstructor +public class DataLoader implements CommandLineRunner { + + private final MemberRepository memberRepository; + + + @Override + public void run(String... args) throws Exception { + memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + } +} diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 2ca0f743..acf254d2 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = {"roomescape", "jwt"}) public class RoomescapeApplication { public static void main(String[] args) { SpringApplication.run(RoomescapeApplication.class, args); diff --git a/src/main/java/roomescape/auth/AdminAuthInterceptor.java b/src/main/java/roomescape/auth/AdminAuthInterceptor.java index 5dbef4e0..3255bed7 100644 --- a/src/main/java/roomescape/auth/AdminAuthInterceptor.java +++ b/src/main/java/roomescape/auth/AdminAuthInterceptor.java @@ -11,20 +11,18 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import roomescape.domain.member.Member; -import roomescape.domain.member.MemberService; import roomescape.exception.CustomException; @Component @RequiredArgsConstructor public class AdminAuthInterceptor implements HandlerInterceptor { - private final CookieUtil cookieUtil; + private final CookieUtils cookieUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { Cookie[] cookies = request.getCookies(); - String token = cookieUtil.extractTokenFromCookies(cookies); + String token = cookieUtils.extractTokenFromCookies(cookies); if (StringUtils.isBlank(token)) { throw new CustomException(EMPTY_TOKEN); diff --git a/src/main/java/roomescape/auth/AuthController.java b/src/main/java/roomescape/auth/AuthController.java index 5b1bf2a5..a9e88e00 100644 --- a/src/main/java/roomescape/auth/AuthController.java +++ b/src/main/java/roomescape/auth/AuthController.java @@ -16,13 +16,13 @@ public class AuthController { private final AuthService authService; - private final CookieUtil cookieUtil; + private final CookieUtils cookieUtils; @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse response) { String token = authService.login(loginRequest); - response.addCookie(cookieUtil.createTokenCookie(token)); + response.addCookie(cookieUtils.createTokenCookie(token)); return ResponseEntity.ok().build(); } @@ -39,7 +39,7 @@ public ResponseEntity checkLogin(HttpServletRequest request, @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse response) { - response.addCookie(cookieUtil.createLogoutCookie()); + response.addCookie(cookieUtils.createLogoutCookie()); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/roomescape/auth/AuthInterceptor.java b/src/main/java/roomescape/auth/AuthInterceptor.java index d099c582..c1e74414 100644 --- a/src/main/java/roomescape/auth/AuthInterceptor.java +++ b/src/main/java/roomescape/auth/AuthInterceptor.java @@ -7,6 +7,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jwt.JwtUtils; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -19,20 +20,20 @@ public class AuthInterceptor implements HandlerInterceptor { private final MemberService memberService; - private final JwtTokenProvider jwtTokenProvider; - private final CookieUtil cookieUtil; + private final JwtUtils jwtUtils; + private final CookieUtils cookieUtils; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Cookie[] cookies = request.getCookies(); - String token = cookieUtil.extractTokenFromCookies(cookies); + String token = cookieUtils.extractTokenFromCookies(cookies); if (StringUtils.isBlank(token)) { throw new CustomException(EMPTY_TOKEN); } - Long memberId = jwtTokenProvider.extractMemberId(token); + Long memberId = jwtUtils.extractMemberId(token); Member member = memberService.getMemberById(memberId) .orElseThrow(() -> new CustomException(INVALID_TOKEN)); diff --git a/src/main/java/roomescape/auth/AuthService.java b/src/main/java/roomescape/auth/AuthService.java index 65c3bc3d..27eea16d 100644 --- a/src/main/java/roomescape/auth/AuthService.java +++ b/src/main/java/roomescape/auth/AuthService.java @@ -2,6 +2,7 @@ import static roomescape.exception.ErrorCode.INVALID_EMAILPASSWORD; +import jwt.JwtUtils; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import roomescape.domain.member.Member; @@ -13,11 +14,11 @@ public class AuthService { private final MemberRepository memberRepository; - private final JwtTokenProvider jwtTokenProvider; + private final JwtUtils jwtUtils; public String login(LoginRequest loginRequest) { Member member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) .orElseThrow(() -> new CustomException(INVALID_EMAILPASSWORD)); - return jwtTokenProvider.createToken(member); + return jwtUtils.createToken(member); } } diff --git a/src/main/java/roomescape/auth/CookieUtil.java b/src/main/java/roomescape/auth/CookieUtils.java similarity index 89% rename from src/main/java/roomescape/auth/CookieUtil.java rename to src/main/java/roomescape/auth/CookieUtils.java index 4ceaa913..d6b016cd 100644 --- a/src/main/java/roomescape/auth/CookieUtil.java +++ b/src/main/java/roomescape/auth/CookieUtils.java @@ -5,11 +5,11 @@ import org.springframework.stereotype.Component; @Component -public class CookieUtil { +public class CookieUtils { private final String cookieName; - public CookieUtil(@Value("${roomescape.auth.cookie.name:token}") String cookieName) { + public CookieUtils(@Value("${roomescape.auth.cookie.name:token}") String cookieName) { this.cookieName = cookieName; } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 00000000..7e9e6273 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,16 @@ +spring: + h2: + console: + enabled: true + path: /h2-console + datasource: + url: jdbc:h2:mem:database + driver-class-name: org.h2.Driver + jpa: + ddl-auto: create-drop + defer-datasource-initialization: true +roomescape: + auth: + jwt: + secret: Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= + expiration: 300000 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..eb7ec043 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,15 @@ +spring: + h2: + console: + enabled: false + datasource: + url: jdbc:h2:mem:database + driver-class-name: org.h2.Driver + jpa: + ddl-auto: update + defer-datasource-initialization: false +roomescape: + auth: + jwt: + secret: Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= + expiration: 3600000 diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..6243316d --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,16 @@ +spring: + h2: + console: + enabled: true + path: /h2-console + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + jpa: + ddl-auto: create-drop + defer-datasource-initialization: true +roomescape: + auth: + jwt: + secret: Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= + expiration: 300000 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index df76501a..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,15 +0,0 @@ -spring.sql.init.encoding=utf-8 -spring.h2.console.enabled=true -spring.h2.console.path=/h2-console -spring.datasource.url=jdbc:h2:mem:database - -spring.jpa.open-in-view=false -spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true -spring.jpa.ddl-auto=create-drop -spring.jpa.defer-datasource-initialization=true - -roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= -#Access Token expiration (1h = 3600000ms) -roomescape.auth.jwt.expiration=3600000 -roomescape.auth.cookie.name=token diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..0e3eacb0 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + sql: + init: + encoding: utf-8 + jpa: + open-in-view: false + show-sql: true + properties: + hibernate: + format_sql: true +roomescape: + auth: + cookie: + name: token diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql deleted file mode 100644 index 08417db2..00000000 --- a/src/main/resources/schema.sql +++ /dev/null @@ -1,24 +0,0 @@ -INSERT INTO member (name, email, password, role) -VALUES ('어드민', 'admin@email.com', 'password', 'ADMIN'), - ('브라운', 'brown@email.com', 'password', 'USER'); - -INSERT INTO theme (name, description, deleted) -VALUES ('테마1', '테마1입니다.', FALSE), - ('테마2', '테마2입니다.', FALSE), - ('테마3', '테마3입니다.', FALSE); - -INSERT INTO time (time_value, deleted) -VALUES ('10:00', FALSE), - ('12:00', FALSE), - ('14:00', FALSE), - ('16:00', FALSE), - ('18:00', FALSE), - ('20:00', FALSE); - -INSERT INTO reservation (member_id, name, date, time_id, theme_id) -VALUES (1, '', '2024-03-01', 1, 1), - (1, '', '2024-03-01', 2, 2), - (1, '', '2024-03-01', 3, 3); - -INSERT INTO reservation (name, date, time_id, theme_id) -VALUES ('브라운', '2024-03-01', 1, 2); diff --git a/src/test/java/roomescape/JpaTest.java b/src/test/java/roomescape/JpaTest.java index 0820d5cf..a5a7aa8e 100644 --- a/src/test/java/roomescape/JpaTest.java +++ b/src/test/java/roomescape/JpaTest.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; import roomescape.domain.member.MemberRepository; import roomescape.domain.reservation.ReservationRepository; @@ -15,6 +16,7 @@ import roomescape.domain.time.Time; import roomescape.domain.time.TimeRepository; +@ActiveProfiles("test") @DataJpaTest @ContextConfiguration(classes = {TimeRepository.class, ReservationRepository.class, MemberRepository.class, ThemeRepository.class, RoomescapeApplication.class}) diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index a808ade7..72aa0c2c 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -6,13 +6,17 @@ import io.restassured.response.Response; import jakarta.transaction.Transactional; import java.util.List; +import jwt.JwtUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.stereotype.Component; import org.springframework.test.annotation.DirtiesContext; import java.util.HashMap; import java.util.Map; +import org.springframework.test.context.ActiveProfiles; import roomescape.domain.reservation.dto.MyReservationResponse; import roomescape.domain.waiting.dto.WaitingResponse; import roomescape.auth.LoginRequest; @@ -20,11 +24,15 @@ import static org.assertj.core.api.Assertions.assertThat; +@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) @Transactional public class MissionStepTest { + @Value("${roomescape.auth.jwt.secret}") + private String secretKey; + @Test @DisplayName("로그인 성공 시 토큰이 포함된 쿠키를 반환한다") void 일단계() { @@ -60,12 +68,17 @@ public class MissionStepTest { String token = createToken("admin@email.com", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. Map params = new HashMap<>(); - params.put("date", "2024-03-01"); + params.put("date", "2024-01-01"); params.put("time", "1"); params.put("theme", "1"); - params.put("name", "어드민"); + Map params2 = new HashMap<>(); + params2.put("date", "2024-02-01"); + params2.put("time", "1"); + params2.put("theme", "1"); + params2.put("name", "브라운"); + ExtractableResponse response = RestAssured.given().log().all() .body(params) .cookie("token", token) @@ -77,10 +90,8 @@ public class MissionStepTest { assertThat(response.statusCode()).isEqualTo(201); assertThat(response.as(ReservationResponse.class).name()).isEqualTo("어드민"); - params.put("name", "브라운"); - ExtractableResponse adminResponse = RestAssured.given().log().all() - .body(params) + .body(params2) .cookie("token", token) .contentType(ContentType.JSON) .post("/reservations") @@ -178,6 +189,18 @@ private String createToken(String email, String password) { assertThat(status).isEqualTo("1번째 예약대기"); } + + @Test + @DisplayName("@Component 없이 JwtUtils가 빈으로 등록되는지 확인한다.") + void 칠단계() { + Component componentAnnotation = JwtUtils.class.getAnnotation(Component.class); + assertThat(componentAnnotation).isNull(); + } + + @Test + void 팔단계() { + assertThat(secretKey).isNotBlank(); + } } diff --git a/src/test/java/roomescape/TestDataLoader.java b/src/test/java/roomescape/TestDataLoader.java new file mode 100644 index 00000000..7a70d1ff --- /dev/null +++ b/src/test/java/roomescape/TestDataLoader.java @@ -0,0 +1,54 @@ +package roomescape; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import roomescape.domain.member.MemberRepository; +import roomescape.domain.reservation.Reservation; +import roomescape.domain.reservation.ReservationRepository; +import roomescape.domain.theme.ThemeRepository; +import roomescape.domain.time.Time; +import roomescape.domain.member.Member; +import roomescape.domain.theme.Theme; +import roomescape.domain.time.TimeRepository; + + +@Component +@Profile("test") +public class TestDataLoader implements CommandLineRunner { + + private final MemberRepository memberRepository; + private final ThemeRepository themeRepository; + private final TimeRepository timeRepository; + private final ReservationRepository reservationRepository; + + public TestDataLoader(MemberRepository memberRepository, ThemeRepository themeRepository, + TimeRepository timeRepository, ReservationRepository reservationRepository) { + this.memberRepository = memberRepository; + this.themeRepository = themeRepository; + this.timeRepository = timeRepository; + this.reservationRepository = reservationRepository; + } + + @Override + public void run(String... args) throws Exception { + Member admin = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + Member user = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + + Theme theme1 = themeRepository.save(new Theme("테마1", "테마1입니다.")); + Theme theme2 = themeRepository.save(new Theme("테마2", "테마2입니다.")); + Theme theme3 = themeRepository.save(new Theme("테마3", "테마3입니다.")); + + Time time1 = timeRepository.save(new Time("10:00")); + Time time2 = timeRepository.save(new Time("12:00")); + Time time3 = timeRepository.save(new Time("14:00")); + Time time4 = timeRepository.save(new Time("16:00")); + Time time5 = timeRepository.save(new Time("18:00")); + Time time6 = timeRepository.save(new Time("20:00")); + + reservationRepository.save(new Reservation("", "2024-03-01", time1, theme1, admin)); + reservationRepository.save(new Reservation("", "2024-03-01", time2, theme2, admin)); + reservationRepository.save(new Reservation("", "2024-03-01", time3, theme3, admin)); + reservationRepository.save(new Reservation("브라운", "2024-03-01", time1, theme2)); + } +}