diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 000000000..072f52022 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,82 @@ +name: CI/CD Pipeline + +on: + pull_request: + branches: + - main + - gotobill-core + push: + branches: + - main + - gotobill-core + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'adopt' + java-version: '17' + + - name: Install Docker Compose + run: | + sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + + - name: Build with Gradle + run: ./gradlew clean -x test build + + - name: Copy JAR for Docker + run: cp build/libs/spring-basic-roomescape-playground-0.0.1-SNAPSHOT.jar app.jar + + - name: Upload app.jar as artifact + uses: actions/upload-artifact@v3 + with: + name: app-jar + path: ./app.jar + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Build and Push Docker Image + run: docker-compose build && docker-compose push + + deploy: + if: github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Download app.jar artifact + uses: actions/download-artifact@v3 + with: + name: app-jar + path: . + + - name: Deploy with Docker Compose + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + echo "$SSH_PRIVATE_KEY" > private_key.pem + chmod 600 private_key.pem + scp -i private_key.pem -o StrictHostKeyChecking=no \ + docker-compose.yml \ + build.sh \ + app.jar \ + Dockerfile \ + ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }}:/home/${{ secrets.EC2_USERNAME }}/ + + ssh -i private_key.pem -o StrictHostKeyChecking=no ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} "chmod +x build.sh && ./build.sh" + rm private_key.pem diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..bce495d5a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# OpenJDK 17을 기반으로 하는 이미지를 사용합니다 +FROM openjdk:17-jdk-slim + +# 컨테이너 내의 작업 디렉터리를 설정합니다 +WORKDIR /app + +# 호스트의 JAR 파일을 컨테이너로 복사합니다 +COPY ./app.jar app.jar + +# 애플리케이션이 사용할 포트를 노출합니다 +EXPOSE 8080 + +# JAR 파일을 실행하는 명령을 지정합니다 +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8d52aebc6..33424381b 100644 --- a/build.gradle +++ b/build.gradle @@ -15,9 +15,9 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' - + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' + implementation 'org.springframework.boot:spring-boot-starter' implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..7d641f6bd --- /dev/null +++ b/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# 현재 구동 중인 애플리케이션 확인 +CURRENT_APP=$(docker-compose ps | grep "app_blue" | grep "Up" || echo "") + +# 새 버전을 배포할 대상 결정 +if [[ -n "$CURRENT_APP" ]]; then + TARGET="app_green" +else + TARGET="app_blue" +fi + +echo "배포할 대상: $TARGET" + +# Docker 이미지 빌드 +docker-compose build $TARGET + +# 새로운 버전 실행 +docker-compose up -d $TARGET + +# 새로운 버전이 구동될 때까지 대기 +echo "새로운 버전의 애플리케이션이 구동될 때까지 대기 중..." +sleep 15 + +# 오래된 버전 중지 +if [[ "$TARGET" == "app_green" ]]; then + OLD_APP="app_blue" +else + OLD_APP="app_green" +fi + +echo "오래된 버전 중지: $OLD_APP" +docker-compose stop $OLD_APP \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..6e3633680 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +services: + app_blue: + build: + context: . + dockerfile: Dockerfile + image: gotobill/app_blue:latest + container_name: roomescape-app-blue + environment: + - SERVER_PORT=8081 + ports: + - "8081:8081" + restart: always + + app_green: + build: + context: . + dockerfile: Dockerfile + image: gotobill/app_green:latest + container_name: roomescape-app-green + environment: + - SERVER_PORT=8082 + ports: + - "8082:8082" + restart: always + + nginx: + build: + context: ./nginx + dockerfile: Dockerfile + image: gotobill/nginx:latest + ports: + - "80:80" + depends_on: + - app_blue + - app_green + container_name: roomescape-nginx + restart: always \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 000000000..c40aaf14a --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,5 @@ +# Nginx 이미지를 기반으로 설정합니다 +FROM nginx:alpine + +# Nginx 설정 파일을 컨테이너로 복사합니다 +COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 000000000..9f1a46081 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,18 @@ +upstream backend { + server app_blue:8081; + server app_green:8082; +} + +server { + listen 80; + + server_name 52.64.39.97; # 서버의 공인 IP 주소를 설정합니다 + + location / { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/src/main/java/auth/AuthConfig.java b/src/main/java/auth/AuthConfig.java new file mode 100644 index 000000000..73662c296 --- /dev/null +++ b/src/main/java/auth/AuthConfig.java @@ -0,0 +1,16 @@ +package auth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AuthConfig { + + @Bean + public JwtUtils jwtUtils(@Value("${roomescape.auth.jwt.secret}") String secretKey) { + return new JwtUtils(secretKey); + // reload: + // rel + } +} \ No newline at end of file diff --git a/src/main/java/auth/JwtTokenMember.java b/src/main/java/auth/JwtTokenMember.java new file mode 100644 index 000000000..817516a8b --- /dev/null +++ b/src/main/java/auth/JwtTokenMember.java @@ -0,0 +1,7 @@ +package auth; + +public record JwtTokenMember( + String name, + String role +) { +} diff --git a/src/main/java/auth/JwtUtils.java b/src/main/java/auth/JwtUtils.java new file mode 100644 index 000000000..76c99c78b --- /dev/null +++ b/src/main/java/auth/JwtUtils.java @@ -0,0 +1,44 @@ +package auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import roomescape.member.Member; + +public class JwtUtils { + + private final String secretKey; + + public JwtUtils(String secretKey) { + this.secretKey = secretKey; + } + + public String createToken(Member member){ + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("name", member.getName()) + .claim("role", member.getRole()) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } + + public JwtTokenMember extractToken(String token) { + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody(); + String name = claims.get("name", String.class); + String role = claims.get("role", String.class); + return new JwtTokenMember(name,role); + } + public Long decodeToken(String token) { + if (token == null) return null; + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + } + +} \ No newline at end of file diff --git a/src/main/java/roomescape/AppConfig.java b/src/main/java/roomescape/AppConfig.java new file mode 100644 index 000000000..4181f4fa4 --- /dev/null +++ b/src/main/java/roomescape/AppConfig.java @@ -0,0 +1,33 @@ +package roomescape; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.argumentResolver.LoginMemberArgumentResolver; +import roomescape.interceptor.AuthInterceptor; + +import java.util.List; + +@Configuration +public class AppConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final AuthInterceptor authInterceptor; + + public AppConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, AuthInterceptor authInterceptor) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.authInterceptor = authInterceptor; + } + + @Override + public void addArgumentResolvers(List argumentResolvers) { + argumentResolvers.add(loginMemberArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/admin/**"); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/Login/LoginController.java b/src/main/java/roomescape/Login/LoginController.java new file mode 100644 index 000000000..b391372ce --- /dev/null +++ b/src/main/java/roomescape/Login/LoginController.java @@ -0,0 +1,35 @@ +package roomescape.Login; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import roomescape.member.MemberResponse; + +@RestController +public class LoginController { + private final LoginService loginService; + + public LoginController(LoginService loginService) { + this.loginService = loginService; + } + + @GetMapping("check_uptodata") + public String login(){ + return "good"; + } + @PostMapping("/login") + public void login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { + String token = loginService.login(loginRequest); //토큰 받음 + Cookie cookie = new Cookie("token", token); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + } + + @GetMapping("/login/check") + public MemberResponse loginCheck( + @CookieValue("token") String token) { + return loginService.checkLogin(token); + } +} diff --git a/src/main/java/roomescape/Login/LoginMember.java b/src/main/java/roomescape/Login/LoginMember.java new file mode 100644 index 000000000..a556a236e --- /dev/null +++ b/src/main/java/roomescape/Login/LoginMember.java @@ -0,0 +1,6 @@ +package roomescape.Login; + +public record LoginMember( + Long id, String name, String email, String role +) { +} diff --git a/src/main/java/roomescape/Login/LoginRequest.java b/src/main/java/roomescape/Login/LoginRequest.java new file mode 100644 index 000000000..5dc30999b --- /dev/null +++ b/src/main/java/roomescape/Login/LoginRequest.java @@ -0,0 +1,7 @@ +package roomescape.Login; + +public record LoginRequest( + String email, + String password +) { +} \ No newline at end of file diff --git a/src/main/java/roomescape/Login/LoginService.java b/src/main/java/roomescape/Login/LoginService.java new file mode 100644 index 000000000..f978cdda1 --- /dev/null +++ b/src/main/java/roomescape/Login/LoginService.java @@ -0,0 +1,44 @@ +package roomescape.Login; + +import org.springframework.dao.DataAccessException; +import org.springframework.stereotype.Service; +import auth.JwtUtils; +import roomescape.member.Member; +import roomescape.member.MemberRepository; +import roomescape.member.MemberResponse; + +import java.util.Optional; + +@Service +public class LoginService { + private final MemberRepository memberRepository; + private final JwtUtils jwtUtils; + + public LoginService(MemberRepository memberRepository, JwtUtils jwtUtils) { + this.memberRepository = memberRepository; + this.jwtUtils = jwtUtils; + } + + public String login(LoginRequest loginRequest) { + try { + Optional member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password()); + return jwtUtils.createToken(member.get()); + } catch (DataAccessException e){ + throw new IllegalArgumentException("로그인 정보가 일치하지 않습니다."); + } + } + + public MemberResponse checkLogin(String token) { + Long userId = jwtUtils.decodeToken(token); + try { + Optional member = memberRepository.findById(userId); + return new MemberResponse(member.get().getId(), member.get().getName(), member.get().getEmail()); + } catch (DataAccessException exception){ + throw new IllegalArgumentException("존재하지 않는 회원입니다."); + } + } + + public Member findByName(String name) { + return memberRepository.findByName(name).stream().findFirst().get(); + } +} diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 2ca0f743f..91306a954 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -1,9 +1,12 @@ package roomescape; +import auth.AuthConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; @SpringBootApplication +@Import(AuthConfig.class) public class RoomescapeApplication { public static void main(String[] args) { SpringApplication.run(RoomescapeApplication.class, args); diff --git a/src/main/java/roomescape/argumentResolver/LoginMemberArgumentResolver.java b/src/main/java/roomescape/argumentResolver/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..fe5656f64 --- /dev/null +++ b/src/main/java/roomescape/argumentResolver/LoginMemberArgumentResolver.java @@ -0,0 +1,47 @@ +package roomescape.argumentResolver; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import roomescape.Login.LoginMember; +import roomescape.Login.LoginService; +import roomescape.member.Member; +import roomescape.member.MemberResponse; +import roomescape.member.MemberService; +import jakarta.servlet.http.Cookie; + +import java.util.Arrays; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + private final LoginService loginService; + + public LoginMemberArgumentResolver(LoginService loginService) { + this.loginService = loginService; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(LoginMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + + String token = Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("token")) + .findFirst() + .map(Cookie::getValue).orElseThrow(() -> new IllegalArgumentException("토큰이 없습니다.")); + + MemberResponse memberResponse = loginService.checkLogin(token); + Member member = loginService.findByName(memberResponse.getName()); + + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } + +} diff --git a/src/main/java/roomescape/data/DataLoader.java b/src/main/java/roomescape/data/DataLoader.java new file mode 100644 index 000000000..0d04d5c88 --- /dev/null +++ b/src/main/java/roomescape/data/DataLoader.java @@ -0,0 +1,29 @@ +package roomescape.data; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import roomescape.member.Member; +import roomescape.member.MemberRepository; + +@Profile("default") +@Component +public class DataLoader implements CommandLineRunner { + + private final MemberRepository memberRepository; + + public DataLoader(final MemberRepository memberRepository) { + // ci 추가한 겸 푸시 + // 브랜치 추가한 겸 다시 해볼게 + // cd + // ci-cd 확인해 보자! + this.memberRepository = memberRepository; + } + + + @Override + public void run(final String... args) throws Exception { + final Member member1 = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + final Member member2 = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/data/TestDataLoader.java b/src/main/java/roomescape/data/TestDataLoader.java new file mode 100644 index 000000000..2630bc0f0 --- /dev/null +++ b/src/main/java/roomescape/data/TestDataLoader.java @@ -0,0 +1,59 @@ +package roomescape.data; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import roomescape.member.Member; +import roomescape.member.MemberRepository; +import roomescape.reservation.Reservation; +import roomescape.reservation.ReservationRepository; +import roomescape.theme.Theme; +import roomescape.theme.ThemeRepository; +import roomescape.time.Time; +import roomescape.time.TimeRepository; + +@Profile("test") +@Component +public class TestDataLoader implements CommandLineRunner { + + private final MemberRepository memberRepository; + + private final ThemeRepository themeRepository; + + private final TimeRepository timeRepository; + + private final ReservationRepository reservationRepository; + + public TestDataLoader(final MemberRepository memberRepository, + final ThemeRepository themeRepository, + final TimeRepository timeRepository, + final ReservationRepository reservationRepository) { + this.memberRepository = memberRepository; + this.themeRepository = themeRepository; + this.timeRepository = timeRepository; + this.reservationRepository = reservationRepository; + } + + @Override + public void run(final String... args) throws Exception { + final Member member1 = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + final Member member2 = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + + final Theme theme1 = themeRepository.save(new Theme("테마1", "테마1입니다.")); + final Theme theme2 = themeRepository.save(new Theme("테마2", "테마2입니다.")); + final Theme theme3 = themeRepository.save(new Theme("테마3", "테마3입니다.")); + + final Time time1 = timeRepository.save(new Time("10:00")); + final Time time2 = timeRepository.save(new Time("12:00")); + final Time time3 = timeRepository.save(new Time("14:00")); + final Time time4 = timeRepository.save(new Time("16:00")); + final Time time5 = timeRepository.save(new Time("18:00")); + final Time time6 = timeRepository.save(new Time("20:00")); + + reservationRepository.save(new Reservation("어드민", "2024-03-01", member1, time1, theme1)); + reservationRepository.save(new Reservation("어드민", "2024-03-01", member1, time2, theme2)); + reservationRepository.save(new Reservation("어드민", "2024-03-01", member1, time3, theme3)); + + reservationRepository.save(new Reservation("브라운", "2024-03-01", member2, time4, theme1)); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/interceptor/AuthInterceptor.java b/src/main/java/roomescape/interceptor/AuthInterceptor.java new file mode 100644 index 000000000..30139e842 --- /dev/null +++ b/src/main/java/roomescape/interceptor/AuthInterceptor.java @@ -0,0 +1,44 @@ +package roomescape.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.Login.LoginService; +import roomescape.member.Member; +import jakarta.servlet.http.Cookie; + +import java.util.Arrays; +import java.util.Optional; + +@Component +public class AuthInterceptor implements HandlerInterceptor { + + private final LoginService loginService; + + public AuthInterceptor(LoginService loginService) { + this.loginService = loginService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Optional token = Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("token")) + .findFirst() + .map(Cookie::getValue); + + if (token.isEmpty()) { + response.setStatus(401); + return false; + } + + Member member = loginService.findByName(loginService.checkLogin(token.get()).getName()); + + if (member == null || !member.getRole().equals("ADMIN")) { + response.setStatus(401); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b0..99705e63c 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,12 +1,22 @@ package roomescape.member; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity public class Member { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private String password; private String role; + protected Member() { + } + public Member(Long id, String name, String email, String role) { this.id = id; this.name = name; diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java deleted file mode 100644 index 81f77f4cd..000000000 --- a/src/main/java/roomescape/member/MemberDao.java +++ /dev/null @@ -1,55 +0,0 @@ -package roomescape.member; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -public class MemberDao { - private JdbcTemplate jdbcTemplate; - - public MemberDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Member save(Member member) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO member(name, email, password, role) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, member.getName()); - ps.setString(2, member.getEmail()); - ps.setString(3, member.getPassword()); - ps.setString(4, member.getRole()); - return ps; - }, keyHolder); - - return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), "USER"); - } - - public Member findByEmailAndPassword(String email, String password) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - email, password - ); - } - - public Member findByName(String name) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE name = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - name - ); - } -} diff --git a/src/main/java/roomescape/member/MemberRepository.java b/src/main/java/roomescape/member/MemberRepository.java new file mode 100644 index 000000000..45b8d5c02 --- /dev/null +++ b/src/main/java/roomescape/member/MemberRepository.java @@ -0,0 +1,12 @@ +package roomescape.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmailAndPassword(String email, String password); + List findByName(String name); +} + diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..638b58c1b 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -4,14 +4,14 @@ @Service public class MemberService { - private MemberDao memberDao; + private MemberRepository memberRepository; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; + public MemberService(MemberRepository memberDao) { + this.memberRepository = memberDao; } public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); + Member member = memberRepository.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } } diff --git a/src/main/java/roomescape/reservation/MyReservationResponse.java b/src/main/java/roomescape/reservation/MyReservationResponse.java new file mode 100644 index 000000000..8285afc32 --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationResponse.java @@ -0,0 +1,5 @@ +package roomescape.reservation; + +public record MyReservationResponse + (Long reservationId, String theme, String date, String time, String status) { +} diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1b..5a0c2d8d7 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,23 +1,40 @@ package roomescape.reservation; +import jakarta.persistence.*; +import roomescape.member.Member; import roomescape.theme.Theme; import roomescape.time.Time; +@Entity public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String date; + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne + @JoinColumn(name = "time_id") private Time time; + @ManyToOne + @JoinColumn(name = "theme_id") private Theme theme; - public Reservation(Long id, String name, String date, Time time, Theme theme) { + protected Reservation() { + } + + public Reservation(Long id, String name, String date, Member member, Time time, Theme theme) { this.id = id; this.name = name; this.date = date; + this.member = member; this.time = time; this.theme = theme; } - public Reservation(String name, String date, Time time, Theme theme) { this.name = name; this.date = date; @@ -25,8 +42,12 @@ public Reservation(String name, String date, Time time, Theme theme) { this.theme = theme; } - public Reservation() { - + public Reservation(String name, String date, Member member, Time time, Theme theme) { + this.name = name; + this.date = date; + this.member = member; + this.time = time; + this.theme = theme; } public Long getId() { @@ -48,4 +69,8 @@ public Time getTime() { public Theme getTheme() { return theme; } + + public Member getMember() { + return member; + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..e1ab66e4f 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,12 +1,9 @@ package roomescape.reservation; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import roomescape.Login.LoginMember; +import auth.JwtUtils; import java.net.URI; import java.util.List; @@ -15,9 +12,11 @@ public class ReservationController { private final ReservationService reservationService; + private final JwtUtils jwtUtils; - public ReservationController(ReservationService reservationService) { + public ReservationController(ReservationService reservationService, JwtUtils jwtUtils) { this.reservationService = reservationService; + this.jwtUtils = jwtUtils; } @GetMapping("/reservations") @@ -25,17 +24,30 @@ public List list() { return reservationService.findAll(); } + @GetMapping("/reservations-mine") + public List findReservations(LoginMember loginMember) { + return reservationService.findMyReservations(loginMember); + } + @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null - || reservationRequest.getTheme() == null - || reservationRequest.getTime() == null) { + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, @CookieValue("token") String token, LoginMember member) { + if (reservationRequest.date() == null + || reservationRequest.theme() == null + || reservationRequest.time() == null) { return ResponseEntity.badRequest().build(); } + if (reservationRequest.name() == null) { + reservationRequest = new ReservationRequest( + member.name(), + reservationRequest.date(), + reservationRequest.theme(), + reservationRequest.time() + ); + } + ReservationResponse reservation = reservationService.save(reservationRequest); - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); } @DeleteMapping("/reservations/{id}") @@ -43,4 +55,6 @@ public ResponseEntity delete(@PathVariable Long id) { reservationService.deleteById(id); return ResponseEntity.noContent().build(); } + + } diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/ReservationDao.java deleted file mode 100644 index a4972430c..000000000 --- a/src/main/java/roomescape/reservation/ReservationDao.java +++ /dev/null @@ -1,127 +0,0 @@ -package roomescape.reservation; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.theme.Theme; -import roomescape.time.Time; - -import java.sql.PreparedStatement; -import java.util.List; - -@Repository -public class ReservationDao { - - private final JdbcTemplate jdbcTemplate; - - public ReservationDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id", - - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public Reservation save(ReservationRequest reservationRequest) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement("INSERT INTO reservation(date, name, theme_id, time_id) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, reservationRequest.getDate()); - ps.setString(2, reservationRequest.getName()); - ps.setLong(3, reservationRequest.getTheme()); - ps.setLong(4, reservationRequest.getTime()); - return ps; - }, keyHolder); - - Time time = jdbcTemplate.queryForObject("SELECT * FROM time WHERE id = ?", - (rs, rowNum) -> new Time(rs.getLong("id"), rs.getString("time_value")), - reservationRequest.getTime()); - - Theme theme = jdbcTemplate.queryForObject("SELECT * FROM theme WHERE id = ?", - (rs, rowNum) -> new Theme(rs.getLong("id"), rs.getString("name"), rs.getString("description")), - reservationRequest.getTheme()); - - return new Reservation( - keyHolder.getKey().longValue(), - reservationRequest.getName(), - reservationRequest.getDate(), - time, - theme - ); - } - - public void deleteById(Long id) { - jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); - } - - public List findReservationsByDateAndTheme(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id" + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public List findByDateAndThemeId(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id " + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } -} diff --git a/src/main/java/roomescape/reservation/ReservationRepository.java b/src/main/java/roomescape/reservation/ReservationRepository.java new file mode 100644 index 000000000..c44835f2d --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,15 @@ +package roomescape.reservation; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ReservationRepository extends JpaRepository { + + List findByDateAndThemeId(String date, Long themeId); + + List findByMemberId(Long id); +} diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f441246..aa48b2e49 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -1,24 +1,9 @@ package roomescape.reservation; -public class ReservationRequest { - private String name; - private String date; - private Long theme; - 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, + String date, + Long theme, + Long time +) { +} \ No newline at end of file diff --git a/src/main/java/roomescape/reservation/ReservationResponse.java b/src/main/java/roomescape/reservation/ReservationResponse.java index 41360a363..1961d92de 100644 --- a/src/main/java/roomescape/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/ReservationResponse.java @@ -1,37 +1,8 @@ package roomescape.reservation; -public class ReservationResponse { - private Long id; - private String name; - private String theme; - private String date; - private String time; - - public ReservationResponse(Long id, String name, String theme, String date, String time) { - this.id = id; - this.name = name; - this.theme = theme; - this.date = date; - this.time = time; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getTheme() { - return theme; - } - - public String getDate() { - return date; - } - - public String getTime() { - return time; - } +public record ReservationResponse(Long id, + String name, + String theme, + String date, + String time) { } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd3313328..d77e9f6ee 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,30 +1,86 @@ package roomescape.reservation; import org.springframework.stereotype.Service; +import roomescape.Login.LoginMember; +import roomescape.member.MemberResponse; +import roomescape.theme.Theme; +import roomescape.theme.ThemeRepository; +import roomescape.time.Time; +import roomescape.time.TimeRepository; +import roomescape.waiting.WaitingRepository; +import roomescape.waiting.dto.WaitingWithRank; +import java.util.Comparator; import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; @Service public class ReservationService { - private ReservationDao reservationDao; + private ReservationRepository reservationRepository; + private TimeRepository timeRepository; + private ThemeRepository themeRepository; + private WaitingRepository waitingRepository; - public ReservationService(ReservationDao reservationDao) { - this.reservationDao = reservationDao; + public ReservationService(ReservationRepository reservationRepository, TimeRepository timeRepository, ThemeRepository themeRepository,WaitingRepository waitingRepository) { + this.reservationRepository = reservationRepository; + this.timeRepository = timeRepository; + this.themeRepository = themeRepository; + this.waitingRepository = waitingRepository; } public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); + Optional