diff --git a/.gitignore b/.gitignore index c2065bc26..6f0ca134f 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + + +application-local.properties diff --git a/build.gradle b/build.gradle index 8d52aebc6..b53ae0b61 100644 --- a/build.gradle +++ b/build.gradle @@ -6,16 +6,22 @@ plugins { group = 'nextstep' version = '0.0.1-SNAPSHOT' -sourceCompatibility = '17' + repositories { mavenCentral() } 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 'org.springframework.boot:spring-boot-starter-validation' + + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..20ad149f6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.java.home=C:\\Program Files\\Eclipse Adoptium\\jdk-17.0.17.10-hotspot + + + diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity new file mode 100644 index 000000000..9d4d6a804 --- /dev/null +++ b/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "win32-x64-127", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/src/main/java/auth/JwtUtils.java b/src/main/java/auth/JwtUtils.java new file mode 100644 index 000000000..0eaaa736d --- /dev/null +++ b/src/main/java/auth/JwtUtils.java @@ -0,0 +1,52 @@ +package auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; + +import java.util.Date; + +public final class JwtUtils { + public static final int DEFAULT_MAX_AGE_SECONDS = 10 * 60; // 10 minutes + + private final String secretKey; + + public JwtUtils(String secretKey) { + this.secretKey = secretKey; + } + + public String extractTokenFromCookies(Cookie[] cookies) { + if (cookies == null || cookies.length == 0) { + return ""; + } + for (Cookie cookie : cookies) { + if ("token".equals(cookie.getName())) { + return cookie.getValue(); + } + } + return ""; + } + + public Claims parseClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String createToken(String subject, String name, String role) { + Date now = new Date(); + Date expiresAt = new Date(now.getTime() + (long) DEFAULT_MAX_AGE_SECONDS * 1000); + return Jwts.builder() + .setSubject(subject) + .setExpiration(expiresAt) + .claim("name", name) + .claim("role", role) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } +} + + diff --git a/src/main/java/roomescape/ExceptionController.java b/src/main/java/roomescape/ExceptionController.java deleted file mode 100644 index 4e2450f9e..000000000 --- a/src/main/java/roomescape/ExceptionController.java +++ /dev/null @@ -1,14 +0,0 @@ -package roomescape; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -@ControllerAdvice -public class ExceptionController { - @ExceptionHandler(Exception.class) - public ResponseEntity handleRuntimeException(Exception e) { - e.printStackTrace(); - return ResponseEntity.badRequest().build(); - } -} diff --git a/src/main/java/roomescape/WebConfig.java b/src/main/java/roomescape/WebConfig.java new file mode 100644 index 000000000..f46d4a6ca --- /dev/null +++ b/src/main/java/roomescape/WebConfig.java @@ -0,0 +1,33 @@ +package roomescape; + +import auth.JwtUtils; +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.auth.AdminAuthInterceptor; +import roomescape.auth.LoginMemberArgumentResolver; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + private final JwtUtils jwtUtils; + + public WebConfig(JwtUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new LoginMemberArgumentResolver(jwtUtils)); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new AdminAuthInterceptor(jwtUtils)) + .addPathPatterns("/admin", "/admin/**"); + } +} + + diff --git a/src/main/java/roomescape/auth/AdminAuthInterceptor.java b/src/main/java/roomescape/auth/AdminAuthInterceptor.java new file mode 100644 index 000000000..a9028dd00 --- /dev/null +++ b/src/main/java/roomescape/auth/AdminAuthInterceptor.java @@ -0,0 +1,57 @@ +package roomescape.auth; + +import auth.JwtUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.common.ApiError; + +import java.io.IOException; + +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final JwtUtils jwtUtils; + + public AdminAuthInterceptor(JwtUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String token = jwtUtils.extractTokenFromCookies(request.getCookies()); + if (token.isEmpty()) { + writeError(response, ApiError.UNAUTHORIZED_MISSING_TOKEN); + return false; + } + + try { + Claims claims = jwtUtils.parseClaims(token); + String role = claims.get("role", String.class); + if (!"ADMIN".equals(role)) { + writeError(response, ApiError.FORBIDDEN_ADMIN_ONLY); + return false; + } + return true; + } catch (ExpiredJwtException e) { + writeError(response, ApiError.UNAUTHORIZED_EXPIRED_TOKEN); + return false; + } catch (Exception e) { + writeError(response, ApiError.UNAUTHORIZED_INVALID_TOKEN); + return false; + } + } + + private void writeError(HttpServletResponse response, ApiError apiError) { + response.setStatus(apiError.getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + try { + String payload = "{\"code\":" + apiError.getCode() + ",\"message\":\"" + apiError.getMessage() + "\"}"; + response.getWriter().write(payload); + } catch (IOException ignored) { + } + } +} + + diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..c56239e8a --- /dev/null +++ b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java @@ -0,0 +1,55 @@ +package roomescape.auth; + +import auth.JwtUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +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.member.LoginMemberDto; + +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtUtils jwtUtils; + + public LoginMemberArgumentResolver(JwtUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().equals(LoginMemberDto.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String token = jwtUtils.extractTokenFromCookies(request.getCookies()); + + if (token.isEmpty()) { + return null; + } + + try { + Claims claims = jwtUtils.parseClaims(token); + + Long id = Long.valueOf(claims.getSubject()); + String name = claims.get("name", String.class); + String role = claims.get("role", String.class); + + return new LoginMemberDto(id, name, null, role); + } catch (ExpiredJwtException e) { + return null; + } catch (Exception e) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/common/ApiError.java b/src/main/java/roomescape/common/ApiError.java new file mode 100644 index 000000000..cc526541a --- /dev/null +++ b/src/main/java/roomescape/common/ApiError.java @@ -0,0 +1,26 @@ +package roomescape.common; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ApiError { + BAD_REQUEST_INVALID_INPUT(HttpStatus.BAD_REQUEST, 40001, "잘못된 요청입니다."), + BAD_REQUEST_ILLEGAL_STATE(HttpStatus.BAD_REQUEST, 40002, "요청을 처리할 수 없습니다."), + NOT_FOUND_RESOURCE(HttpStatus.NOT_FOUND, 40401, "리소스를 찾을 수 없습니다."), + UNAUTHORIZED_MISSING_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "토큰이 없습니다."), + UNAUTHORIZED_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40102, "토큰이 유효하지 않습니다."), + UNAUTHORIZED_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40103, "토큰이 만료되었습니다."), + FORBIDDEN_ADMIN_ONLY(HttpStatus.FORBIDDEN, 40301, "관리자 권한이 필요합니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50000, "서버 오류가 발생했습니다."); + + private final HttpStatus httpStatus; + private final int code; + private final String message; + + ApiError(HttpStatus httpStatus, int code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/common/ExceptionController.java b/src/main/java/roomescape/common/ExceptionController.java new file mode 100644 index 000000000..b9b17cd9e --- /dev/null +++ b/src/main/java/roomescape/common/ExceptionController.java @@ -0,0 +1,50 @@ +package roomescape.common; + +import jakarta.persistence.EntityNotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +@RestControllerAdvice +public class ExceptionController { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException e) { + return build(ApiError.BAD_REQUEST_INVALID_INPUT); + } + + @ExceptionHandler({IllegalStateException.class}) + public ResponseEntity> handleIllegalState(IllegalStateException e) { + return build(ApiError.BAD_REQUEST_ILLEGAL_STATE); + } + + @ExceptionHandler({NoSuchElementException.class, EntityNotFoundException.class}) + public ResponseEntity> handleNotFound(RuntimeException e) { + return build(ApiError.NOT_FOUND_RESOURCE); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleNotReadable(HttpMessageNotReadableException e) { + return build(ApiError.BAD_REQUEST_INVALID_INPUT); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnknown(Exception e) { + e.printStackTrace(); + return build(ApiError.INTERNAL_SERVER_ERROR); + } + + private ResponseEntity> build(ApiError apiError) { + Map body = new HashMap<>(); + body.put("code", apiError.getCode()); + body.put("message", apiError.getMessage()); + return ResponseEntity.status(apiError.getHttpStatus()).body(body); + } +} + + diff --git a/src/main/java/roomescape/config/AuthConfig.java b/src/main/java/roomescape/config/AuthConfig.java new file mode 100644 index 000000000..04d4181c9 --- /dev/null +++ b/src/main/java/roomescape/config/AuthConfig.java @@ -0,0 +1,17 @@ +package roomescape.config; + +import auth.JwtUtils; +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); + } +} + + diff --git a/src/main/java/roomescape/member/LoginMemberDto.java b/src/main/java/roomescape/member/LoginMemberDto.java new file mode 100644 index 000000000..b4be8b74d --- /dev/null +++ b/src/main/java/roomescape/member/LoginMemberDto.java @@ -0,0 +1,7 @@ +package roomescape.member; + +public record LoginMemberDto(Long id, String name, String email, String role) { +} + + + diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b0..d43223505 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,20 +1,30 @@ package roomescape.member; +import jakarta.persistence.*; + +@Entity public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; + @Column(unique = true) private String email; private String password; - private String role; + @Enumerated(EnumType.STRING) + private Role role; + + public Member() { + } - public Member(Long id, String name, String email, String role) { + public Member(Long id, String name, String email, Role role) { this.id = id; this.name = name; this.email = email; this.role = role; } - public Member(String name, String email, String password, String role) { + public Member(String name, String email, String password, Role role) { this.name = name; this.email = email; this.password = password; @@ -37,7 +47,7 @@ public String getPassword() { return password; } - public String getRole() { + public Role getRole() { return role; } } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0d..19d33f6de 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -1,5 +1,6 @@ package roomescape.member; +import jakarta.validation.Valid; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -8,29 +9,61 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import auth.JwtUtils; +import roomescape.util.CookieUtil; import java.net.URI; @RestController public class MemberController { - private MemberService memberService; + private final MemberService memberService; + private final JwtUtils jwtUtils; - public MemberController(MemberService memberService) { + public MemberController(MemberService memberService, JwtUtils jwtUtils) { this.memberService = memberService; + this.jwtUtils = jwtUtils; } @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { - MemberResponse member = memberService.createMember(memberRequest); - return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); + public ResponseEntity createMember(@RequestBody @Valid MemberRequestDto memberRequest) { + MemberResponseDto member = memberService.createMember(memberRequest); + return ResponseEntity.created(URI.create("/members/" + member.id())).body(member); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody @Valid MemberRequestDto memberRequest, HttpServletResponse response) { + Member member = memberService.login(memberRequest.email(), memberRequest.password()); + + String accessToken = createToken(member); + + Cookie cookie = CookieUtil.createHttpOnlyCookie("token", accessToken, JwtUtils.DEFAULT_MAX_AGE_SECONDS, false); + response.addCookie(cookie); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(HttpServletRequest request) { + String token = jwtUtils.extractTokenFromCookies(request.getCookies()); + + String name = jwtUtils.parseClaims(token).get("name", String.class); + + MemberResponseDto body = new MemberResponseDto(null, name, null); + return ResponseEntity.ok(body); + } + + public String createToken(Member member) { + return jwtUtils.createToken(member.getId().toString(), member.getName(), member.getRole().name()); + } + + public String createTokenFromEmailAndPassword(String email, String password) { + Member member = memberService.login(email, password); + return jwtUtils.createToken(member.getId().toString(), member.getName(), member.getRole().name()); } @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - Cookie cookie = new Cookie("token", ""); - cookie.setHttpOnly(true); - cookie.setPath("/"); - cookie.setMaxAge(0); + public ResponseEntity logout(HttpServletResponse response) { + Cookie cookie = CookieUtil.expireCookie("token"); response.addCookie(cookie); return ResponseEntity.ok().build(); } 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..fe7f83305 --- /dev/null +++ b/src/main/java/roomescape/member/MemberRepository.java @@ -0,0 +1,11 @@ +package roomescape.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmailAndPassword(String email, String password); +} + + diff --git a/src/main/java/roomescape/member/MemberRequest.java b/src/main/java/roomescape/member/MemberRequest.java deleted file mode 100644 index cafb79f14..000000000 --- a/src/main/java/roomescape/member/MemberRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -package roomescape.member; - -public class MemberRequest { - private String name; - private String email; - private String password; - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } -} diff --git a/src/main/java/roomescape/member/MemberRequestDto.java b/src/main/java/roomescape/member/MemberRequestDto.java new file mode 100644 index 000000000..1abf3a326 --- /dev/null +++ b/src/main/java/roomescape/member/MemberRequestDto.java @@ -0,0 +1,13 @@ +package roomescape.member; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record MemberRequestDto( + String name, + @NotBlank @Email String email, + @NotBlank @Size(min = 8) String password +) { +} + diff --git a/src/main/java/roomescape/member/MemberResponse.java b/src/main/java/roomescape/member/MemberResponse.java deleted file mode 100644 index b9fa3b97a..000000000 --- a/src/main/java/roomescape/member/MemberResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package roomescape.member; - -public class MemberResponse { - private Long id; - private String name; - private String email; - - 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 String getEmail() { - return email; - } -} diff --git a/src/main/java/roomescape/member/MemberResponseDto.java b/src/main/java/roomescape/member/MemberResponseDto.java new file mode 100644 index 000000000..ea2f9e8c9 --- /dev/null +++ b/src/main/java/roomescape/member/MemberResponseDto.java @@ -0,0 +1,9 @@ +package roomescape.member; + +public record MemberResponseDto(Long id, String name, String email) { + public static MemberResponseDto from(Member member) { + return new MemberResponseDto(member.getId(), member.getName(), member.getEmail()); + } +} + + diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..4f1140153 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,17 +1,30 @@ package roomescape.member; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@Transactional(readOnly = true) public class MemberService { - private MemberDao memberDao; + private final MemberRepository memberRepository; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; } - public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); - return new MemberResponse(member.getId(), member.getName(), member.getEmail()); + @Transactional + public MemberResponseDto createMember(MemberRequestDto memberRequest) { + Member memberToSave = new Member( + memberRequest.name(), + memberRequest.email(), + memberRequest.password(), + Role.USER + ); + Member savedMember = memberRepository.save(memberToSave); + return MemberResponseDto.from(savedMember); + } + + public Member login(String email, String password) { + return memberRepository.findByEmailAndPassword(email, password).orElseThrow(); } } diff --git a/src/main/java/roomescape/member/Role.java b/src/main/java/roomescape/member/Role.java new file mode 100644 index 000000000..4f396ad10 --- /dev/null +++ b/src/main/java/roomescape/member/Role.java @@ -0,0 +1,8 @@ +package roomescape.member; + +public enum Role { + USER, + ADMIN +} + + diff --git a/src/main/java/roomescape/reservation/MyReservationResponse.java b/src/main/java/roomescape/reservation/MyReservationResponse.java new file mode 100644 index 000000000..a02945219 --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationResponse.java @@ -0,0 +1,38 @@ +package roomescape.reservation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; + +@Getter +public class MyReservationResponse { + private Long reservationId; + private String theme; + private String date; + private String time; + private String status; + + public MyReservationResponse(Long reservationId, String theme, String date, String time, String status) { + this.reservationId = reservationId; + this.theme = theme; + this.date = date; + this.time = time; + this.status = status; + } + + public static MyReservationResponse from(Reservation reservation) { + return new MyReservationResponse( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue(), + "예약" + ); + } + + @JsonIgnore + public Long getId() { + return reservationId; + } +} + + diff --git a/src/main/java/roomescape/reservation/MyReservationResponseDto.java b/src/main/java/roomescape/reservation/MyReservationResponseDto.java new file mode 100644 index 000000000..63c119d99 --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationResponseDto.java @@ -0,0 +1,19 @@ +package roomescape.reservation; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public record MyReservationResponseDto(Long reservationId, String theme, String date, String time, String status) { + + public static MyReservationResponseDto from(Reservation reservation) { + return new MyReservationResponseDto( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue(), + "예약" + ); + } + +} + + diff --git a/src/main/java/roomescape/reservation/MyReservationService.java b/src/main/java/roomescape/reservation/MyReservationService.java new file mode 100644 index 000000000..84fe85868 --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationService.java @@ -0,0 +1,44 @@ +package roomescape.reservation; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.waiting.WaitingService; +import roomescape.waiting.WaitingWithRankDto; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Transactional(readOnly = true) +public class MyReservationService { + private final ReservationRepository reservationRepository; + private final WaitingService waitingService; + + public MyReservationService(ReservationRepository reservationRepository, WaitingService waitingService) { + this.reservationRepository = reservationRepository; + this.waitingService = waitingService; + } + + public List findMine(Long memberId) { + List result = new ArrayList<>(); + result.addAll( + reservationRepository.findByMember_Id(memberId).stream() + .map(MyReservationResponseDto::from) + .toList() + ); + List waitings = waitingService.findMineWithRank(memberId); + for (WaitingWithRankDto w : waitings) { + long rankOneBased = (w.rank() == null ? 0 : w.rank()) + 1; + result.add(new MyReservationResponseDto( + w.waiting().getId(), + w.waiting().getTheme().getName(), + w.waiting().getDate(), + w.waiting().getTime().getValue(), + rankOneBased + "번째 예약대기" + )); + } + return result; + } +} + + diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1b..e6175a8d5 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,15 +1,28 @@ 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(fetch = FetchType.LAZY) private Time time; + + @ManyToOne(fetch = FetchType.LAZY) private Theme theme; + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + public Reservation(Long id, String name, String date, Time time, Theme theme) { this.id = id; this.name = name; @@ -48,4 +61,12 @@ public Time getTime() { public Theme getTheme() { return theme; } + + public Member getMember() { + return member; + } + + public void setMember(Member member) { + this.member = member; + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..155d59f97 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 jakarta.validation.Valid; 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.member.LoginMemberDto; import java.net.URI; import java.util.List; @@ -15,27 +12,54 @@ public class ReservationController { private final ReservationService reservationService; + private final MyReservationService myReservationService; - public ReservationController(ReservationService reservationService) { + public ReservationController(ReservationService reservationService, MyReservationService myReservationService) { this.reservationService = reservationService; + this.myReservationService = myReservationService; } @GetMapping("/reservations") - public List list() { + public List list() { + return reservationService.findAll(); + } + + @GetMapping("/admin/reservations") + public List adminList() { return reservationService.findAll(); } @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 @Valid ReservationRequestDto reservationRequest, LoginMemberDto member) { + + String effectiveName = reservationRequest.name() != null && !reservationRequest.name().isBlank() + ? reservationRequest.name() + : (member != null ? member.name() : null); + if (effectiveName == null || effectiveName.isBlank()) { return ResponseEntity.badRequest().build(); } - ReservationResponse reservation = reservationService.save(reservationRequest); + ReservationRequestDto requestWithName = new ReservationRequestDto( + effectiveName, + reservationRequest.date(), + reservationRequest.theme(), + reservationRequest.time() + ); - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + ReservationResponseDto reservation = reservationService.save( + requestWithName, + member != null ? member.id() : null + ); + + return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); + } + + @PostMapping("/admin/reservations") + public ResponseEntity adminCreate(@RequestBody @Valid ReservationRequestDto reservationRequest) { + if (reservationRequest.name() == null || reservationRequest.name().isBlank()) { + return ResponseEntity.badRequest().build(); + } + ReservationResponseDto reservation = reservationService.save(reservationRequest, null); + return ResponseEntity.created(URI.create("/admin/reservations/" + reservation.id())).body(reservation); } @DeleteMapping("/reservations/{id}") @@ -43,4 +67,15 @@ public ResponseEntity delete(@PathVariable Long id) { reservationService.deleteById(id); return ResponseEntity.noContent().build(); } + + @DeleteMapping("/admin/reservations/{id}") + public ResponseEntity adminDelete(@PathVariable Long id) { + reservationService.deleteById(id); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/reservations-mine") + public ResponseEntity> mine(LoginMemberDto member) { + return ResponseEntity.ok(myReservationService.findMine(member.id())); + } } 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..acb5ec036 --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,24 @@ +package roomescape.reservation; + +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReservationRepository extends JpaRepository { + + List findAll(); + + @EntityGraph(attributePaths = {"theme", "time"}) + List findByMember_Id(Long memberId); + + List findByDateAndTheme_Id(String date, Long themeId); + + boolean existsByMember_IdAndDateAndTime_IdAndTheme_Id(Long memberId, String date, Long timeId, Long themeId); + + default boolean existsForMemberOnSlot(Long memberId, String date, Long timeId, Long themeId) { + return existsByMember_IdAndDateAndTime_IdAndTheme_Id(memberId, date, timeId, themeId); + } +} + + diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f441246..150a2d708 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -1,11 +1,26 @@ package roomescape.reservation; +import jakarta.validation.constraints.NotNull; + public class ReservationRequest { private String name; + @NotNull private String date; + @NotNull private Long theme; + @NotNull private Long time; + public ReservationRequest() { + } + + public ReservationRequest(String name, String date, Long theme, Long time) { + this.name = name; + this.date = date; + this.theme = theme; + this.time = time; + } + public String getName() { return name; } diff --git a/src/main/java/roomescape/reservation/ReservationRequestDto.java b/src/main/java/roomescape/reservation/ReservationRequestDto.java new file mode 100644 index 000000000..176b38b91 --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRequestDto.java @@ -0,0 +1,8 @@ +package roomescape.reservation; + +import jakarta.validation.constraints.NotNull; + +public record ReservationRequestDto(String name, @NotNull String date, @NotNull Long theme, @NotNull Long time) { +} + + diff --git a/src/main/java/roomescape/reservation/ReservationResponse.java b/src/main/java/roomescape/reservation/ReservationResponse.java index 41360a363..a825b34a3 100644 --- a/src/main/java/roomescape/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/ReservationResponse.java @@ -1,5 +1,8 @@ package roomescape.reservation; +import lombok.Getter; + +@Getter public class ReservationResponse { private Long id; private String name; @@ -14,24 +17,4 @@ public ReservationResponse(Long id, String name, String theme, String date, Stri 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; - } } diff --git a/src/main/java/roomescape/reservation/ReservationResponseDto.java b/src/main/java/roomescape/reservation/ReservationResponseDto.java new file mode 100644 index 000000000..086a5272f --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationResponseDto.java @@ -0,0 +1,5 @@ +package roomescape.reservation; + +public record ReservationResponseDto(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..1c03250e7 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,30 +1,71 @@ package roomescape.reservation; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.member.Member; +import roomescape.member.MemberRepository; +import roomescape.theme.Theme; +import roomescape.theme.ThemeRepository; +import roomescape.time.Time; +import roomescape.time.TimeRepository; import java.util.List; @Service +@Transactional(readOnly = true) public class ReservationService { - private ReservationDao reservationDao; + private final ReservationRepository reservationRepository; + private final TimeRepository timeRepository; + private final ThemeRepository themeRepository; + private final MemberRepository memberRepository; - public ReservationService(ReservationDao reservationDao) { - this.reservationDao = reservationDao; + public ReservationService( + ReservationRepository reservationRepository, + TimeRepository timeRepository, + ThemeRepository themeRepository, + MemberRepository memberRepository + ) { + this.reservationRepository = reservationRepository; + this.timeRepository = timeRepository; + this.themeRepository = themeRepository; + this.memberRepository = memberRepository; } - public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); + @Transactional + public ReservationResponseDto save(ReservationRequestDto reservationRequest, Long loginMemberId) { + Time time = timeRepository.findById(reservationRequest.time()).orElseThrow(); + Theme theme = themeRepository.findById(reservationRequest.theme()).orElseThrow(); + Reservation reservation = new Reservation(reservationRequest.name(), reservationRequest.date(), time, theme); + if (loginMemberId != null) { + Member memberRef = memberRepository.getReferenceById(loginMemberId); + reservation.setMember(memberRef); + } + reservation = reservationRepository.save(reservation); - return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + return new ReservationResponseDto( + reservation.getId(), + reservation.getName(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue() + ); } + @Transactional public void deleteById(Long id) { - reservationDao.deleteById(id); + reservationRepository.deleteById(id); } - public List findAll() { - return reservationDao.findAll().stream() - .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) + public List findAll() { + return reservationRepository.findAll().stream() + .map(it -> new ReservationResponseDto( + it.getId(), + it.getName(), + it.getTheme().getName(), + it.getDate(), + it.getTime().getValue() + )) .toList(); } + } diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/Theme.java index 430a6239c..bc6fc5055 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/Theme.java @@ -1,6 +1,15 @@ package roomescape.theme; +import jakarta.persistence.*; +import roomescape.reservation.Reservation; + +import java.util.List; + +@Entity public class Theme { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; @@ -8,6 +17,9 @@ public class Theme { public Theme() { } + @OneToMany(mappedBy = "theme") + private List reservations; + public Theme(Long id, String name, String description) { this.id = id; this.name = name; diff --git a/src/main/java/roomescape/theme/ThemeController.java b/src/main/java/roomescape/theme/ThemeController.java index 03bca41a6..4f370203b 100644 --- a/src/main/java/roomescape/theme/ThemeController.java +++ b/src/main/java/roomescape/theme/ThemeController.java @@ -1,38 +1,50 @@ package roomescape.theme; 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 java.net.URI; import java.util.List; @RestController public class ThemeController { - private ThemeDao themeDao; + private ThemeRepository themeRepository; - public ThemeController(ThemeDao themeDao) { - this.themeDao = themeDao; + public ThemeController(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; } @PostMapping("/themes") public ResponseEntity createTheme(@RequestBody Theme theme) { - Theme newTheme = themeDao.save(theme); + Theme newTheme = themeRepository.save(theme); return ResponseEntity.created(URI.create("/themes/" + newTheme.getId())).body(newTheme); } + @PostMapping("/admin/themes") + public ResponseEntity adminCreateTheme(@RequestBody Theme theme) { + Theme newTheme = themeRepository.save(theme); + return ResponseEntity.created(URI.create("/admin/themes/" + newTheme.getId())).body(newTheme); + } + @GetMapping("/themes") public ResponseEntity> list() { - return ResponseEntity.ok(themeDao.findAll()); + return ResponseEntity.ok(themeRepository.findAll()); + } + + @GetMapping("/admin/themes") + public ResponseEntity> adminList() { + return ResponseEntity.ok(themeRepository.findAll()); } @DeleteMapping("/themes/{id}") public ResponseEntity deleteTheme(@PathVariable Long id) { - themeDao.deleteById(id); + themeRepository.deleteById(id); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/admin/themes/{id}") + public ResponseEntity adminDeleteTheme(@PathVariable Long id) { + themeRepository.deleteById(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/theme/ThemeDao.java b/src/main/java/roomescape/theme/ThemeDao.java deleted file mode 100644 index 945341d8d..000000000 --- a/src/main/java/roomescape/theme/ThemeDao.java +++ /dev/null @@ -1,41 +0,0 @@ -package roomescape.theme; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public class ThemeDao { - private JdbcTemplate jdbcTemplate; - - public ThemeDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query("SELECT * FROM theme where deleted = false", (rs, rowNum) -> new Theme( - rs.getLong("id"), - rs.getString("name"), - rs.getString("description") - )); - } - - public Theme save(Theme theme) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO theme(name, description) VALUES (?, ?)", new String[]{"id"}); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getDescription()); - return ps; - }, keyHolder); - - return new Theme(keyHolder.getKey().longValue(), theme.getName(), theme.getDescription()); - } - - public void deleteById(Long id) { - jdbcTemplate.update("UPDATE theme SET deleted = true WHERE id = ?", id); - } -} diff --git a/src/main/java/roomescape/theme/ThemeRepository.java b/src/main/java/roomescape/theme/ThemeRepository.java new file mode 100644 index 000000000..ff68970b5 --- /dev/null +++ b/src/main/java/roomescape/theme/ThemeRepository.java @@ -0,0 +1,8 @@ +package roomescape.theme; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ThemeRepository extends JpaRepository { +} + + diff --git a/src/main/java/roomescape/time/AvailableTimeDto.java b/src/main/java/roomescape/time/AvailableTimeDto.java new file mode 100644 index 000000000..79aa3cd5d --- /dev/null +++ b/src/main/java/roomescape/time/AvailableTimeDto.java @@ -0,0 +1,6 @@ +package roomescape.time; + +public record AvailableTimeDto(Long timeId, String time, boolean booked) { +} + + diff --git a/src/main/java/roomescape/time/Time.java b/src/main/java/roomescape/time/Time.java index 008ed93cf..52c3e3dfe 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/Time.java @@ -1,7 +1,17 @@ package roomescape.time; +import jakarta.persistence.*; +import roomescape.reservation.Reservation; + +import java.util.List; + +@Entity public class Time { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "time_value") private String value; public Time(Long id, String value) { @@ -9,6 +19,9 @@ public Time(Long id, String value) { this.value = value; } + @OneToMany(mappedBy = "time") + private List reservations; + public Time(String value) { this.value = value; } @@ -24,4 +37,5 @@ public Long getId() { public String getValue() { return value; } + } diff --git a/src/main/java/roomescape/time/TimeController.java b/src/main/java/roomescape/time/TimeController.java index 2343114d1..b8b6de442 100644 --- a/src/main/java/roomescape/time/TimeController.java +++ b/src/main/java/roomescape/time/TimeController.java @@ -1,13 +1,7 @@ package roomescape.time; 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.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.net.URI; import java.util.List; @@ -25,6 +19,11 @@ public List