From 8242b4656b167312b3acd0791223616580495ce9 Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 02:05:13 +0900 Subject: [PATCH 01/12] =?UTF-8?q?chore:=20Auth=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AuthenticationController.java | 27 +++++++ .../auth/controller/request/LoginRequest.java | 6 ++ .../controller/request/RefreshRequest.java | 5 ++ .../controller/request/SignupRequest.java | 6 ++ .../mygardenbe/auth/jwt/domain/Token.java | 54 ++++++++++++- .../mygardenbe/auth/jwt/domain/TokenType.java | 14 ++++ .../auth/jwt/entity/TokenEntity.java | 43 +++++++++++ .../jwt/filter/JwtAuthenticationFilter.java | 68 +++++++++++++++++ .../jwt/filter/JwtExceptionHandlerFilter.java | 20 +++++ .../auth/jwt/repository/TokenRepository.java | 22 +++++- .../auth/jwt/service/JwtService.java | 75 ++++++++++++++++++ .../auth/jwt/service/MyLogoutHandler.java | 16 +++- .../mygardenbe/auth/jwt/util/JwtAuthUtil.java | 19 +++++ .../auth/service/AuthenticationService.java | 76 +++++++++++++++++++ .../response/AuthenticationResponse.java | 6 ++ 15 files changed, 454 insertions(+), 3 deletions(-) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/AuthenticationController.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/AuthenticationController.java index dbfa6a1c..8fe247e7 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/AuthenticationController.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/AuthenticationController.java @@ -13,14 +13,29 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +/** + * 인증 관련 API 컨트롤러 + */ @RestController @RequiredArgsConstructor @RequestMapping(AuthenticationController.AUTH_BASE_API_PATH) public class AuthenticationController { + /** + * 인증 API의 기본 경로 + */ public static final String AUTH_BASE_API_PATH = "/api/auth"; + /** + * 인증 서비스 + */ private final AuthenticationService authenticationService; + /** + * 회원가입 + * + * @param request 회원가입 요청 객체 + * @return 회원가입의 결과로 생성된 회원 ID를 반환 + */ @PostMapping("/signup") public ApiResponse signUp(@RequestBody @Valid SignupRequest request) { final Long memberId = authenticationService.signUp(request.email(), request.password()); @@ -28,6 +43,12 @@ public ApiResponse signUp(@RequestBody @Valid SignupRequest request) { return ApiResponse.ok(memberId); } + /** + * 로그인 + * + * @param request 로그인 요청 객체 + * @return 로그인의 결과로 생성된 토큰들을 반환 (accessToken, refreshToken) + */ @PostMapping("/login") public ApiResponse login(@RequestBody @Valid LoginRequest request) { final AuthenticationResponse response = authenticationService.login(request.email(), request.password()); @@ -35,6 +56,12 @@ public ApiResponse login(@RequestBody @Valid LoginReques return ApiResponse.ok(response); } + /** + * 토큰 재발급 + * + * @param request 토큰 재발급 요청 객체 + * @return 토큰 재발급의 결과로 생성된 토큰들을 반환 (accessToken, refreshToken) + */ @PostMapping("/refresh") public ApiResponse refresh(@RequestBody @Valid RefreshRequest request) { final AuthenticationResponse response = authenticationService.refresh(request.refreshToken()); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/LoginRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/LoginRequest.java index 31ab5d7e..0efc0044 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/LoginRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/LoginRequest.java @@ -4,6 +4,12 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Builder; +/** + * 로그인 요청시에 사용하는 Request DTO + * + * @param email 이메일 + * @param password 비밀번호 + */ @Builder public record LoginRequest( @NotEmpty(message = "이메일은 null이 될 수 없습니다.") diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/RefreshRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/RefreshRequest.java index 20b37cee..1c578d54 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/RefreshRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/RefreshRequest.java @@ -2,6 +2,11 @@ import jakarta.validation.constraints.NotBlank; +/** + * 토큰 재발급 요청시에 사용하는 Request DTO [로그인시에 발급했던 리프레시 토큰을 사용하여 재발급] + * + * @param refreshToken 리프레시 토큰 + */ public record RefreshRequest( @NotBlank(message = "리프레시 토큰은 비어있을 수 없습니다.") String refreshToken diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/SignupRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/SignupRequest.java index 019bb331..9a022e75 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/SignupRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/controller/request/SignupRequest.java @@ -4,6 +4,12 @@ import jakarta.validation.constraints.NotBlank; import lombok.Builder; +/** + * 회원가입 요청시에 사용하는 Request DTO + * + * @param email 이메일 + * @param password 비밀번호 + */ @Builder public record SignupRequest( @NotBlank(message = "이메일은 null이 될 수 없습니다.") diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/Token.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/Token.java index ec75987b..2bdf6f1a 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/Token.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/Token.java @@ -5,11 +5,26 @@ import lombok.Getter; import org.springframework.util.Assert; +/** + * 토큰 Domain + */ @Getter public class Token { + /** + * 토큰 값 + */ private String tokenText; + /** + * 토큰 타입 - Bearer + */ private TokenType tokenType; + /** + * 토큰 폐기 여부 + */ private boolean revoked; + /** + * 토큰 만료 여부 + */ private boolean expired; @Builder(access = AccessLevel.PRIVATE) @@ -20,6 +35,15 @@ private Token(final String tokenText, final TokenType tokenType, final boolean r this.expired = expired; } + /** + * 토큰 Doamin 생성 + * + * @param tokenText 토큰 값 + * @param tokenType 토큰 타입 + * @param revoked 폐기 여부 + * @param expired 만료 여부 + * @return 토큰 Domain + */ public static Token of(final String tokenText, final TokenType tokenType, final boolean revoked, final boolean expired) { Assert.hasText(tokenText, "토큰은 null 혹은 빈 문자열이 될 수 없습니다."); Assert.notNull(tokenType, "토큰 타입은 null이 될 수 없습니다."); @@ -32,30 +56,58 @@ public static Token of(final String tokenText, final TokenType tokenType, final .build(); } + /** + * Bearer 타입의 토큰 생성 + * + * @param tokenText 토큰 값 + * @return Bearer 토큰 Domain + */ public static Token createBearerToken(final String tokenText) { return of(tokenText, TokenType.BEARER, false, false); } + /** + * 토큰이 유효한지 확인 + * + * @return 유효 여부 + */ public boolean isValid() { return !this.revoked && !this.expired; } + /** + * 토큰을 폐기한다. + */ public void revoke() { this.revoked = true; this.expired = true; } + /** + * 토큰을 만료한다. + */ public void expire() { this.expired = true; } + /** + * 토큰 값이 같은지 확인 + * + * @param tokenText 비교할 토큰 값 + * @return 받아온 토큰과 현재 토큰이 같은지 여부 + */ public boolean isSameTokenText(final String tokenText) { return this.tokenText.equals(tokenText); } + /** + * 토큰을 갱신한다. + * + * @param refreshTokenText 갱신할 토큰 값 + */ public void refresh(final String refreshTokenText) { Assert.hasText(refreshTokenText, "토큰은 null 혹은 빈 문자열이 될 수 없습니다."); - + this.tokenText = refreshTokenText; this.revoked = false; this.expired = false; diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/TokenType.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/TokenType.java index 275cdb35..512b831a 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/TokenType.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/domain/TokenType.java @@ -2,16 +2,30 @@ import lombok.Getter; +/** + * 토큰 타입 + */ @Getter public enum TokenType { + /** + * Bearer 타입 + */ BEARER("Bearer "); + /** + * parsing시에 사용할 Text + */ private final String parseText; TokenType(final String parseText) { this.parseText = parseText; } + /** + * parseText 길이 반환 + * + * @return parseText 길이 + */ public int getParseTextLength() { return this.parseText.length(); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/entity/TokenEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/entity/TokenEntity.java index 416020f0..fafbcd37 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/entity/TokenEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/entity/TokenEntity.java @@ -10,20 +10,46 @@ import org.hyunggi.mygardenbe.common.entity.BaseEntity; import org.springframework.util.Assert; +/** + * 토큰 Entity + */ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity public class TokenEntity extends BaseEntity { + /** + * 토큰 ID + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + /** + * 토큰 값 + */ @Getter @Column(nullable = false, unique = true) private String tokenText; + + /** + * 토큰 타입 + */ @Enumerated(EnumType.STRING) @Column(nullable = false, length = 10) private TokenType tokenType; + + /** + * 폐기 여부 + */ private boolean revoked; + + /** + * 만료 여부 + */ private boolean expired; + + /** + * 회원 ID + */ @Getter private Long memberId; @@ -36,6 +62,13 @@ private TokenEntity(final String tokenText, final TokenType tokenType, final boo this.memberId = memberId; } + /** + * 토큰 생성 + * + * @param token 토큰 Entity로 변환할 토큰 Domain + * @param memberId 회원 ID + * @return 토큰 Entity + */ public static TokenEntity of(final Token token, final Long memberId) { Assert.notNull(token, "토큰은 null이 될 수 없습니다."); Assert.isTrue(memberId != null && memberId > 0, "회원 ID는 null이 될 수 없고, 0보다 커야 합니다."); @@ -49,10 +82,20 @@ public static TokenEntity of(final Token token, final Long memberId) { .build(); } + /** + * 토큰 Entity를 토큰 Domain으로 변환 + * + * @return 토큰 Domain + */ public Token toDomain() { return Token.of(this.tokenText, this.tokenType, this.revoked, this.expired); } + /** + * 토큰 업데이트 + * + * @param token 업데이트할 토큰 + */ public void update(final Token token) { Assert.notNull(token, "토큰은 null이 될 수 없습니다."); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtAuthenticationFilter.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtAuthenticationFilter.java index 5548b7b4..fdc92c8f 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtAuthenticationFilter.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtAuthenticationFilter.java @@ -26,17 +26,39 @@ import java.io.IOException; import java.util.Collection; +/** + * JWT 인증 필터 + */ @RequiredArgsConstructor @Slf4j @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { + /** + * JWT 서비스 + */ private final JwtService jwtService; + /** + * Spring Security에서 Login시에 사용하는 UserDetailsService Interface + */ private final UserDetailsService userDetailsService; + /** + * Actuator 계정용 Email (Promehteus, Grafana 등의 모니터링 툴에서 사용) + */ @Value("${actuator.email:}") private String actuatorEmail; + + /** + * Actuator 인증 Token (Promehteus, Grafana 등의 모니터링 툴에서 사용) + */ private UsernamePasswordAuthenticationToken actuatorAuthenticationToken; + /** + * Actuator 인증 Token을 생성 및 초기화 한다. + *

+ * - Actuator Email이 설정되어 있고 유효한 경우, Actuator 인증 Token이 정상 발급
+ * - Actuator Email이 설정되어 있지 않거나 유효하지 않은 경우, Actuator 인증 Token이 null 이거나, 권한이 제대로 부여되지 않음 + */ @PostConstruct protected void init() { if (isBlankActuatorEmail()) { @@ -54,10 +76,21 @@ protected void init() { ); } + /** + * Actuator Email이 빈 문자열인지 확인한다. + * + * @return Actuator Email이 빈 문자열인 경우 true, 아닌 경우 false + */ private boolean isBlankActuatorEmail() { return actuatorEmail.isBlank(); } + /** + * UserDetails가 활성화 상태인지 확인한다. + * + * @param userDetails + * @return 활성화 상태인 경우 가져온 authorities, 아닌 경우 empty authorities 반환한다. + */ private Collection validateEnabled(final UserDetails userDetails) { Collection authorities = userDetails.getAuthorities(); @@ -69,6 +102,18 @@ private Collection validateEnabled(final UserDetails return authorities; } + /** + * JWT 인증 필터
+ *
+ * - JWT 토큰이 유효한 경우, SecurityContextHolder에 인증 정보를 설정한다.
+ * - JWT 토큰이 유효하지 않은 경우, SecurityContextHolder에 인증 정보를 설정하지 않는다.
+ * + * @param request Http 요청 + * @param response Http 응답 + * @param filterChain 필터 체인 + * @throws ServletException 서블릿 예외 + * @throws IOException IO 예외 + */ @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @@ -86,10 +131,22 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, filterChain.doFilter(request, response); } + /** + * 요청이 인증 API 경로인지 확인한다. + * + * @param request Http 요청 + * @return 인증 API 경로인 경우 true, 아닌 경우 false + */ private boolean isAuthApiPath(final HttpServletRequest request) { return request.getServletPath().contains(AuthenticationController.AUTH_BASE_API_PATH); } + /** + * 토큰이 유효한 경우, SecurityContextHolder에 인증 정보를 설정한다. + * + * @param request Http 요청 + * @param accessTokenText Access Token 값 + */ private void setAuthenticationIfTokenIsValid(final HttpServletRequest request, final String accessTokenText) { final Claims claims = jwtService.extractAllClaims(accessTokenText); final String userEmail = claims.getSubject(); @@ -103,10 +160,21 @@ private void setAuthenticationIfTokenIsValid(final HttpServletRequest request, f } } + /** + * 사용자가 인증되지 않은 경우 true를 반환한다. + * + * @return 사용자가 인증되지 않은 경우 true, 아닌 경우 false + */ private boolean isUserNotAuthenticated() { return SecurityContextHolder.getContext().getAuthentication() == null; } + /** + * 사용자의 인증 정보가 담긴 UsernamePasswordAuthenticationToken을 생성한다. + * + * @param userEmail 사용자 Email + * @return 사용자의 인증 정보가 담긴 UsernamePasswordAuthenticationToken + */ private UsernamePasswordAuthenticationToken buildAuthToken(final String userEmail) { if (userEmail.equals(actuatorEmail)) { return actuatorAuthenticationToken; diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtExceptionHandlerFilter.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtExceptionHandlerFilter.java index 04b28205..7294f5dc 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtExceptionHandlerFilter.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/filter/JwtExceptionHandlerFilter.java @@ -14,8 +14,22 @@ import java.io.IOException; +/** + * JWT 예외 처리 필터 + */ @Slf4j public class JwtExceptionHandlerFilter extends OncePerRequestFilter { + /** + * JWT 예외 처리 필터
+ *
+ * - InvalidTokenRequestException 예외가 발생하면, 예외 메시지를 응답으로 반환한다. + * + * @param request Http 요청 객체 + * @param response Http 응답 객체 + * @param filterChain 필터 체인 + * @throws ServletException 서블릿 예외 + * @throws IOException IO 예외 + */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { @@ -25,6 +39,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } + /** + * JWT 예외 메세지를 응답으로 반환한다. + * + * @param response Http 응답 객체 + * @param message 응답 메시지 + */ private void setErrorResponse(HttpServletResponse response, final String message) { ObjectMapper objectMapper = new ObjectMapper(); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/repository/TokenRepository.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/repository/TokenRepository.java index 7e4b855b..d5f181c8 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/repository/TokenRepository.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/repository/TokenRepository.java @@ -6,12 +6,32 @@ import java.util.Optional; +/** + * 토큰 Entity Repository + */ public interface TokenRepository extends JpaRepository { - + /** + * 회원 ID로 토큰 Entity 조회 + * + * @param memberId 회원 ID + * @return 토큰 Entity + */ Optional findByMemberId(final Long memberId); + /** + * 토큰 값으로 토큰 Entity 조회 + * + * @param tokenText 토큰 값 + * @return 토큰 Entity + */ Optional findByTokenText(final String tokenText); + /** + * 사용자 이메일로 토큰 Entity 조회 + * + * @param userEmail 사용자 이메일 + * @return 토큰 Entity + */ @Query("select t from TokenEntity t join MemberEntity m on t.memberId = m.id where m.email = :userEmail") Optional findTokenByUserEmail(String userEmail); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/JwtService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/JwtService.java index 53d0f33d..9b003de1 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/JwtService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/JwtService.java @@ -16,36 +16,73 @@ import java.util.*; import java.util.stream.Collectors; +/** + * JWT Service + */ @Slf4j @Service public class JwtService { + /** + * JWT용 Secret Key + */ @Value("${application.security.jwt.secret-key}") private String secretKey; + /** + * JWT 만료 시간 + */ @Value("${application.security.jwt.expiration}") private long jwtExpiration; + /** + * JWT Refresh Token 만료 시간 + */ @Value("${application.security.jwt.refresh-token.expiration}") private long refreshExpiration; + /** + * JWT 토큰에서 Payload의 Claim 목록 추출 + * + * @param token 토큰 + * @return Payload의 Claim 목록 + */ public Claims extractAllClaims(final String token) { final JwtParser jwtParser = getJwtParser(); return getClaimsBody(jwtParser, token); } + /** + * JWT Parse를 위한 JwtParser 생성 + * + * @return JwtParser + */ private JwtParser getJwtParser() { return Jwts.parserBuilder() .setSigningKey(getSignInKey()) .build(); } + /** + * JWT Secret Key 생성 + * + * @return Secret Key + */ private Key getSignInKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } + /** + * JWT 토큰에서 jwtParser를 이용하여 Payload의 Claim 목록 추출
+ *
+ * - 토큰이 만료되었거나, 유효하지 않은 토큰일 경우, log를 남기고 InvalidTokenRequestException 예외 발생 + * + * @param jwtParser JwtParser + * @param token 토큰 + * @return Payload의 Claim 목록 + */ private Claims getClaimsBody(final JwtParser jwtParser, final String token) { final Claims body; try { @@ -63,14 +100,34 @@ private Claims getClaimsBody(final JwtParser jwtParser, final String token) { return body; } + /** + * userDetail로부터 AccessToken 생성 + * + * @param userDetails Spring Security의 UserDetails Interface (유저 정보) + * @return AccessToken + */ public String generateAccessToken(final UserDetails userDetails) { return buildToken(new HashMap<>(), userDetails, jwtExpiration); } + /** + * userDetail로부터 RefreshToken 생성 + * + * @param userDetails Spring Security의 UserDetails Interface (유저 정보) + * @return RefreshToken + */ public String generateRefreshToken(final UserDetails userDetails) { return buildToken(new HashMap<>(), userDetails, refreshExpiration); } + /** + * userDetail와 추가 Claim을 이용하여 만료 시간을 설정한 JWT 토큰 생성 + * + * @param extraClaims 추가 Claim + * @param userDetails Spring Security의 UserDetails Interface (유저 정보) + * @param expiration 만료 시간 + * @return JWT 토큰 + */ private String buildToken(final Map extraClaims, final UserDetails userDetails, final long expiration) { addRolesToClaims(extraClaims, userDetails); @@ -83,17 +140,35 @@ private String buildToken(final Map extraClaims, final UserDetai .compact(); } + /** + * userDetail의 권한을 JWT에도 담기 위해서, 추가 Claim에 roles를 추가 + * + * @param extraClaims 추가 Claim + * @param userDetails Spring Security의 UserDetails Interface (유저 정보) + */ private void addRolesToClaims(final Map extraClaims, final UserDetails userDetails) { String authorities = convertString(userDetails.getAuthorities()); extraClaims.put("roles", authorities); } + /** + * 권한을 String으로 변환 + * + * @param authorities 권한 목록 + * @return 권한 String + */ private String convertString(final Collection authorities) { return authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); } + /** + * JWT 토큰의 roles Claim을 이용하여 권한 목록 생성 + * + * @param rolesText roles Claim 값 + * @return 권한 목록 (GrantedAuthority의 Collection) + */ public Collection convertStringToAuthorities(final String rolesText) { final String[] roles = rolesText.split(","); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/MyLogoutHandler.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/MyLogoutHandler.java index 9291b30a..93fe8fa6 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/MyLogoutHandler.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/service/MyLogoutHandler.java @@ -12,12 +12,26 @@ import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.stereotype.Service; +/** + * Logout 핸들러 (Logout 요청이 들어왔을 때 사용) + */ @RequiredArgsConstructor @Service public class MyLogoutHandler implements LogoutHandler { - + /** + * 토큰 Entity Repository + */ private final TokenRepository tokenRepository; + /** + * Logout 요청시 실행되는 메서드 + *

+ * - Logout 요청이 오면, 같이 전달된 accessToken을 통해 저장된 RefreshToken을 폐기한다. + * + * @param request Http 요청 + * @param response Http 응답 + * @param authentication Spring Security Authentication Interface + */ @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String accessTokenText = JwtAuthUtil.extractTokenTextFromRequestHeader(request, TokenType.BEARER); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/util/JwtAuthUtil.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/util/JwtAuthUtil.java index 773e2e8c..2d9cc87d 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/util/JwtAuthUtil.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/jwt/util/JwtAuthUtil.java @@ -3,13 +3,26 @@ import jakarta.servlet.http.HttpServletRequest; import org.hyunggi.mygardenbe.auth.jwt.domain.TokenType; +/** + * JWT 인증 관련 유틸 클래스 + */ public class JwtAuthUtil { + /** + * Token이 전달되는 Http Header + */ public static final String AUTH_HEADER = "Authorization"; private JwtAuthUtil() { //Utility class } + /** + * Request Header에서 Token 값 추출 + * + * @param request Http 요청 + * @param tokenType Token Type + * @return Token 값 + */ public static String extractTokenTextFromRequestHeader(final HttpServletRequest request, final TokenType tokenType) { if (hasNotAuthHeader(request)) { return null; @@ -20,6 +33,12 @@ public static String extractTokenTextFromRequestHeader(final HttpServletRequest return authHeader.substring(tokenType.getParseTextLength()); } + /** + * Request Header에 Authorization Header가 없는지 확인 + * + * @param request Http 요청 + * @return Authorization Header가 없으면 true, 있으면 false + */ private static boolean hasNotAuthHeader(final HttpServletRequest request) { String authHeader = request.getHeader(AUTH_HEADER); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/AuthenticationService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/AuthenticationService.java index 247018f0..7b4037f8 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/AuthenticationService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/AuthenticationService.java @@ -17,15 +17,40 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +/** + * 인증 Service + */ @RequiredArgsConstructor @Service public class AuthenticationService { + /** + * 회원 Entity Repository + */ private final MemberRepository memberRepository; + /** + * 비밀번호 암호화시에 사용할 Encoder + */ private final PasswordEncoder passwordEncoder; + /** + * JWT Service + */ private final JwtService jwtService; + /** + * 토큰 Entity Repository + */ private final TokenRepository tokenRepository; + /** + * Spring Security Authentication Manager + */ private final AuthenticationManager authenticationManager; + /** + * 회원 가입 + * + * @param email 이메일 + * @param password 비밀번호 + * @return 회원 ID + */ public Long signUp(final String email, final String password) { final String trimEmail = email.trim(); final String trimPassword = password.trim(); @@ -40,6 +65,11 @@ public Long signUp(final String email, final String password) { return savedMember.getId(); } + /** + * 이메일 중복 체크 + * + * @param email 이메일 + */ private void checkDuplicateEmail(final String email) { memberRepository.findByEmail(email) .ifPresent(m -> { @@ -47,6 +77,13 @@ private void checkDuplicateEmail(final String email) { }); } + /** + * 회원 저장 + * + * @param email 이메일 + * @param password 비밀번호 + * @return 저장된 회원 Entity + */ private MemberEntity saveMember(final String email, final String password) { final MemberEntity member = MemberEntity.of( new Member(email, password), @@ -56,12 +93,25 @@ private MemberEntity saveMember(final String email, final String password) { return memberRepository.save(member); } + /** + * 토큰 저장 + * + * @param member 회원 Entity + * @param jwtToken JWT 토큰 + */ private void saveToken(final MemberEntity member, final String jwtToken) { final Token token = Token.createBearerToken(jwtToken); tokenRepository.save(TokenEntity.of(token, member.getId())); } + /** + * 로그인 + * + * @param email 이메일 + * @param password 비밀번호 + * @return 인증 응답 + */ @Transactional public AuthenticationResponse login(final String email, final String password) { Authentication authenticate = authenticationManager.authenticate( @@ -85,11 +135,25 @@ public AuthenticationResponse login(final String email, final String password) { .build(); } + /** + * 이메일로 토큰 Entity 조회 + * + * @param email 이메일 + * @return 토큰 Entity + */ private TokenEntity getTokenByEmail(final String email) { return tokenRepository.findTokenByUserEmail(email) .orElseThrow(() -> new EntityNotFoundException("해당하는 유저의 토큰을 찾을 수 없습니다.")); } + /** + * 토큰 갱신 + *

+ * - Refresh Token을 통해 AccessToken과 RefreshToken을 갱신한다. + * + * @param refreshToken 리프레시 토큰 + * @return 갱신된 AccessToken과 RefreshToken을 담은 인증 응답 + */ @Transactional public AuthenticationResponse refresh(final String refreshToken) { final TokenEntity tokenEntity = getRefreshTokenByTokenText(refreshToken); @@ -109,11 +173,23 @@ public AuthenticationResponse refresh(final String refreshToken) { .build(); } + /** + * Refresh 토큰으로 토큰 Entity 조회 + * + * @param refreshToken 리프레시 토큰 + * @return 토큰 Entity + */ private TokenEntity getRefreshTokenByTokenText(final String refreshToken) { return tokenRepository.findByTokenText(refreshToken) .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 리프레시 토큰입니다.")); } + /** + * 회원 ID로 회원 Entity 조회 + * + * @param memberId 회원 ID + * @return 회원 Entity + */ private MemberEntity getMemberById(final Long memberId) { return memberRepository.findById(memberId) .orElseThrow(() -> new EntityNotFoundException("존재하지 않는 회원입니다.")); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java index 7b18356f..65d017b6 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java @@ -2,6 +2,12 @@ import lombok.Builder; +/** + * 로그인 성공시에 반환되는 응답 + *

+ * - accessToken: JWT Access 토큰
+ * - refreshToken: JWT Refresh 토큰 + */ @Builder public record AuthenticationResponse(String accessToken, String refreshToken) { } From 9c20cdd0bd72945a1ed8b074d19529449f9355ad Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 13:26:36 +0900 Subject: [PATCH 02/12] =?UTF-8?q?chore:=20boards.common=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/BoardCategoryController.java | 9 ++ .../category/entity/BoardCategoryEntity.java | 25 ++++ .../repository/BoardCategoryRepository.java | 16 ++ .../common/category/request/GetRequest.java | 140 ++++++++++++++++++ .../response/BoardCategoryResponse.java | 6 + .../service/BoardCategoryService.java | 30 +++- .../boards/common/response/CustomPage.java | 45 ++++++ 7 files changed, 270 insertions(+), 1 deletion(-) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryController.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryController.java index b6f32078..43f9b9d5 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryController.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryController.java @@ -11,12 +11,21 @@ import java.util.List; +/** + * 게시판 분류 Controller + */ @RestController @RequiredArgsConstructor @RequestMapping("/api/boards/categories") public class BoardCategoryController { private final BoardCategoryService boardCategoryService; + /** + * 게시판 분류 목록 조회 + * + * @param boardType 분류를 조회할 게시판 타입 + * @return 게시판 분류 목록 + */ @GetMapping public ApiResponse> getCategories(@RequestParam final String boardType) { return ApiResponse.ok(boardCategoryService.getCategories(boardType)); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/entity/BoardCategoryEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/entity/BoardCategoryEntity.java index 67d5af65..40fc2d1e 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/entity/BoardCategoryEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/entity/BoardCategoryEntity.java @@ -5,17 +5,35 @@ import lombok.NoArgsConstructor; import org.hyunggi.mygardenbe.common.entity.BaseEntity; +/** + * 게시판 분류 Entity + */ @Getter @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Entity public class BoardCategoryEntity extends BaseEntity { + /** + * 분류 ID + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + /** + * 분류 코드 + */ @Column(nullable = false, length = 30) private String code; + + /** + * 분류 명 + */ @Column(nullable = false, length = 30) private String text; + + /** + * 게시판 타입 + */ @Column(nullable = false, length = 30) private String boardType; @@ -27,6 +45,13 @@ public BoardCategoryEntity(String code, String text, String boardType) { this.boardType = boardType; } + /** + * 생성자 유효성 검사 + * + * @param code 분류 코드 + * @param text 분류 명 + * @param boardType 게시판 타입 + */ private void validateConstructor(final String code, final String text, final String boardType) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code는 null이거나 비어있을 수 없습니다."); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/repository/BoardCategoryRepository.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/repository/BoardCategoryRepository.java index 0b245009..4f1cd735 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/repository/BoardCategoryRepository.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/repository/BoardCategoryRepository.java @@ -6,8 +6,24 @@ import java.util.List; import java.util.Optional; +/** + * 게시판 분류 Repository + */ public interface BoardCategoryRepository extends JpaRepository { + /** + * 분류 코드와 게시판 타입으로 분류 조회 + * + * @param categoryCode 분류 코드 + * @param boardType 게시판 타입 + * @return 분류 Entity의 Optional + */ Optional findByCodeAndBoardType(final String categoryCode, final String boardType); + /** + * 게시판 타입으로 분류 목록 조회 + * + * @param boardType 게시판 타입 + * @return 분류 Entity 목록 + */ List findAllByBoardType(final String boardType); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequest.java index 5062e860..97c018d0 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequest.java @@ -7,10 +7,24 @@ import java.time.LocalDate; import java.util.stream.Stream; +/** + * 게시글 조회시에 사용되는 Request (검색 및 필터링 할 때 사용) + */ @Getter public final class GetRequest { + /** + * 조회 기간 + */ private final SearchDate searchDate; + + /** + * 조회 조건 + */ private final SearchCondition searchCondition; + + /** + * 페이징 + */ private final SearchPaging searchPaging; @Builder @@ -26,10 +40,24 @@ private GetRequest(final SearchDate searchDate, final SearchCondition searchCond this.searchPaging = searchPaging; } + /** + * GetRequest 생성 메서드 + * + * @param searchDate 조회 기간 + * @param searchCondition 조회 조건 + * @param searchPaging 페이징 + * @return GetRequest + */ public static GetRequest of(final SearchDate searchDate, final SearchCondition searchCondition, final SearchPaging searchPaging) { return new GetRequest(searchDate, searchCondition, searchPaging); } + /** + * 조회 기간 + * + * @param startDate 조회 시작일 + * @param endDate 조회 종료일 + */ @Builder public record SearchDate(LocalDate startDate, LocalDate endDate) { public SearchDate(LocalDate startDate, LocalDate endDate) { @@ -39,12 +67,27 @@ public SearchDate(LocalDate startDate, LocalDate endDate) { this.endDate = initEndDate(endDate); } + /** + * 조회 시작일과 종료일의 유효성 검사 + * + * @param startDate 조회 시작일 + * @param endDate 조회 종료일 + */ private void validateSearchDate(final LocalDate startDate, final LocalDate endDate) { if (startDate != null && endDate != null && startDate.isAfter(endDate)) { throw new IllegalArgumentException("시작일은 종료일보다 빠를 수 없습니다."); } } + /** + * 조회 시작일 초기화 + *

+ * - 조회 시작일이 없을 경우, 1달 전으로 초기화
+ * - 조회 시작일이 있을 경우, 그대로 반환 + * + * @param startDate 조회 시작일 + * @return 초기화된 조회 시작일 + */ private LocalDate initStartDate(LocalDate startDate) { if (startDate == null) { return LocalDate.now().minusMonths(1); @@ -53,6 +96,15 @@ private LocalDate initStartDate(LocalDate startDate) { return startDate; } + /** + * 조회 종료일 초기화 + *

+ * - 조회 종료일이 없을 경우, 현재 날짜로 초기화
+ * - 조회 종료일이 있을 경우, 그대로 반환 + * + * @param endDate 조회 종료일 + * @return 초기화된 조회 종료일 + */ private LocalDate initEndDate(LocalDate endDate) { if (endDate == null) { return LocalDate.now(); @@ -62,6 +114,12 @@ private LocalDate initEndDate(LocalDate endDate) { } } + /** + * 조회 조건 + * + * @param category 분류 + * @param searchText 검색어 + */ @Builder public record SearchCondition(String category, String searchText) { public SearchCondition(String category, String searchText) { @@ -69,6 +127,14 @@ public SearchCondition(String category, String searchText) { this.searchText = initSearchText(searchText); } + /** + * 분류 초기화 + *

+ * - 분류가 없을 경우, 빈 문자열로 초기화 + * + * @param category 분류 + * @return 초기화된 분류 + */ private String initCategory(final String category) { if (category == null || category.trim().isBlank()) { return ""; @@ -77,6 +143,14 @@ private String initCategory(final String category) { return category; } + /** + * 검색어 초기화 + *

+ * - 검색어가 없을 경우, 빈 문자열로 초기화 + * + * @param searchText 검색어 + * @return 초기화된 검색어 + */ private String initSearchText(String searchText) { if (searchText == null || searchText.trim().isBlank()) { return ""; @@ -86,6 +160,14 @@ private String initSearchText(String searchText) { } } + /** + * 페이징 + * + * @param currentPage 현재 페이지 + * @param pageSize 페이지 크기 + * @param sort 정렬 기준 + * @param order 정렬 순서 + */ @Builder public record SearchPaging(Integer currentPage, Integer pageSize, String sort, String order) { public SearchPaging(final Integer currentPage, final Integer pageSize, final String sort, final String order) { @@ -95,6 +177,15 @@ public SearchPaging(final Integer currentPage, final Integer pageSize, final Str this.order = initOrder(order); } + /** + * 현재 페이지 초기화 + *

+ * - 현재 페이지가 없거나 1보다 작은 경우, 0으로 초기화
+ * - 현재 페이지가 1 이상일 경우, 1을 뺀 값으로 초기화 + * + * @param currentPage 현재 페이지 + * @return 초기화된 현재 페이지 + */ private Integer initCurrentPage(final Integer currentPage) { if (currentPage == null || currentPage < 1) { return 0; @@ -103,6 +194,14 @@ private Integer initCurrentPage(final Integer currentPage) { return currentPage - 1; } + /** + * 페이지 크기 초기화 + *

+ * - 페이지 크기가 없거나 1보다 작은 경우, 10으로 초기화 + * + * @param pageSize 페이지 크기 + * @return 초기화된 페이지 크기 + */ private Integer initPageSize(final Integer pageSize) { if (pageSize == null || pageSize < 1) { return 10; @@ -111,6 +210,15 @@ private Integer initPageSize(final Integer pageSize) { return pageSize; } + /** + * 정렬 기준 초기화 + *

+ * - 정렬 기준이 없을 경우, writtenAt으로 초기화
+ * - 정렬 기준 유효성 검사 진행 + * + * @param sort 정렬 기준 + * @return 초기화된 정렬 기준 + */ private String initSort(final String sort) { if (sort == null || sort.isBlank()) { return "writtenAt"; @@ -118,6 +226,15 @@ private String initSort(final String sort) { return validateSort(sort); } + /** + * 정렬 기준 유효성 검사 + *

+ * - 정렬 기준이 writtenAt, title, category, views 중 하나일 경우, 해당 정렬 조건 반환
+ * - 정렬 기준이 writtenAt, title, category, views 중 하나가 아닐 경우, IllegalArgumentException 발생 + * + * @param sort 정렬 기준 + * @return 유효한 정렬 기준 + */ private String validateSort(final String sort) { return Stream.of("writtenAt", "title", "category", "views") .filter(s -> s.equals(sort)) @@ -125,6 +242,15 @@ private String validateSort(final String sort) { .orElseThrow(() -> new IllegalArgumentException("정렬 기준은 writtenAt, title, category, views 중 하나여야 합니다.")); } + /** + * 정렬 순서 초기화 + *

+ * - 정렬 순서가 없을 경우, desc로 초기화
+ * - 정렬 순서 유효성 검사 진행 + * + * @param order 정렬 순서 + * @return 초기화된 정렬 순서 + */ private String initOrder(final String order) { if (order == null || order.isBlank()) { return "desc"; @@ -133,6 +259,15 @@ private String initOrder(final String order) { return validateOrder(order); } + /** + * 정렬 순서 유효성 검사 + *

+ * - 정렬 순서가 asc, desc 중 하나일 경우, 해당 정렬 순서 반환
+ * - 정렬 순서가 asc, desc 중 하나가 아닐 경우, IllegalArgumentException 발생 + * + * @param order 정렬 순서 + * @return 유효한 정렬 순서 + */ private String validateOrder(final String order) { return Stream.of("asc", "desc") .filter(o -> o.equals(order)) @@ -140,6 +275,11 @@ private String validateOrder(final String order) { .orElseThrow(() -> new IllegalArgumentException("정렬 순서는 asc, desc 중 하나여야 합니다.")); } + /** + * 정렬 순서를 Sort.Direction으로 변환 + * + * @return Sort.Direction + */ public Sort.Direction convertOrderToSortDirection() { if (order.equals("desc")) return Sort.Direction.DESC; diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java index fb5bf798..a1d945ef 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java @@ -3,6 +3,12 @@ import lombok.Builder; import org.hyunggi.mygardenbe.boards.common.category.entity.BoardCategoryEntity; +/** + * 게시판 분류 응답 + *

+ * - code: 분류 코드
+ * - text: 분류 명 + */ @Builder public record BoardCategoryResponse(String code, String text) { public static BoardCategoryResponse of(BoardCategoryEntity boardCategoryEntity) { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryService.java index ea958935..5bc998b0 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryService.java @@ -9,11 +9,23 @@ import java.util.List; +/** + * 게시판 분류 Service + */ @RequiredArgsConstructor @Service public class BoardCategoryService { + /** + * 게시판 분류 Entity Repository + */ private final BoardCategoryRepository boardCategoryRepository; + /** + * 게시판 분류 목록 조회 + * + * @param boardType 게시판 타입 + * @return 게시판 분류 목록 + */ public List getCategories(final String boardType) { validateBoardType(boardType); return boardCategoryRepository.findAllByBoardType(boardType).stream() @@ -21,18 +33,34 @@ public List getCategories(final String boardType) { .toList(); } + /** + * 게시판 타입 검증 + * + * @param boardType 게시판 타입 + */ private void validateBoardType(final String boardType) { Assert.hasText(boardType, "게시판 타입은 비어있을 수 없습니다."); } + /** + * 분류와 게시판 타입 검증 + * + * @param category 분류 + * @param boardType 게시판 타입 + */ public void validateCategoryWithBoardType(final String category, final String boardType) { validateCategory(category); validateBoardType(boardType); - + boardCategoryRepository.findByCodeAndBoardType(category, boardType) .orElseThrow(() -> new EntityNotFoundException("해당 분류가 존재하지 않습니다.")); } + /** + * 분류 검증 + * + * @param category 분류 + */ private void validateCategory(final String category) { Assert.hasText(category, "분류는 비어있을 수 없습니다."); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/response/CustomPage.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/response/CustomPage.java index 3f55a643..78d29eab 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/response/CustomPage.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/response/CustomPage.java @@ -7,14 +7,46 @@ import java.util.List; +/** + * 커스텀 Page + * + * @param Page의 Content Type + */ @Getter public class CustomPage { + /** + * 총 페이지 수 + */ private final Integer totalPages; + + /** + * 총 개수 + */ private final Long totalElements; + + /** + * 현재 페이지 + */ private final Integer currentPage; + + /** + * 페이지 크기 + */ private final Integer pageSize; + + /** + * 현재 페이지가 첫 페이지인지 여부 + */ private final Boolean isFirst; + + /** + * 현재 페이지가 마지막 페이지 여부 + */ private final Boolean isLast; + + /** + * 페이지 내용 + */ private final List content; @Builder(access = AccessLevel.PRIVATE) @@ -28,6 +60,13 @@ private CustomPage(final Page page) { this.isLast = page.isLast(); } + /** + * Page 객체로부터 CustomPage 객체 생성 + * + * @param page Page 객체 + * @param Page의 Content Type + * @return CustomPage 객체 + */ public static CustomPage of(final Page page) { validate(page); @@ -36,6 +75,12 @@ public static CustomPage of(final Page page) { .build(); } + /** + * Page 객체 유효성 검사 + * + * @param page Page 객체 + * @param Page의 Content Type + */ private static void validate(final Page page) { if (page == null) { throw new IllegalArgumentException("Page 객체는 null이 될 수 없습니다."); From e70d88952f6d481d8e2ab6727823c1c862f9262c Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 13:27:30 +0900 Subject: [PATCH 03/12] =?UTF-8?q?refact:=20'=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC'=20=EB=8B=A8=EC=96=B4=EB=A5=BC=20'=EB=B6=84=EB=A5=98'?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- my-garden-be/src/docs/asciidoc/index.adoc | 2 +- .../boards/learn/entity/LearnBoardEntity.java | 2 +- .../learn/service/LearnBoardService.java | 2 +- .../notice/entity/NoticeBoardEntity.java | 2 +- .../notice/service/NoticeBoardService.java | 2 +- .../BoardCategoryControllerTest.java | 2 +- .../category/request/GetRequestTest.java | 2 +- .../service/BoardCategoryServiceTest.java | 6 ++-- .../learn/entity/LearnBoardEntityTest.java | 2 +- .../repository/LearnBoardRepositoryTest.java | 4 +-- .../learn/service/LearnBoardServiceTest.java | 6 ++-- .../notice/entity/NoticeBoardEntityTest.java | 4 +-- .../repository/NoticeBoardRepositoryTest.java | 4 +-- .../service/NoticeBoardServiceTest.java | 6 ++-- .../BoardCategoryControllerDocsTest.java | 8 +++--- .../learn/LearnBoardControllerDocsTest.java | 24 ++++++++-------- .../notice/NoticeBoardControllerDocsTest.java | 28 +++++++++---------- .../src/components/boards/learn/api/api.js | 2 +- .../src/components/boards/notice/api/api.js | 2 +- 19 files changed, 55 insertions(+), 55 deletions(-) diff --git a/my-garden-be/src/docs/asciidoc/index.adoc b/my-garden-be/src/docs/asciidoc/index.adoc index 611b151a..2fe82fd8 100644 --- a/my-garden-be/src/docs/asciidoc/index.adoc +++ b/my-garden-be/src/docs/asciidoc/index.adoc @@ -26,7 +26,7 @@ include::dailyroutine/put.adoc[] include::dailyroutine/delete.adoc[] [[board-category]] -== Board Category [게시판 카테고리] +== Board Category [게시판 분류] include::board/category/getList.adoc[] diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java index b2edf715..c143f6cf 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java @@ -82,7 +82,7 @@ private static void validateContent(final String content) { private static void validateCategory(final String category) { if (category == null || category.isBlank()) { - throw new IllegalArgumentException("카테고리는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); + throw new IllegalArgumentException("분류는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java index 72647e1c..05596e27 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java @@ -38,7 +38,7 @@ private void validateArguments(final LocalDate startDate, final LocalDate endDat Assert.isTrue(endDate != null, "종료일은 null이 될 수 없습니다."); Assert.isTrue(startDate.isEqual(endDate) || startDate.isBefore(endDate), "시작일은 종료일보다 느릴 수 없습니다."); - Assert.isTrue(category != null, "카테고리는 null이 될 수 없습니다."); + Assert.isTrue(category != null, "분류는 null이 될 수 없습니다."); Assert.isTrue(searchText != null, "검색어는 null이 될 수 없습니다."); Assert.isTrue(pageable != null, "페이징 정보는 null이 될 수 없습니다."); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java index 5c8db76a..d2d77580 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java @@ -87,7 +87,7 @@ private static void validateContent(final String content) { private static void validateCategory(final String category) { if (category == null || category.isBlank()) { - throw new IllegalArgumentException("카테고리는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); + throw new IllegalArgumentException("분류는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java index a4390bf0..2bc42224 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java @@ -38,7 +38,7 @@ private void validateArguments(final LocalDate startDate, final LocalDate endDat Assert.isTrue(endDate != null, "종료일은 null이 될 수 없습니다."); Assert.isTrue(startDate.isEqual(endDate) || startDate.isBefore(endDate), "시작일은 종료일보다 느릴 수 없습니다."); - Assert.isTrue(category != null, "카테고리는 null이 될 수 없습니다."); + Assert.isTrue(category != null, "분류는 null이 될 수 없습니다."); Assert.isTrue(searchText != null, "검색어는 null이 될 수 없습니다."); Assert.isTrue(pageable != null, "페이징 정보는 null이 될 수 없습니다."); } diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryControllerTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryControllerTest.java index d6b78d8a..218d0b8c 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryControllerTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/controller/BoardCategoryControllerTest.java @@ -14,7 +14,7 @@ class BoardCategoryControllerTest extends ControllerTestSupport { @Test - @DisplayName("공지사항 카테고리 목록을 조회한다.") + @DisplayName("공지사항 분류 목록을 조회한다.") void getCategories() throws Exception { //given final List categories = List.of( diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequestTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequestTest.java index 290d1b09..42f12601 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequestTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/request/GetRequestTest.java @@ -67,7 +67,7 @@ void searchDateConstructor3() { } @Test - @DisplayName("카테고리가 null일 때, 카테고리는 빈 문자열이다.") + @DisplayName("분류가 null일 때, 분류는 빈 문자열이다.") void searchConditionConstructor() { //given final String category = null; diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryServiceTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryServiceTest.java index 4740e37b..b7a75301 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryServiceTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/common/category/service/BoardCategoryServiceTest.java @@ -26,7 +26,7 @@ class BoardCategoryServiceTest extends IntegrationTestSupport { private BoardCategoryRepository boardCategoryRepository; @Test - @DisplayName("공지사항 카테고리 목록을 조회한다.") + @DisplayName("공지사항 분류 목록을 조회한다.") void getCategories() { //given BoardCategoryEntity boardCategoryEntity = new BoardCategoryEntity("project", "프로젝트", "notice"); @@ -69,7 +69,7 @@ void validateBoardType(final String boardType) { @ParameterizedTest @NullAndEmptySource - @DisplayName("해당 하는 카테고리가 Null이거나 빈 문자열이면 예외를 발생시킨다.") + @DisplayName("해당 하는 분류가 Null이거나 빈 문자열이면 예외를 발생시킨다.") void validateCategory(final String category) { //given BoardCategoryEntity boardCategoryEntity = new BoardCategoryEntity("project", "프로젝트", "notice"); @@ -83,7 +83,7 @@ void validateCategory(final String category) { } @Test - @DisplayName("해당 하는 카테고리가 존재하지 않으면 예외를 발생시킨다.") + @DisplayName("해당 하는 분류가 존재하지 않으면 예외를 발생시킨다.") void validateCategoryWithBoardType() { //given BoardCategoryEntity boardCategoryEntity = new BoardCategoryEntity("project", "프로젝트", "notice"); diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntityTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntityTest.java index 0732c925..a1383e9e 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntityTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntityTest.java @@ -83,7 +83,7 @@ void categoryIsNull(final String category) { // when, then assertThatThrownBy(() -> LearnBoardEntity.of(title, content, category, writer, writtenAt, memberId)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("카테고리는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); + .hasMessageContaining("분류는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } @ParameterizedTest diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryTest.java index a934e755..e4bb53a8 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryTest.java @@ -55,7 +55,7 @@ private LearnBoardEntity buildLearnBoardWith(final LocalDateTime writtenAt) { } @Test - @DisplayName("해당 날짜에 해당하면서 카테고리가 같은 TIL을 조회할 수 있다.") + @DisplayName("해당 날짜에 해당하면서 분류가 같은 TIL을 조회할 수 있다.") void searchLearnBoards_with_writtenAt_and_category() { //given learnBoardRepository.save(buildLearnBoardWith("title", "content", "category1")); @@ -110,7 +110,7 @@ void searchLearnBoards_with_writtenAt_and_searchText() { } @Test - @DisplayName("해당 날짜에 해당하면서 카테고리가 같고 제목 또는 내용에 검색어가 포함된 TIL을 조회할 수 있다.") + @DisplayName("해당 날짜에 해당하면서 분류가 같고 제목 또는 내용에 검색어가 포함된 TIL을 조회할 수 있다.") void searchLearnBoards_with_writtenAt_and_category_and_searchText() { //given learnBoardRepository.save(buildLearnBoardWith("title11", "content", "category1")); diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardServiceTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardServiceTest.java index 27d7b7a8..6030e895 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardServiceTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardServiceTest.java @@ -101,7 +101,7 @@ private LearnBoardEntity buildLearnBoardWith(final String title, final String co } @Test - @DisplayName("category가 있고, searchText가 없으면, 기간 내의 해당 카테고리의 TIL을 조회할 수 있다.") + @DisplayName("category가 있고, searchText가 없으면, 기간 내의 해당 분류의 TIL을 조회할 수 있다.") void getLearnBoardsWithCategory() { // given final LocalDate startDate = LocalDate.of(2024, 1, 26); @@ -160,7 +160,7 @@ void getLearnBoardsWithSearchText() { } @Test - @DisplayName("category가 있고, searchText가 있으면, 기간 내의 해당 카테고리의 검색어가 포함된 TIL을 조회할 수 있다.") + @DisplayName("category가 있고, searchText가 있으면, 기간 내의 해당 분류의 검색어가 포함된 TIL을 조회할 수 있다.") void getLearnBoardsWithCategoryAndSearchText() { // given final LocalDate startDate = LocalDate.of(2024, 1, 26); @@ -249,7 +249,7 @@ void getLearnBoardsWithNullCategory() { // when,then assertThatThrownBy(() -> learnBoardService.getLearnBoards(startDate, endDate, category, searchText, pageable)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("카테고리는 null이 될 수 없습니다."); + .hasMessage("분류는 null이 될 수 없습니다."); } @Test diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntityTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntityTest.java index 5398ef59..bfbd6a8a 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntityTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntityTest.java @@ -89,7 +89,7 @@ void categoryIsNull(final String category) { // when, then assertThatThrownBy(() -> NoticeBoardEntity.of(title, content, category, isImportant, writer, writtenAt, memberId)) .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("카테고리는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); + .hasMessageContaining("분류는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } @Test @@ -235,7 +235,7 @@ void update() { void isWriter() { // given final Long memberId = 1L; - + final NoticeBoardEntity noticeBoardEntity = NoticeBoardEntity.of("title", "content", "category", false, "writer", LocalDateTime.now(), memberId); // when, then diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryTest.java index 2501510f..2a508040 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryTest.java @@ -88,7 +88,7 @@ private NoticeBoardEntity buildNoticeBoardWith(final LocalDateTime writtenAt) { } @Test - @DisplayName("해당 날짜에 해당하면서 카테고리가 같은 공지사항을 조회할 수 있다.") + @DisplayName("해당 날짜에 해당하면서 분류가 같은 공지사항을 조회할 수 있다.") void searchNoticeBoards_with_writtenAt_and_category() { //given noticeBoardRepository.save(buildNoticeBoardWith("title", "content", "category1")); @@ -144,7 +144,7 @@ void searchNoticeBoards_with_writtenAt_and_searchText() { } @Test - @DisplayName("해당 날짜에 해당하면서 카테고리가 같고 제목 또는 내용에 검색어가 포함된 공지사항을 조회할 수 있다.") + @DisplayName("해당 날짜에 해당하면서 분류가 같고 제목 또는 내용에 검색어가 포함된 공지사항을 조회할 수 있다.") void searchNoticeBoards_with_writtenAt_and_category_and_searchText() { //given noticeBoardRepository.save(buildNoticeBoardWith("title11", "content", "category1")); diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardServiceTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardServiceTest.java index 764013bc..95871ac5 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardServiceTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardServiceTest.java @@ -122,7 +122,7 @@ private NoticeBoardEntity buildNoticeBoardWith(final String title, final String } @Test - @DisplayName("category가 있고, searchText가 없으면, 기간 내의 해당 카테고리의 공지사항을 조회할 수 있다.") + @DisplayName("category가 있고, searchText가 없으면, 기간 내의 해당 분류의 공지사항을 조회할 수 있다.") void getNoticeBoardsWithCategory() { // given final LocalDate startDate = LocalDate.of(2024, 1, 26); @@ -181,7 +181,7 @@ void getNoticeBoardsWithSearchText() { } @Test - @DisplayName("category가 있고, searchText가 있으면, 기간 내의 해당 카테고리의 검색어가 포함된 공지사항을 조회할 수 있다.") + @DisplayName("category가 있고, searchText가 있으면, 기간 내의 해당 분류의 검색어가 포함된 공지사항을 조회할 수 있다.") void getNoticeBoardsWithCategoryAndSearchText() { // given final LocalDate startDate = LocalDate.of(2024, 1, 26); @@ -270,7 +270,7 @@ void getNoticeBoardsWithNullCategory() { // when,then assertThatThrownBy(() -> noticeBoardService.getNoticeBoards(startDate, endDate, category, searchText, pageable)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("카테고리는 null이 될 수 없습니다."); + .hasMessage("분류는 null이 될 수 없습니다."); } @Test diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/category/BoardCategoryControllerDocsTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/category/BoardCategoryControllerDocsTest.java index 73654ea4..e0203b68 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/category/BoardCategoryControllerDocsTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/category/BoardCategoryControllerDocsTest.java @@ -33,7 +33,7 @@ protected Object initController() { } @Test - @DisplayName("카테고리 목록을 조회한다.") + @DisplayName("분류 목록을 조회한다.") void getCategories() throws Exception { //given BDDMockito.given(boardCategoryService.getCategories(any())) @@ -68,9 +68,9 @@ void getCategories() throws Exception { fieldWithPath("code").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), fieldWithPath("status").type(JsonFieldType.STRING).description("HTTP 상태"), fieldWithPath("message").type(JsonFieldType.STRING).description("메시지"), - fieldWithPath("data").type(JsonFieldType.ARRAY).description("데이터 (카테고리 목록)"), - fieldWithPath("data[].code").type(JsonFieldType.STRING).description("카테고리 코드 (value)"), - fieldWithPath("data[].text").type(JsonFieldType.STRING).description("카테고리 텍스트 (label)") + fieldWithPath("data").type(JsonFieldType.ARRAY).description("데이터 (분류 목록)"), + fieldWithPath("data[].code").type(JsonFieldType.STRING).description("분류 코드 (value)"), + fieldWithPath("data[].text").type(JsonFieldType.STRING).description("분류 텍스트 (label)") ) )); } diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/learn/LearnBoardControllerDocsTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/learn/LearnBoardControllerDocsTest.java index 5bea51c8..8e97c4ba 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/learn/LearnBoardControllerDocsTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/learn/LearnBoardControllerDocsTest.java @@ -74,8 +74,8 @@ void getLearnBoards() throws Exception { parameterWithName("endDate").description("조회 종료일") .attributes(field("constraints", "yyyy-MM-dd")) .optional(), - parameterWithName("category").description("카테고리") - .attributes(field("constraints", "TIL 카테고리")) + parameterWithName("category").description("분류") + .attributes(field("constraints", "TIL 분류")) .optional(), parameterWithName("searchText").description("검색어") .optional(), @@ -106,7 +106,7 @@ void getLearnBoards() throws Exception { fieldWithPath("data.content[].id").type(JsonFieldType.NUMBER).description("TIL 게시글 ID"), fieldWithPath("data.content[].title").type(JsonFieldType.STRING).description("TIL 게시글 제목"), fieldWithPath("data.content[].content").type(JsonFieldType.STRING).description("TIL 게시글 내용"), - fieldWithPath("data.content[].category").type(JsonFieldType.STRING).description("TIL 게시글 카테고리"), + fieldWithPath("data.content[].category").type(JsonFieldType.STRING).description("TIL 게시글 분류"), fieldWithPath("data.content[].views").type(JsonFieldType.NUMBER).description("TIL 게시글 조회수"), fieldWithPath("data.content[].writer").type(JsonFieldType.STRING).description("TIL 게시글 작성자"), fieldWithPath("data.content[].writtenAt").type(JsonFieldType.STRING).description("TIL 게시글 작성일시") @@ -120,7 +120,7 @@ private List buildLearnBoards() { .id(1L) .title("TIL 제목") .content("TIL 내용") - .category("TIL 카테고리") + .category("TIL 분류") .views(0) .writer("작성자") .writtenAt("2024-02-01 03:00:00") @@ -138,7 +138,7 @@ void getLearnBoard() throws Exception { .id(1L) .title("TIL 제목") .content("TIL 내용") - .category("TIL 카테고리") + .category("TIL 분류") .views(0) .writer("작성자") .writtenAt("2024-02-01 03:00:00") @@ -165,7 +165,7 @@ void getLearnBoard() throws Exception { fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("TIL 게시글 ID"), fieldWithPath("data.title").type(JsonFieldType.STRING).description("TIL 게시글 제목"), fieldWithPath("data.content").type(JsonFieldType.STRING).description("TIL 게시글 내용"), - fieldWithPath("data.category").type(JsonFieldType.STRING).description("TIL 게시글 카테고리"), + fieldWithPath("data.category").type(JsonFieldType.STRING).description("TIL 게시글 분류"), fieldWithPath("data.views").type(JsonFieldType.NUMBER).description("TIL 게시글 조회수"), fieldWithPath("data.writer").type(JsonFieldType.STRING).description("TIL 게시글 작성자"), fieldWithPath("data.writtenAt").type(JsonFieldType.STRING).description("TIL 게시글 작성일시") @@ -181,7 +181,7 @@ void postLearnBoard() throws Exception { final PostRequest request = PostRequest.builder() .title("TIL 제목") - .category("TIL 카테고리") + .category("TIL 분류") .content("TIL 내용") .build(); @@ -207,8 +207,8 @@ void postLearnBoard() throws Exception { , requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("TIL 제목") .attributes(field("constraints", "100자 이내")), - fieldWithPath("category").type(JsonFieldType.STRING).description("TIL 카테고리") - .attributes(field("constraints", "20자 이내, TIL 카테고리")), + fieldWithPath("category").type(JsonFieldType.STRING).description("TIL 분류") + .attributes(field("constraints", "20자 이내, TIL 분류")), fieldWithPath("content").type(JsonFieldType.STRING).description("TIL 내용") .attributes(field("constraints", "4000자 이내")) ) @@ -229,7 +229,7 @@ void putLearnBoard() throws Exception { final PostRequest request = PostRequest.builder() .title("수정된 TIL 제목") - .category("수정된 TIL 카테고리") + .category("수정된 TIL 분류") .content("수정된 TIL 내용") .build(); @@ -258,8 +258,8 @@ void putLearnBoard() throws Exception { , requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("수정할 TIL 제목") .attributes(field("constraints", "100자 이내")), - fieldWithPath("category").type(JsonFieldType.STRING).description("수정할 TIL 카테고리") - .attributes(field("constraints", "20자 이내, TIL 카테고리")), + fieldWithPath("category").type(JsonFieldType.STRING).description("수정할 TIL 분류") + .attributes(field("constraints", "20자 이내, TIL 분류")), fieldWithPath("content").type(JsonFieldType.STRING).description("수정할 TIL 내용") .attributes(field("constraints", "4000자 이내")) ) diff --git a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/notice/NoticeBoardControllerDocsTest.java b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/notice/NoticeBoardControllerDocsTest.java index 4b09c34a..0a5b7dd0 100644 --- a/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/notice/NoticeBoardControllerDocsTest.java +++ b/my-garden-be/src/test/java/org/hyunggi/mygardenbe/docs/board/notice/NoticeBoardControllerDocsTest.java @@ -63,7 +63,7 @@ void getNoticeImportantBoards() throws Exception { fieldWithPath("data[].id").type(JsonFieldType.NUMBER).description("공지사항 게시글 ID"), fieldWithPath("data[].title").type(JsonFieldType.STRING).description("공지사항 게시글 제목"), fieldWithPath("data[].content").type(JsonFieldType.STRING).description("공지사항 게시글 내용"), - fieldWithPath("data[].category").type(JsonFieldType.STRING).description("공지사항 게시글 카테고리"), + fieldWithPath("data[].category").type(JsonFieldType.STRING).description("공지사항 게시글 분류"), fieldWithPath("data[].isImportant").type(JsonFieldType.BOOLEAN).description("공지사항 게시글 중요 여부"), fieldWithPath("data[].views").type(JsonFieldType.NUMBER).description("공지사항 게시글 조회수"), fieldWithPath("data[].writer").type(JsonFieldType.STRING).description("공지사항 게시글 작성자"), @@ -78,7 +78,7 @@ private List buildNoticeImportantBoards() { .id(1L) .title("공지사항 제목") .content("공지사항 내용") - .category("공지사항 카테고리") + .category("공지사항 분류") .isImportant(true) .views(0) .writer("작성자") @@ -122,8 +122,8 @@ void getNoticeBoards() throws Exception { parameterWithName("endDate").description("조회 종료일") .attributes(field("constraints", "yyyy-MM-dd")) .optional(), - parameterWithName("category").description("카테고리") - .attributes(field("constraints", "공지사항 카테고리")) + parameterWithName("category").description("분류") + .attributes(field("constraints", "공지사항 분류")) .optional(), parameterWithName("searchText").description("검색어") .optional(), @@ -154,7 +154,7 @@ void getNoticeBoards() throws Exception { fieldWithPath("data.content[].id").type(JsonFieldType.NUMBER).description("공지사항 게시글 ID"), fieldWithPath("data.content[].title").type(JsonFieldType.STRING).description("공지사항 게시글 제목"), fieldWithPath("data.content[].content").type(JsonFieldType.STRING).description("공지사항 게시글 내용"), - fieldWithPath("data.content[].category").type(JsonFieldType.STRING).description("공지사항 게시글 카테고리"), + fieldWithPath("data.content[].category").type(JsonFieldType.STRING).description("공지사항 게시글 분류"), fieldWithPath("data.content[].isImportant").type(JsonFieldType.BOOLEAN).description("공지사항 게시글 중요 여부"), fieldWithPath("data.content[].views").type(JsonFieldType.NUMBER).description("공지사항 게시글 조회수"), fieldWithPath("data.content[].writer").type(JsonFieldType.STRING).description("공지사항 게시글 작성자"), @@ -169,7 +169,7 @@ private List buildNoticeBoards() { .id(1L) .title("공지사항 제목") .content("공지사항 내용") - .category("공지사항 카테고리") + .category("공지사항 분류") .isImportant(false) .views(0) .writer("작성자") @@ -188,7 +188,7 @@ void getNoticeBoard() throws Exception { .id(1L) .title("공지사항 제목") .content("공지사항 내용") - .category("공지사항 카테고리") + .category("공지사항 분류") .isImportant(false) .views(0) .writer("작성자") @@ -216,7 +216,7 @@ void getNoticeBoard() throws Exception { fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("공지사항 게시글 ID"), fieldWithPath("data.title").type(JsonFieldType.STRING).description("공지사항 게시글 제목"), fieldWithPath("data.content").type(JsonFieldType.STRING).description("공지사항 게시글 내용"), - fieldWithPath("data.category").type(JsonFieldType.STRING).description("공지사항 게시글 카테고리"), + fieldWithPath("data.category").type(JsonFieldType.STRING).description("공지사항 게시글 분류"), fieldWithPath("data.isImportant").type(JsonFieldType.BOOLEAN).description("공지사항 게시글 중요 여부"), fieldWithPath("data.views").type(JsonFieldType.NUMBER).description("공지사항 게시글 조회수"), fieldWithPath("data.writer").type(JsonFieldType.STRING).description("공지사항 게시글 작성자"), @@ -233,7 +233,7 @@ void postNoticeBoard() throws Exception { final PostRequest request = PostRequest.builder() .title("공지사항 제목") - .category("공지사항 카테고리") + .category("공지사항 분류") .content("공지사항 내용") .isImportant(false) .build(); @@ -260,8 +260,8 @@ void postNoticeBoard() throws Exception { , requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("공지사항 제목") .attributes(field("constraints", "100자 이내")), - fieldWithPath("category").type(JsonFieldType.STRING).description("공지사항 카테고리") - .attributes(field("constraints", "20자 이내, 공지사항 카테고리")), + fieldWithPath("category").type(JsonFieldType.STRING).description("공지사항 분류") + .attributes(field("constraints", "20자 이내, 공지사항 분류")), fieldWithPath("content").type(JsonFieldType.STRING).description("공지사항 내용") .attributes(field("constraints", "4000자 이내")), fieldWithPath("isImportant").type(JsonFieldType.BOOLEAN).description("공지사항 중요 여부 (상단에 배치)") @@ -283,7 +283,7 @@ void putNoticeBoard() throws Exception { final PostRequest request = PostRequest.builder() .title("수정된 공지사항 제목") - .category("수정된 공지사항 카테고리") + .category("수정된 공지사항 분류") .content("수정된 공지사항 내용") .isImportant(false) .build(); @@ -313,8 +313,8 @@ void putNoticeBoard() throws Exception { , requestFields( fieldWithPath("title").type(JsonFieldType.STRING).description("수정할 공지사항 제목") .attributes(field("constraints", "100자 이내")), - fieldWithPath("category").type(JsonFieldType.STRING).description("수정할 공지사항 카테고리") - .attributes(field("constraints", "20자 이내, 공지사항 카테고리")), + fieldWithPath("category").type(JsonFieldType.STRING).description("수정할 공지사항 분류") + .attributes(field("constraints", "20자 이내, 공지사항 분류")), fieldWithPath("content").type(JsonFieldType.STRING).description("수정할 공지사항 내용") .attributes(field("constraints", "4000자 이내")), fieldWithPath("isImportant").type(JsonFieldType.BOOLEAN).description("수정할 공지사항 중요 여부 (상단에 배치)") diff --git a/my-garden-fe/src/components/boards/learn/api/api.js b/my-garden-fe/src/components/boards/learn/api/api.js index 13eeca52..f43192a4 100644 --- a/my-garden-fe/src/components/boards/learn/api/api.js +++ b/my-garden-fe/src/components/boards/learn/api/api.js @@ -23,7 +23,7 @@ export function getLearnBoardCategoryApi(boardType) { return data.data; }) .catch(error => { - alert('TIL 카테고리를 불러오는데 실패했습니다.') + alert('TIL 분류를 불러오는데 실패했습니다.') }); } diff --git a/my-garden-fe/src/components/boards/notice/api/api.js b/my-garden-fe/src/components/boards/notice/api/api.js index fe4ff3e1..a16d3a94 100644 --- a/my-garden-fe/src/components/boards/notice/api/api.js +++ b/my-garden-fe/src/components/boards/notice/api/api.js @@ -33,7 +33,7 @@ export function getNoticeBoardCategoryApi(boardType) { return data.data; }) .catch(error => { - alert('공지사항 카테고리를 불러오는데 실패했습니다.') + alert('공지사항 분류를 불러오는데 실패했습니다.') }); } From aed8e7cb00f68a5818bb5d98dffba171b5912e9c Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 13:49:54 +0900 Subject: [PATCH 04/12] =?UTF-8?q?chore:=20boards.learn=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/LearnBoardController.java | 54 ++++++++ .../learn/controller/request/PostRequest.java | 3 + .../boards/learn/entity/LearnBoardEntity.java | 118 ++++++++++++++++++ .../repository/LearnBoardRepository.java | 3 + .../LearnBoardRepositoryCustom.java | 13 ++ .../LearnBoardRepositoryCustomImpl.java | 50 ++++++++ .../learn/service/LearnBoardService.java | 116 +++++++++++++++++ .../service/response/LearnBoardResponse.java | 52 ++++++-- 8 files changed, 401 insertions(+), 8 deletions(-) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/LearnBoardController.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/LearnBoardController.java index d34f9627..0ce192e3 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/LearnBoardController.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/LearnBoardController.java @@ -13,12 +13,24 @@ import org.springframework.data.domain.Sort; import org.springframework.web.bind.annotation.*; +/** + * TIL 게시판 Controller + */ @RestController @RequestMapping("/api/boards/learn") @RequiredArgsConstructor public class LearnBoardController { + /** + * TIL 게시판 Service + */ private final LearnBoardService learnBoardService; + /** + * TIL 게시판 목록 조회 + * + * @param getRequest 조회 조건 + * @return TIL 게시판 목록 + */ @GetMapping("/list") public ApiResponse> getDailyRoutine(@ModelAttribute final GetRequest getRequest) { final CustomPage learnBoardResponses = getLearnBoards(getRequest.getSearchDate(), getRequest.getSearchCondition(), getRequest.getSearchPaging()); @@ -26,6 +38,14 @@ public ApiResponse> getDailyRoutine(@ModelAttribu return ApiResponse.ok(learnBoardResponses); } + /** + * TIL 게시판 Service를 통해서 목록 조회 + * + * @param searchDate 조회 기간 + * @param searchCondition 조회 조건 + * @param searchPaging 페이징 + * @return TIL 게시판 목록 + */ private CustomPage getLearnBoards(final GetRequest.SearchDate searchDate, final GetRequest.SearchCondition searchCondition, final GetRequest.SearchPaging searchPaging) { return learnBoardService.getLearnBoards( searchDate.startDate(), @@ -36,6 +56,12 @@ private CustomPage getLearnBoards(final GetRequest.SearchDat ); } + /** + * 페이징 정보를 통해서 PageRequest 생성 + * + * @param searchPaging 페이징 정보 + * @return PageRequest + */ private PageRequest buildPageable(final GetRequest.SearchPaging searchPaging) { return PageRequest.of( searchPaging.currentPage(), @@ -44,21 +70,49 @@ private PageRequest buildPageable(final GetRequest.SearchPaging searchPaging) { ); } + /** + * TIL 게시판 조회 + * + * @param boardId 게시판 ID + * @return TIL 게시판 + */ @GetMapping("/{boardId}") public ApiResponse getLearnBoard(@PathVariable final Long boardId) { return ApiResponse.ok(learnBoardService.getLearnBoard(boardId)); } + /** + * TIL 게시판 등록 + * + * @param postRequest 게시판에 등록할 정보 + * @param member 로그인한 유저 (JWT에서 추출한 정보) + * @return 등록된 게시판 ID + */ @PostMapping public ApiResponse postLearnBoard(@RequestBody final PostRequest postRequest, @WithLoginUserEntity final MemberEntity member) { return ApiResponse.ok(learnBoardService.postLearnBoard(postRequest.category(), postRequest.title(), postRequest.content(), member)); } + /** + * TIL 게시판 수정 + * + * @param boardId 게시판 ID + * @param postRequest 게시판에 수정할 정보 + * @param member 로그인한 유저 (JWT에서 추출한 정보) + * @return 수정된 게시판 ID + */ @PutMapping("/{boardId}") public ApiResponse putLearnBoard(@PathVariable final Long boardId, @RequestBody final PostRequest postRequest, @WithLoginUserEntity final MemberEntity member) { return ApiResponse.ok(learnBoardService.putLearnBoard(boardId, postRequest.category(), postRequest.title(), postRequest.content(), member)); } + /** + * TIL 게시판 삭제 + * + * @param boardId 게시판 ID + * @param member 로그인한 유저 (JWT에서 추출한 정보) + * @return 삭제된 게시판 ID + */ @DeleteMapping("/{boardId}") public ApiResponse deleteLearnBoard(@PathVariable final Long boardId, @WithLoginUserEntity final MemberEntity member) { return ApiResponse.ok(learnBoardService.deleteLearnBoard(boardId, member)); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java index ce0fdc75..7255de93 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java @@ -4,6 +4,9 @@ import jakarta.validation.constraints.Size; import lombok.Builder; +/** + * TIL 게시글 작성시에 사용되는 Request + */ @Builder public record PostRequest( @NotBlank(message = "제목은 비어있을 수 없습니다.") diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java index c143f6cf..7c6b16de 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/entity/LearnBoardEntity.java @@ -8,25 +8,59 @@ import java.time.LocalDateTime; +/** + * TIL 게시판 Entity + */ @Getter @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Entity public class LearnBoardEntity extends BaseEntity { + /** + * 게시글 ID + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + /** + * 제목 + */ @Column(nullable = false, length = 100) private String title; + + /** + * 내용 + */ @Column(nullable = false, length = 4000) private String content; + + /** + * 분류 + */ @Column(nullable = false, length = 20) private String category; + + /** + * 조회수 + */ @Column(nullable = false) private Integer views; + + /** + * 작성자 + */ @Column(nullable = false, length = 30) private String writer; + + /** + * 작성일 + */ @Column(nullable = false, length = 15) private LocalDateTime writtenAt; + + /** + * 작성자 ID + */ @Column(nullable = false) private Long memberId; @@ -42,10 +76,31 @@ private LearnBoardEntity(final String title, final String content, final String this.views = 0; } + /** + * TIL 게시글 Entity 생성 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param writer 작성자 + * @param memberId 작성자 ID + * @return TIL 게시글 Entity + */ public static LearnBoardEntity of(final String title, final String content, final String category, final String writer, final Long memberId) { return of(title, content, category, writer, LocalDateTime.now(), memberId); } + /** + * TIL 게시글 Entity 생성 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param writer 작성자 + * @param writtenAt 작성일 + * @param memberId 작성자 ID + * @return TIL 게시글 Entity + */ public static LearnBoardEntity of(final String title, final String content, final String category, final String writer, final LocalDateTime writtenAt, final Long memberId) { validateConstructor(title, content, category, writer, writtenAt, memberId); @@ -59,6 +114,16 @@ public static LearnBoardEntity of(final String title, final String content, fina .build(); } + /** + * 생성자 유효성 검사 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param writer 작성자 + * @param writtenAt 작성일 + * @param memberId 작성자 ID + */ private static void validateConstructor(final String title, final String content, final String category, final String writer, final LocalDateTime writtenAt, final Long memberId) { validateTitle(title); validateContent(content); @@ -68,46 +133,86 @@ private static void validateConstructor(final String title, final String content validateMemberId(memberId); } + /** + * 제목 유효성 검사 + * + * @param title 제목 + */ private static void validateTitle(final String title) { if (title == null || title.isBlank()) { throw new IllegalArgumentException("제목은 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 내용 유효성 검사 + * + * @param content 내용 + */ private static void validateContent(final String content) { if (content == null || content.isBlank()) { throw new IllegalArgumentException("내용은 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 분류 유효성 검사 + * + * @param category 분류 + */ private static void validateCategory(final String category) { if (category == null || category.isBlank()) { throw new IllegalArgumentException("분류는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 작성자 유효성 검사 + * + * @param writer 작성자 + */ private static void validateWriter(final String writer) { if (writer == null || writer.isBlank()) { throw new IllegalArgumentException("작성자는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 작성일 유효성 검사 + * + * @param writtenAt 작성일 + */ private static void validateWrittenAt(final LocalDateTime writtenAt) { if (writtenAt == null) { throw new IllegalArgumentException("작성일은 null이 될 수 없습니다."); } } + /** + * 작성자 ID 유효성 검사 + * + * @param memberId 작성자 ID + */ private static void validateMemberId(final Long memberId) { if (memberId == null || memberId <= 0) { throw new IllegalArgumentException("멤버 아이디는 null이 될 수 없고 0보다 커야 합니다."); } } + /** + * 조회수 증가 + */ public void increaseViewCount() { this.views++; } + /** + * 게시글 수정 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + */ public void update(final String title, final String content, final String category) { validateUpdate(title, content, category); @@ -116,12 +221,25 @@ public void update(final String title, final String content, final String catego this.category = category; } + /** + * 게시글 수정 유효성 검사 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + */ private void validateUpdate(final String title, final String content, final String category) { validateTitle(title); validateContent(content); validateCategory(category); } + /** + * 현재 글의 작성자가 맞는지 여부 확인 + * + * @param memberId 멤버 아이디 + * @return 작성자 여부 확인 + */ public boolean isWriter(final Long memberId) { return this.memberId.equals(memberId); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepository.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepository.java index 7821b44d..3851e285 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepository.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepository.java @@ -3,5 +3,8 @@ import org.hyunggi.mygardenbe.boards.learn.entity.LearnBoardEntity; import org.springframework.data.jpa.repository.JpaRepository; +/** + * TIL 게시판 Entity Repository + */ public interface LearnBoardRepository extends JpaRepository, LearnBoardRepositoryCustom { } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustom.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustom.java index cea6efa1..993a20e7 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustom.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustom.java @@ -6,6 +6,19 @@ import java.time.LocalDateTime; +/** + * TIL 게시판 Entity Repository Custom + */ public interface LearnBoardRepositoryCustom { + /** + * 게시글 검색 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return 게시글 목록 + */ Page searchLearnBoards(LocalDateTime startDateTime, LocalDateTime endDateTime, String category, String searchText, Pageable pageable); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustomImpl.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustomImpl.java index 1ef448cb..6b9a1054 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustomImpl.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/repository/LearnBoardRepositoryCustomImpl.java @@ -14,11 +14,24 @@ import static org.hyunggi.mygardenbe.boards.learn.entity.QLearnBoardEntity.learnBoardEntity; +/** + * TIL 게시판 Entity Repository Custom 구현체 + */ public class LearnBoardRepositoryCustomImpl extends Querydsl4RepositorySupport implements LearnBoardRepositoryCustom { public LearnBoardRepositoryCustomImpl() { super(LearnBoardEntity.class); } + /** + * TIL 게시글 Entity 검색 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return 게시글 목록 (페이지) + */ @Override public Page searchLearnBoards(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText, final Pageable pageable) { return applyPagination( @@ -28,6 +41,15 @@ public Page searchLearnBoards(final LocalDateTime startDateTim ); } + /** + * 조건에 맞는 TIL 게시글 조회 쿼리 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @return 조건에 맞는 TIL 게시글 조회 쿼리 + */ private Function getContentQuery(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText) { return queryFactory -> queryFactory .selectFrom(learnBoardEntity) @@ -38,6 +60,15 @@ private Function getContentQuery(final LocalDateTime ); } + /** + * 조건에 맞는 TIL 게시글 개수 조회 쿼리 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @return 조건에 맞는 TIL 게시글 개수 + */ private Function> getCountQuery(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText) { return queryFactory -> queryFactory .select(learnBoardEntity.count()) @@ -49,10 +80,23 @@ private Function> getCountQuery(final LocalDateT ); } + /** + * 작성일 범위 조건 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @return 작성일 범위 조건 + */ private BooleanExpression writtenAtBetween(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { return learnBoardEntity.writtenAt.between(startDateTime, endDateTime); } + /** + * 분류 조건 + * + * @param category 조회할 분류 + * @return 분류 조건 + */ private BooleanExpression categoryEquals(final String category) { if (!StringUtils.hasText(category)) { return null; @@ -61,6 +105,12 @@ private BooleanExpression categoryEquals(final String category) { return learnBoardEntity.category.eq(category); } + /** + * 검색어 포함 조건 + * + * @param searchText 검색어 + * @return 검색어 포함 조건 + */ private BooleanExpression searchTextContains(final String searchText) { if (!StringUtils.hasText(searchText)) { return null; diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java index 05596e27..61135aa5 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/LearnBoardService.java @@ -18,12 +18,32 @@ import java.time.LocalDate; import java.time.LocalDateTime; +/** + * TIL 게시판 Service + */ @Service @RequiredArgsConstructor public class LearnBoardService { + /** + * TIL 게시판 Entity Repository + */ private final LearnBoardRepository learnBoardRepository; + + /** + * 게시판 분류 Service + */ private final BoardCategoryService boardCategoryService; + /** + * TIL 게시판 목록 조회 + * + * @param startDate 조회 시작일 + * @param endDate 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return TIL 게시판 목록 (페이지) + */ public CustomPage getLearnBoards(final LocalDate startDate, final LocalDate endDate, final String category, final String searchText, final Pageable pageable) { validateArguments(startDate, endDate, category, searchText, pageable); @@ -33,6 +53,15 @@ public CustomPage getLearnBoards(final LocalDate startDate, return searchLearnBoards(startDateTime, endDateTime, category, searchText, pageable); } + /** + * 게시판 목록 조회의 인자 검증 + * + * @param startDate 조회 시작일 + * @param endDate 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + */ private void validateArguments(final LocalDate startDate, final LocalDate endDate, final String category, final String searchText, final Pageable pageable) { Assert.isTrue(startDate != null, "시작일은 null이 될 수 없습니다."); Assert.isTrue(endDate != null, "종료일은 null이 될 수 없습니다."); @@ -43,12 +72,28 @@ private void validateArguments(final LocalDate startDate, final LocalDate endDat Assert.isTrue(pageable != null, "페이징 정보는 null이 될 수 없습니다."); } + /** + * TIL 게시글 검색 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return TIL 게시글 목록 (페이지) + */ private CustomPage searchLearnBoards(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText, final Pageable pageable) { final Page learnBoardEntityPage = learnBoardRepository.searchLearnBoards(startDateTime, endDateTime, category, searchText, pageable); return CustomPage.of(learnBoardEntityPage.map(LearnBoardResponse::of)); } + /** + * TIL 게시글 조회 + * + * @param boardId 게시글 ID + * @return TIL 게시글 + */ @Transactional public LearnBoardResponse getLearnBoard(final Long boardId) { validateBoardId(boardId); @@ -59,15 +104,35 @@ public LearnBoardResponse getLearnBoard(final Long boardId) { return LearnBoardResponse.of(learnBoardEntity); } + /** + * 게시글 ID 유효성 검사 + * + * @param boardId 게시글 ID + */ private void validateBoardId(final Long boardId) { Assert.isTrue(boardId != null && boardId > 0, "boardId는 null이 될 수 없고 0보다 커야합니다."); } + /** + * TIL 게시글 Entity 조회 + * + * @param boardId 게시글 ID + * @return 조회한 게시글 Entity + */ private LearnBoardEntity getLearnBoardEntity(final Long boardId) { return learnBoardRepository.findById(boardId) .orElseThrow(() -> new EntityNotFoundException("해당 게시글이 존재하지 않습니다.")); } + /** + * TIL 게시글 작성 + * + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param member 유저 Entity + * @return 작성한 게시글 ID + */ public Long postLearnBoard(final String category, final String title, final String content, final MemberEntity member) { validatePostRequest(category, title, content); @@ -83,10 +148,24 @@ public Long postLearnBoard(final String category, final String title, final Stri return learnBoardRepository.save(learnBoardEntity).getId(); } + /** + * 게시글 작성의 인자 검증 + * + * @param category 분류 + * @param title 제목 + * @param content 내용 + */ private void validatePostRequest(final String category, final String title, final String content) { validateContent(category, title, content); } + /** + * 게시글 내용 유효성 검사 + * + * @param category 분류 + * @param title 제목 + * @param content 내용 + */ private void validateContent(final String category, final String title, final String content) { boardCategoryService.validateCategoryWithBoardType(category, "learn"); @@ -96,10 +175,26 @@ private void validateContent(final String category, final String title, final St Assert.isTrue(content.length() <= 4000, "내용은 4000자를 넘을 수 없습니다."); } + /** + * 유저 Entity에서 작성자의 이메일 ID 추출 + * + * @param member 유저 Entity + * @return 이메일 ID + */ private String getMemberEmailId(final MemberEntity member) { return member.getEmail().split("@")[0]; } + /** + * TIL 게시글 수정 + * + * @param boardId 게시글 ID + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param member 유저 Entity + * @return 수정한 게시글 ID + */ @Transactional public Long putLearnBoard(final Long boardId, final String category, final String title, final String content, final MemberEntity member) { validatePutRequest(boardId, category, title, content); @@ -117,18 +212,39 @@ public Long putLearnBoard(final Long boardId, final String category, final Strin return boardId; } + /** + * 게시글 작성자 유효성 검사 + * + * @param member 유저 Entity + * @param learnBoardEntity 게시글 Entity + */ private void validateBoardWriter(final MemberEntity member, final LearnBoardEntity learnBoardEntity) { if (!learnBoardEntity.isWriter(member.getId())) { throw new IllegalArgumentException("해당 게시글의 작성자가 아닙니다."); } } + /** + * 게시글 수정의 인자 검증 + * + * @param boardId 게시글 ID + * @param category 분류 + * @param title 제목 + * @param content 내용 + */ private void validatePutRequest(final Long boardId, final String category, final String title, final String content) { validateBoardId(boardId); validateContent(category, title, content); } + /** + * TIL 게시글 삭제 + * + * @param boardId 게시글 ID + * @param member 유저 Entity + * @return 삭제한 게시글 ID + */ @Transactional public Long deleteLearnBoard(final Long boardId, final MemberEntity member) { validateBoardId(boardId); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/response/LearnBoardResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/response/LearnBoardResponse.java index 8826627a..b0d29f5a 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/response/LearnBoardResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/service/response/LearnBoardResponse.java @@ -6,14 +6,44 @@ import static java.time.format.DateTimeFormatter.ofPattern; +/** + * TIL 게시판 응답 + */ @Getter public class LearnBoardResponse { + /** + * 게시글 ID + */ private final Long id; + + /** + * 제목 + */ private final String title; + + /** + * 내용 + */ private final String content; + + /** + * 분류 + */ private final String category; + + /** + * 조회수 + */ private final Integer views; + + /** + * 작성자 + */ private final String writer; + + /** + * 작성일 + */ private final String writtenAt; @Builder @@ -27,15 +57,21 @@ private LearnBoardResponse(final Long id, final String title, final String conte this.writtenAt = writtenAt; } - public static LearnBoardResponse of(final LearnBoardEntity noticeBoardEntity) { + /** + * Entity -> Response 변환 + * + * @param learnBoardEntity 게시판 Entity + * @return 게시판 응답 + */ + public static LearnBoardResponse of(final LearnBoardEntity learnBoardEntity) { return LearnBoardResponse.builder() - .id(noticeBoardEntity.getId()) - .title(noticeBoardEntity.getTitle()) - .content(noticeBoardEntity.getContent()) - .category(noticeBoardEntity.getCategory()) - .views(noticeBoardEntity.getViews()) - .writer(noticeBoardEntity.getWriter()) - .writtenAt(noticeBoardEntity.getWrittenAt().format(ofPattern("yyyy-MM-dd HH:mm:ss"))) + .id(learnBoardEntity.getId()) + .title(learnBoardEntity.getTitle()) + .content(learnBoardEntity.getContent()) + .category(learnBoardEntity.getCategory()) + .views(learnBoardEntity.getViews()) + .writer(learnBoardEntity.getWriter()) + .writtenAt(learnBoardEntity.getWrittenAt().format(ofPattern("yyyy-MM-dd HH:mm:ss"))) .build(); } } From bc9404f3e532ab200d03f58fbd3afc52b3ef17d3 Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 14:39:23 +0900 Subject: [PATCH 05/12] =?UTF-8?q?chore:=20boards.notice=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/AuthenticationResponse.java | 6 +- .../response/BoardCategoryResponse.java | 6 +- .../learn/controller/request/PostRequest.java | 4 + .../controller/NoticeBoardController.java | 61 +++++++- .../controller/request/PostRequest.java | 8 ++ .../notice/entity/NoticeBoardEntity.java | 134 ++++++++++++++++++ .../repository/NoticeBoardRepository.java | 9 ++ .../NoticeBoardRepositoryCustom.java | 13 ++ .../NoticeBoardRepositoryCustomImpl.java | 55 +++++++ .../notice/service/NoticeBoardService.java | 128 +++++++++++++++++ .../service/response/NoticeBoardResponse.java | 40 ++++++ 11 files changed, 457 insertions(+), 7 deletions(-) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java index 65d017b6..ea11045d 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/auth/service/response/AuthenticationResponse.java @@ -4,9 +4,9 @@ /** * 로그인 성공시에 반환되는 응답 - *

- * - accessToken: JWT Access 토큰
- * - refreshToken: JWT Refresh 토큰 + * + * @param accessToken JWT Access 토큰 + * @param refreshToken JWT Refresh 토큰 */ @Builder public record AuthenticationResponse(String accessToken, String refreshToken) { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java index a1d945ef..e4380daa 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/common/category/response/BoardCategoryResponse.java @@ -5,9 +5,9 @@ /** * 게시판 분류 응답 - *

- * - code: 분류 코드
- * - text: 분류 명 + * + * @param code 분류 코드 + * @param text 분류 명 */ @Builder public record BoardCategoryResponse(String code, String text) { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java index 7255de93..6a8b23f7 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/learn/controller/request/PostRequest.java @@ -6,6 +6,10 @@ /** * TIL 게시글 작성시에 사용되는 Request + * + * @param title 제목 + * @param category 분류 + * @param content 내용 */ @Builder public record PostRequest( diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/NoticeBoardController.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/NoticeBoardController.java index b1d178e3..e0bc6915 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/NoticeBoardController.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/NoticeBoardController.java @@ -15,24 +15,49 @@ import java.util.List; +/** + * 공지사항 게시판 Controller + */ @RestController @RequestMapping("/api/boards/notice") @RequiredArgsConstructor public class NoticeBoardController { + /** + * 공지사항 게시판 Service + */ private final NoticeBoardService noticeBoardService; + /** + * 중요 공지사항 목록 조회 + * + * @return 중요 공지사항 목록 + */ @GetMapping("/important") public ApiResponse> getNoticeImportantBoards() { return ApiResponse.ok(noticeBoardService.getNoticeImportantBoards()); } + /** + * 공지사항 목록 조회 + * + * @param getRequest 조회 조건 + * @return 공지사항 목록 + */ @GetMapping("/list") - public ApiResponse> getDailyRoutine(@ModelAttribute final GetRequest getRequest) { + public ApiResponse> getNoticeBoards(@ModelAttribute final GetRequest getRequest) { final CustomPage noticeBoardResponses = getNoticeBoards(getRequest.getSearchDate(), getRequest.getSearchCondition(), getRequest.getSearchPaging()); return ApiResponse.ok(noticeBoardResponses); } + /** + * 공지사항 게시판 Service를 이용하여 목록 조회 + * + * @param searchDate 조회 일자 + * @param searchCondition 조회 조건 + * @param searchPaging 페이징 + * @return 공지사항 목록 + */ private CustomPage getNoticeBoards(final GetRequest.SearchDate searchDate, final GetRequest.SearchCondition searchCondition, final GetRequest.SearchPaging searchPaging) { return noticeBoardService.getNoticeBoards( searchDate.startDate(), @@ -43,6 +68,12 @@ private CustomPage getNoticeBoards(final GetRequest.SearchD ); } + /** + * 페이징 정보를 이용하여 PageRequest 생성 + * + * @param searchPaging 페이징 정보 + * @return PageRequest + */ private PageRequest buildPageable(final GetRequest.SearchPaging searchPaging) { return PageRequest.of( searchPaging.currentPage(), @@ -51,21 +82,49 @@ private PageRequest buildPageable(final GetRequest.SearchPaging searchPaging) { ); } + /** + * 공지사항 상세 조회 + * + * @param boardId 게시글 ID + * @return 공지사항 상세 + */ @GetMapping("/{boardId}") public ApiResponse getNoticeBoard(@PathVariable final Long boardId) { return ApiResponse.ok(noticeBoardService.getNoticeBoard(boardId)); } + /** + * 공지사항 등록 + * + * @param postRequest 등록할 게시글 내용 + * @param member 로그인 사용자 + * @return 등록한 게시글 ID + */ @PostMapping public ApiResponse postNoticeBoard(@RequestBody final PostRequest postRequest, @WithLoginUserEntity final MemberEntity member) { return ApiResponse.ok(noticeBoardService.postNoticeBoard(postRequest.category(), postRequest.title(), postRequest.content(), postRequest.isImportant(), member)); } + /** + * 공지사항 수정 + * + * @param boardId 게시글 ID + * @param postRequest 수정할 게시글 내용 + * @param member 로그인 사용자 + * @return 수정한 게시글 ID + */ @PutMapping("/{boardId}") public ApiResponse putNoticeBoard(@PathVariable final Long boardId, @RequestBody final PostRequest postRequest, @WithLoginUserEntity final MemberEntity member) { return ApiResponse.ok(noticeBoardService.putNoticeBoard(boardId, postRequest.category(), postRequest.title(), postRequest.content(), postRequest.isImportant(), member)); } + /** + * 공지사항 삭제 + * + * @param boardId 게시글 ID + * @param member 로그인 사용자 + * @return 삭제한 게시글 ID + */ @DeleteMapping("/{boardId}") public ApiResponse deleteNoticeBoard(@PathVariable final Long boardId, @WithLoginUserEntity final MemberEntity member) { return ApiResponse.ok(noticeBoardService.deleteNoticeBoard(boardId, member)); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/request/PostRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/request/PostRequest.java index 6afd5d05..57a6322a 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/request/PostRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/controller/request/PostRequest.java @@ -5,6 +5,14 @@ import jakarta.validation.constraints.Size; import lombok.Builder; +/** + * 공지사항 게시글 작성시에 사용되는 Request + * + * @param title 제목 + * @param category 분류 + * @param content 내용 + * @param isImportant 중요 여부 + */ @Builder public record PostRequest( @NotBlank(message = "제목은 비어있을 수 없습니다.") diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java index d2d77580..17807731 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/entity/NoticeBoardEntity.java @@ -8,27 +8,65 @@ import java.time.LocalDateTime; +/** + * 공지사항 게시판 Entity + */ @Getter @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Entity public class NoticeBoardEntity extends BaseEntity { + /** + * 게시글 ID + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + /** + * 제목 + */ @Column(nullable = false, length = 100) private String title; + + /** + * 내용 + */ @Column(nullable = false, length = 4000) private String content; + + /** + * 분류 + */ @Column(nullable = false, length = 20) private String category; + + /** + * 중요 여부 + */ @Column(nullable = false, length = 10) private Boolean isImportant; + + /** + * 조회수 + */ @Column(nullable = false) private Integer views; + + /** + * 작성자 + */ @Column(nullable = false, length = 30) private String writer; + + /** + * 작성일 + */ @Column(nullable = false, length = 15) private LocalDateTime writtenAt; + + /** + * 작성자 ID + */ @Column(nullable = false) private Long memberId; @@ -45,10 +83,32 @@ private NoticeBoardEntity(final String title, final String content, final String this.views = 0; } + /** + * 게시글 작성 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param writer 작성자 + * @param memberId 작성자 ID + * @return 게시글 Entity + */ public static NoticeBoardEntity of(final String title, final String content, final String category, final String writer, final Long memberId) { return of(title, content, category, false, writer, LocalDateTime.now(), memberId); } + /** + * 게시글 작성 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param isImportant 중요 여부 + * @param writer 작성자 + * @param writtenAt 작성일 + * @param memberId 작성자 ID + * @return 게시글 Entity + */ public static NoticeBoardEntity of(final String title, final String content, final String category, final Boolean isImportant, final String writer, final LocalDateTime writtenAt, final Long memberId) { validateConstructor(title, content, category, isImportant, writer, writtenAt, memberId); @@ -63,6 +123,17 @@ public static NoticeBoardEntity of(final String title, final String content, fin .build(); } + /** + * 게시글 작성의 인자 검증 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param isImportant 중요 여부 + * @param writer 작성자 + * @param writtenAt 작성일 + * @param memberId 작성자 ID + */ private static void validateConstructor(final String title, final String content, final String category, final Boolean isImportant, final String writer, final LocalDateTime writtenAt, final Long memberId) { validateTitle(title); validateContent(content); @@ -73,56 +144,105 @@ private static void validateConstructor(final String title, final String content validateMemberId(memberId); } + /** + * 제목 검증 + * + * @param title 제목 + */ private static void validateTitle(final String title) { if (title == null || title.isBlank()) { throw new IllegalArgumentException("제목은 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 내용 검증 + * + * @param content 내용 + */ private static void validateContent(final String content) { if (content == null || content.isBlank()) { throw new IllegalArgumentException("내용은 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 분류 검증 + * + * @param category 분류 + */ private static void validateCategory(final String category) { if (category == null || category.isBlank()) { throw new IllegalArgumentException("분류는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 중요 여부 검증 + * + * @param isImportant 중요 여부 + */ private static void validateIsImportant(final Boolean isImportant) { if (isImportant == null) { throw new IllegalArgumentException("알림글 여부는 null이 될 수 없습니다."); } } + /** + * 작성자 검증 + * + * @param writer 작성자 + */ private static void validateWriter(final String writer) { if (writer == null || writer.isBlank()) { throw new IllegalArgumentException("작성자는 null이 될 수 없고 빈 문자열이 될 수 없습니다."); } } + /** + * 작성일 검증 + * + * @param writtenAt 작성일 + */ private static void validateWrittenAt(final LocalDateTime writtenAt) { if (writtenAt == null) { throw new IllegalArgumentException("작성일은 null이 될 수 없습니다."); } } + /** + * 작성자 ID 검증 + * + * @param memberId 작성자 ID + */ private static void validateMemberId(final Long memberId) { if (memberId == null || memberId <= 0) { throw new IllegalArgumentException("멤버 아이디는 null이 될 수 없고 0보다 커야 합니다."); } } + /** + * 중요 게시글로 설정 + */ public void setImportant() { this.isImportant = true; } + /** + * 조회수 증가 + */ public void increaseViewCount() { this.views++; } + /** + * 게시글 수정 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param important 중요 여부 + */ public void update(final String title, final String content, final String category, final Boolean important) { validateUpdate(title, content, category, important); @@ -132,6 +252,14 @@ public void update(final String title, final String content, final String catego this.isImportant = important; } + /** + * 게시글 수정의 인자 검증 + * + * @param title 제목 + * @param content 내용 + * @param category 분류 + * @param important 중요 여부 + */ private void validateUpdate(final String title, final String content, final String category, final Boolean important) { validateTitle(title); validateContent(content); @@ -139,6 +267,12 @@ private void validateUpdate(final String title, final String content, final Stri validateIsImportant(important); } + /** + * 해당 게시글의 작성자인지 여부 확인 + * + * @param memberId 멤버 아이디 + * @return 작성자 여부 확인 + */ public boolean isWriter(final Long memberId) { return this.memberId.equals(memberId); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepository.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepository.java index 61af93af..400a1802 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepository.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepository.java @@ -5,6 +5,15 @@ import java.util.List; +/** + * 공지사항 게시판 Repository + */ public interface NoticeBoardRepository extends JpaRepository, NoticeBoardRepositoryCustom { + /** + * 중요 공지사항 조회 + * + * @param isImportant 중요 여부 + * @return 중요 공지사항 목록 + */ List findAllByIsImportantOrderByWrittenAtDesc(boolean isImportant); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustom.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustom.java index 672ff376..b0e4af31 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustom.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustom.java @@ -6,6 +6,19 @@ import java.time.LocalDateTime; +/** + * 공지사항 게시판 Repository Custom + */ public interface NoticeBoardRepositoryCustom { + /** + * 공지사항 게시판 목록 조회 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return 공지사항 게시판 목록 (페이지) + */ Page searchNoticeBoards(LocalDateTime startDateTime, LocalDateTime endDateTime, String category, String searchText, Pageable pageable); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustomImpl.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustomImpl.java index 70f9cf08..0ffdf4f5 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustomImpl.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/repository/NoticeBoardRepositoryCustomImpl.java @@ -14,11 +14,24 @@ import static org.hyunggi.mygardenbe.boards.notice.entity.QNoticeBoardEntity.noticeBoardEntity; +/** + * 공지사항 게시판 Repository Custom 구현체 + */ public class NoticeBoardRepositoryCustomImpl extends Querydsl4RepositorySupport implements NoticeBoardRepositoryCustom { public NoticeBoardRepositoryCustomImpl() { super(NoticeBoardEntity.class); } + /** + * 공지사항 게시판 목록 조회 (중요 여부 X인 공지사항) + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return 공지사항 게시판 목록 (페이지) + */ @Override public Page searchNoticeBoards(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText, final Pageable pageable) { return applyPagination( @@ -28,6 +41,15 @@ public Page searchNoticeBoards(final LocalDateTime startDateT ); } + /** + * 공지사항 게시판 목록 조회 쿼리 (중요 여부 X인 공지사항) + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @return 공지사항 게시판 목록 + */ private Function getContentQuery(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText) { return queryFactory -> queryFactory .selectFrom(noticeBoardEntity) @@ -39,6 +61,15 @@ private Function getContentQuery(final LocalDateTime ); } + /** + * 공지사항 게시글 수 조회 쿼리 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @return 공지사항 게시글 수 + */ private Function> getCountQuery(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText) { return queryFactory -> queryFactory .select(noticeBoardEntity.count()) @@ -51,10 +82,23 @@ private Function> getCountQuery(final LocalDateT ); } + /** + * 작성일 범위 조건 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @return 작성일 범위 조건 + */ private BooleanExpression writtenAtBetween(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { return noticeBoardEntity.writtenAt.between(startDateTime, endDateTime); } + /** + * 분류 조건 + * + * @param category 조회할 분류 + * @return 분류 조건 + */ private BooleanExpression categoryEquals(final String category) { if (!StringUtils.hasText(category)) { return null; @@ -63,6 +107,12 @@ private BooleanExpression categoryEquals(final String category) { return noticeBoardEntity.category.eq(category); } + /** + * 검색어 포함 조건 + * + * @param searchText 검색어 + * @return 검색어 포함 조건 + */ private BooleanExpression searchTextContains(final String searchText) { if (!StringUtils.hasText(searchText)) { return null; @@ -71,6 +121,11 @@ private BooleanExpression searchTextContains(final String searchText) { return noticeBoardEntity.title.contains(searchText).or(noticeBoardEntity.content.contains(searchText)); } + /** + * 중요 여부 조건 + * + * @return 중요 여부 조건 + */ private BooleanExpression isNotImportant() { return noticeBoardEntity.isImportant.eq(false); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java index 2bc42224..0088e11e 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/NoticeBoardService.java @@ -18,12 +18,32 @@ import java.time.LocalDateTime; import java.util.List; +/** + * 공지사항 게시판 Service + */ @Service @RequiredArgsConstructor public class NoticeBoardService { + /** + * 공지사항 게시판 Entity Repository + */ private final NoticeBoardRepository noticeBoardRepository; + + /** + * 게시판 분류 Service + */ private final BoardCategoryService boardCategoryService; + /** + * 공지사항 게시판 목록 조회 (중요 여부 X인 공지사항) + * + * @param startDate 조회 시작일 + * @param endDate 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return 공지사항 게시판 목록 (페이지) + */ public CustomPage getNoticeBoards(final LocalDate startDate, final LocalDate endDate, final String category, final String searchText, final Pageable pageable) { validateArguments(startDate, endDate, category, searchText, pageable); @@ -33,6 +53,15 @@ public CustomPage getNoticeBoards(final LocalDate startDate return searchNoticeBoards(startDateTime, endDateTime, category, searchText, pageable); } + /** + * 공지사항 게시판 목록 조회 인자 유효성 검증 + * + * @param startDate 조회 시작일 + * @param endDate 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + */ private void validateArguments(final LocalDate startDate, final LocalDate endDate, final String category, final String searchText, final Pageable pageable) { Assert.isTrue(startDate != null, "시작일은 null이 될 수 없습니다."); Assert.isTrue(endDate != null, "종료일은 null이 될 수 없습니다."); @@ -43,12 +72,30 @@ private void validateArguments(final LocalDate startDate, final LocalDate endDat Assert.isTrue(pageable != null, "페이징 정보는 null이 될 수 없습니다."); } + /** + * 공지사항 게시판 Service를 통해서 공지사항 목록 조회 + * + * @param startDateTime 조회 시작일 + * @param endDateTime 조회 종료일 + * @param category 조회할 분류 + * @param searchText 검색어 + * @param pageable 페이징 + * @return 공지사항 게시판 목록 (페이지) + */ private CustomPage searchNoticeBoards(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final String category, final String searchText, final Pageable pageable) { final Page noticeBoardEntityPage = noticeBoardRepository.searchNoticeBoards(startDateTime, endDateTime, category, searchText, pageable); return CustomPage.of(noticeBoardEntityPage.map(NoticeBoardResponse::of)); } + /** + * 공지사항 게시판 조회 + *

+ * - 조회시 조회수 증가 + * + * @param boardId 게시글 ID + * @return 공지사항 게시판 조회 결과 + */ @Transactional public NoticeBoardResponse getNoticeBoard(final Long boardId) { validateBoardId(boardId); @@ -59,15 +106,36 @@ public NoticeBoardResponse getNoticeBoard(final Long boardId) { return NoticeBoardResponse.of(noticeBoardEntity); } + /** + * 게시글 ID 유효성 검증 + * + * @param boardId 게시글 ID + */ private void validateBoardId(final Long boardId) { Assert.isTrue(boardId != null && boardId > 0, "boardId는 null이 될 수 없고 0보다 커야합니다."); } + /** + * 게시글 ID로 게시글 조회 + * + * @param boardId 게시글 ID + * @return 게시글 Entity + */ private NoticeBoardEntity getNoticeBoardEntity(final Long boardId) { return noticeBoardRepository.findById(boardId) .orElseThrow(() -> new EntityNotFoundException("해당 게시글이 존재하지 않습니다.")); } + /** + * 공지사항 게시판 작성 + * + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param isImportant 중요 여부 + * @param member 유저 Entity + * @return 게시글 ID + */ public Long postNoticeBoard(final String category, final String title, final String content, final Boolean isImportant, final MemberEntity member) { validatePostRequest(category, title, content, isImportant); @@ -84,10 +152,26 @@ public Long postNoticeBoard(final String category, final String title, final Str return noticeBoardRepository.save(noticeBoardEntity).getId(); } + /** + * 게시글 작성의 인자 검증 + * + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param isImportant 중요 여부 + */ private void validatePostRequest(final String category, final String title, final String content, final Boolean isImportant) { validateContent(category, title, content, isImportant); } + /** + * 게시글 작성의 내용 검증 + * + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param isImportant 중요 여부 + */ private void validateContent(final String category, final String title, final String content, final Boolean isImportant) { boardCategoryService.validateCategoryWithBoardType(category, "notice"); @@ -98,10 +182,27 @@ private void validateContent(final String category, final String title, final St Assert.isTrue(isImportant != null, "중요 여부는 null이 될 수 없습니다."); } + /** + * 유저 Entity에서 작성자의 이메일 ID 추출 + * + * @param member 유저 Entity + * @return 이메일 ID + */ private String getMemberEmailId(final MemberEntity member) { return member.getEmail().split("@")[0]; } + /** + * 공지사항 게시판 수정 + * + * @param boardId 게시글 ID + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param isImportant 중요 여부 + * @param member 유저 Entity + * @return 게시글 ID + */ @Transactional public Long putNoticeBoard(final Long boardId, final String category, final String title, final String content, final Boolean isImportant, final MemberEntity member) { validatePutRequest(boardId, category, title, content, isImportant); @@ -120,18 +221,40 @@ public Long putNoticeBoard(final Long boardId, final String category, final Stri return boardId; } + /** + * 게시글 작성자 유효성 검증 + * + * @param member 유저 Entity + * @param noticeBoardEntity 게시글 Entity + */ private void validateBoardWriter(final MemberEntity member, final NoticeBoardEntity noticeBoardEntity) { if (!noticeBoardEntity.isWriter(member.getId())) { throw new IllegalArgumentException("해당 게시글의 작성자가 아닙니다."); } } + /** + * 게시글 수정의 인자 검증 + * + * @param boardId 게시글 ID + * @param category 분류 + * @param title 제목 + * @param content 내용 + * @param isImportant 중요 여부 + */ private void validatePutRequest(final Long boardId, final String category, final String title, final String content, final Boolean isImportant) { validateBoardId(boardId); validateContent(category, title, content, isImportant); } + /** + * 공지사항 게시판 삭제 + * + * @param boardId 게시글 ID + * @param member 유저 Entity + * @return 삭제한 게시글 ID + */ @Transactional public Long deleteNoticeBoard(final Long boardId, final MemberEntity member) { validateBoardId(boardId); @@ -145,6 +268,11 @@ public Long deleteNoticeBoard(final Long boardId, final MemberEntity member) { return boardId; } + /** + * 중요 공지사항 게시판 목록 조회 + * + * @return 중요 공지사항 게시판 목록 + */ public List getNoticeImportantBoards() { final List noticeAlarmBoards = noticeBoardRepository.findAllByIsImportantOrderByWrittenAtDesc(true); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/response/NoticeBoardResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/response/NoticeBoardResponse.java index 1c591aa9..be1ee112 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/response/NoticeBoardResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/boards/notice/service/response/NoticeBoardResponse.java @@ -6,15 +6,49 @@ import static java.time.format.DateTimeFormatter.ofPattern; +/** + * 공지사항 게시판 응답 + */ @Getter public class NoticeBoardResponse { + /** + * 게시글 ID + */ private final Long id; + + /** + * 제목 + */ private final String title; + + /** + * 내용 + */ private final String content; + + /** + * 분류 + */ private final String category; + + /** + * 중요 여부 + */ private final Boolean isImportant; + + /** + * 조회수 + */ private final Integer views; + + /** + * 작성자 + */ private final String writer; + + /** + * 작성일 + */ private final String writtenAt; @Builder @@ -29,6 +63,12 @@ private NoticeBoardResponse(final Long id, final String title, final String cont this.writtenAt = writtenAt; } + /** + * Entity -> Response 변환 + * + * @param noticeBoardEntity 게시판 Entity + * @return 게시판 응답 + */ public static NoticeBoardResponse of(final NoticeBoardEntity noticeBoardEntity) { return NoticeBoardResponse.builder() .id(noticeBoardEntity.getId()) From 9456011eb7e8c795d0adac9eb4199ea9fa1299bf Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 15:06:36 +0900 Subject: [PATCH 06/12] =?UTF-8?q?chore:=20common=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/annotation/WithLoginUserEntity.java | 5 ++ .../mygardenbe/common/entity/BaseEntity.java | 6 +++ .../common/exception/BusinessException.java | 5 ++ .../InvalidTokenRequestException.java | 5 ++ .../controlleradvice/ApiControllerAdvice.java | 30 ++++++++++- .../ServerErrorDetectControllerAdvice.java | 41 +++++++++++++++ .../support/Querydsl4RepositorySupport.java | 44 ++++++++++++++++ .../common/response/ApiResponse.java | 52 +++++++++++++++++++ .../common/view/filter/HistoryModeFilter.java | 35 ++++++++++--- 9 files changed, 216 insertions(+), 7 deletions(-) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/auth/annotation/WithLoginUserEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/auth/annotation/WithLoginUserEntity.java index c46c5aac..d499446d 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/auth/annotation/WithLoginUserEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/auth/annotation/WithLoginUserEntity.java @@ -4,6 +4,11 @@ import java.lang.annotation.*; +/** + * 로그인 유저 Entity 어노테이션 + *

+ * - 로그인 유저 Entity를 주입받기 위한 어노테이션 + */ @Target({ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/entity/BaseEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/entity/BaseEntity.java index 430a02e7..34199d67 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/entity/BaseEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/entity/BaseEntity.java @@ -9,6 +9,12 @@ import java.time.LocalDateTime; +/** + * Base Entity + *

+ * - 공통 Entity + * - 생성일, 수정일을 관리 + */ @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/BusinessException.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/BusinessException.java index 05bf152e..88913006 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/BusinessException.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/BusinessException.java @@ -1,5 +1,10 @@ package org.hyunggi.mygardenbe.common.exception; +/** + * Business Exception + *

+ * - 비즈니스 로직에서 발생하는 예외 + */ public class BusinessException extends RuntimeException { public BusinessException(final String message) { super(message); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/InvalidTokenRequestException.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/InvalidTokenRequestException.java index edd1c7d4..aa29c693 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/InvalidTokenRequestException.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/InvalidTokenRequestException.java @@ -1,5 +1,10 @@ package org.hyunggi.mygardenbe.common.exception; +/** + * Invalid Token Request Exception + *

+ * - 토큰 요청이 유효하지 않을 때 발생하는 예외 + */ public class InvalidTokenRequestException extends RuntimeException { public InvalidTokenRequestException(String message) { super(message); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ApiControllerAdvice.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ApiControllerAdvice.java index 43622659..c2a42511 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ApiControllerAdvice.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ApiControllerAdvice.java @@ -10,10 +10,20 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +/** + * API Controller Advice + *

+ * - API Controller에서 발생하는 예외를 처리하는 Advice + */ @RestControllerAdvice @Slf4j public class ApiControllerAdvice { - + /** + * BindException 예외 처리 + * + * @param e BindException + * @return ApiResponse + */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BindException.class) public ApiResponse bindException(final BindException e) { @@ -27,6 +37,12 @@ public ApiResponse bindException(final BindException e) { ); } + /** + * EntityNotFoundException 예외 처리 + * + * @param e EntityNotFoundException + * @return ApiResponse + */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(EntityNotFoundException.class) public ApiResponse entityNotFoundException(final EntityNotFoundException e) { @@ -40,6 +56,12 @@ public ApiResponse entityNotFoundException(final EntityNotFoundException ); } + /** + * IllegalArgumentException 예외 처리 + * + * @param e IllegalArgumentException + * @return ApiResponse + */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(IllegalArgumentException.class) public ApiResponse illegalArgumentException(final IllegalArgumentException e) { @@ -53,6 +75,12 @@ public ApiResponse illegalArgumentException(final IllegalArgumentExcepti ); } + /** + * BusinessException 예외 처리 + * + * @param e BusinessException + * @return ApiResponse + */ @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(BusinessException.class) public ApiResponse businessException(final BusinessException e) { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ServerErrorDetectControllerAdvice.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ServerErrorDetectControllerAdvice.java index fdbc997c..358337c3 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ServerErrorDetectControllerAdvice.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/exception/controlleradvice/ServerErrorDetectControllerAdvice.java @@ -16,13 +16,29 @@ import java.util.Date; import java.util.List; +/** + * Server Error Detect Controller Advice + *

+ * - 서버 에러 발생 시 Slack으로 알림을 보내는 Controller Advice + * - Profile이 prod일 때만 동작 + */ @Profile("prod") @RequiredArgsConstructor @RestControllerAdvice @Slf4j public class ServerErrorDetectControllerAdvice { + /** + * Slack API Bean + */ private final SlackApi slackApi; + /** + * Exception 발생 시 Slack으로 알림을 보냄 + * + * @param req HttpServletRequest + * @param e Exception + * @throws Exception Exception + */ @ExceptionHandler(Exception.class) public void handleException(final HttpServletRequest req, final Exception e) throws Exception { log.error("Server Error : {}", e.getMessage(), e); @@ -31,6 +47,12 @@ public void handleException(final HttpServletRequest req, final Exception e) thr throw e; } + /** + * Slack Message 생성 + * + * @param slackAttachment Slack Attachment + * @return Slack Message + */ private SlackMessage buildSlackMessage(final SlackAttachment slackAttachment) { final SlackMessage slackMessage = new SlackMessage(); @@ -42,6 +64,13 @@ private SlackMessage buildSlackMessage(final SlackAttachment slackAttachment) { return slackMessage; } + /** + * Slack Attachment 생성 + * + * @param req HttpServletRequest + * @param e Exception + * @return Slack Attachment + */ private SlackAttachment buildSlackAttachment(final HttpServletRequest req, final Exception e) { final SlackAttachment slackAttachment = new SlackAttachment(); @@ -56,6 +85,12 @@ private SlackAttachment buildSlackAttachment(final HttpServletRequest req, final return slackAttachment; } + /** + * Exception Stack Trace를 String으로 변환 + * + * @param e Exception + * @return Stack Trace + */ private String getStackTrace(final Exception e) { StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); @@ -63,6 +98,12 @@ private String getStackTrace(final Exception e) { return sw.toString(); } + /** + * Slack Attachment Field 추가 + * + * @param req HttpServletRequest + * @return Slack Attachment Field + */ private List addField(final HttpServletRequest req) { return List.of( new SlackField().setTitle("Request URL").setValue(req.getRequestURL().toString()), diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/querydsl/support/Querydsl4RepositorySupport.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/querydsl/support/Querydsl4RepositorySupport.java index 24ee2e9a..8c148591 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/querydsl/support/Querydsl4RepositorySupport.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/querydsl/support/Querydsl4RepositorySupport.java @@ -39,6 +39,9 @@ protected Querydsl4RepositorySupport(Class domainClass) { this.domainClass = domainClass; } + /** + * Setter injection이 제대로 되었는지 검증 + */ @PostConstruct public void validate() { Assert.notNull(entityManager, "EntityManager must not be null!"); @@ -46,18 +49,36 @@ public void validate() { Assert.notNull(queryFactory, "QueryFactory must not be null!"); } + /** + * JPAQueryFactory를 반환 + * + * @return JPAQueryFactory + */ protected JPAQueryFactory getQueryFactory() { return queryFactory; } + /** + * Querydsl을 반환 + * + * @return Querydsl + */ protected Querydsl getQuerydsl() { return querydsl; } + /** + * EntityManager를 반환 + * + * @return EntityManager + */ protected EntityManager getEntityManager() { return entityManager; } + /** + * EntityManager를 주입하고, Querydsl과 JPAQueryFactory를 초기화 + */ @Autowired public void setEntityManager(EntityManager entityManager) { Assert.notNull(entityManager, "EntityManager must not be null!"); @@ -71,14 +92,37 @@ public void setEntityManager(EntityManager entityManager) { this.queryFactory = new JPAQueryFactory(entityManager); } + /** + * JPAQueryFactory를 이용하여 JPAQuery의 select 쿼리를 생성 + * + * @param expr select할 Expression + * @param 반환 타입 + * @return JPAQuery + */ protected JPAQuery select(Expression expr) { return getQueryFactory().select(expr); } + /** + * JPAQueryFactory를 이용하여 JPAQuery의 selectFrom 쿼리를 생성 + * + * @param from select할 EntityPath + * @param 반환 타입 + * @return JPAQuery + */ protected JPAQuery selectFrom(EntityPath from) { return getQueryFactory().selectFrom(from); } + /** + * Querydsl을 이용하여 페이징 처리 + * + * @param pageable 페이징 정보 + * @param contentQuery 콘텐츠 조회 쿼리 + * @param countQuery 콘텐츠 개수 조회 쿼리 + * @param 반환 타입 + * @return 페이징 처리된 결과 + */ protected Page applyPagination(Pageable pageable, Function contentQuery, Function> countQuery) { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/response/ApiResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/response/ApiResponse.java index 6c029376..1dadf15e 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/response/ApiResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/response/ApiResponse.java @@ -3,11 +3,33 @@ import lombok.Getter; import org.springframework.http.HttpStatus; +/** + * API 응답 + *

+ * - API 응답을 위한 클래스 + * + * @param 응답 데이터 타입 + */ @Getter public class ApiResponse { + /** + * Http 응답 코드 + */ private final int code; + + /** + * Http 응답 상태 + */ private final HttpStatus status; + + /** + * 응답 메시지 + */ private final String message; + + /** + * 응답 데이터 + */ private final T data; private ApiResponse(final HttpStatus status, final String message, final T data) { @@ -17,18 +39,48 @@ private ApiResponse(final HttpStatus status, final String message, final T data) this.data = data; } + /** + * 성공 응답 - 데이터 없음 + * + * @param 응답 데이터 타입 + * @return ApiResponse - 데이터 없음 + */ public static ApiResponse ok() { return ok(null); } + /** + * 성공 응답 + * + * @param data 응답 데이터 + * @param 응답 데이터 타입 + * @return ApiResponse + */ public static ApiResponse ok(final T data) { return of(HttpStatus.OK, data); } + /** + * 응답 생성 + * + * @param httpStatus Http 응답 상태 + * @param data 응답 데이터 + * @param 응답 데이터 타입 + * @return ApiResponse + */ public static ApiResponse of(final HttpStatus httpStatus, final T data) { return of(httpStatus, httpStatus.name(), data); } + /** + * 응답 생성 + * + * @param httpStatus Http 응답 상태 + * @param message 응답 메시지 + * @param data 응답 데이터 + * @param 응답 데이터 타입 + * @return ApiResponse + */ public static ApiResponse of(final HttpStatus httpStatus, final String message, final T data) { return new ApiResponse<>(httpStatus, message, data); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/view/filter/HistoryModeFilter.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/view/filter/HistoryModeFilter.java index baf94841..5a79ca6e 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/view/filter/HistoryModeFilter.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/common/view/filter/HistoryModeFilter.java @@ -11,16 +11,39 @@ import java.io.IOException; import java.util.regex.Pattern; -//Forwards all routes to the main route -//This is for SPA (Single Page Application) [Ex. React, Angular, Vue] +/** + * History Mode Filter + *

+ * - 모든 라우트를 메인 라우트로 포워딩 + * - SPA (Single Page Application)를 위한 필터 [Ex. React, Angular, Vue] + */ public class HistoryModeFilter extends OncePerRequestFilter { - // '/'로 시작하고 그 뒤에 점이 .이 없는 문자열과 일치 - // '/api'로 시작하는 URL은 제외 - // /home 또는 /user/profile과 일치, 하지만 /api/data, /api?query, /image.jpg, /script.js 와는 일치하지 않습니다. + /** + * forward할 url 패턴 정규식 + *

+ * - '/'로 시작하고 그 뒤에 점이 .이 없는 문자열과 일치 + * - '/api'로 시작하는 URL은 제외 + * - /home 또는 /user/profile과 일치, 하지만 /api/data, /api?query, /image.jpg, /script.js 와는 일치하지 않습니다. + */ private Pattern patt = Pattern.compile("^(?!/api)/([^.]*)"); - // Main route + + /** + * forward할 endpoint + */ private String endpoint = "/"; + /** + * url 패턴 Filter 처리 + *

+ * - forward할 url 패턴 정규식과 일치하면 endpoint로 forward + * - 그 외에는 filterChain으로 넘김 + * + * @param request HttpServletRequest + * @param response HttpServletResponse + * @param filterChain FilterChain + * @throws ServletException ServletException + * @throws IOException IOException + */ @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, From 042bb8b5ff1da6b2e9f777d7e47fd97367c4ec15 Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 15:42:39 +0900 Subject: [PATCH 07/12] =?UTF-8?q?chore:=20configuration=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/HttpsConfiguration.java | 6 +++ .../JpaAuditingConfiguration.java | 5 ++ .../configuration/JwtBeanConfiguration.java | 31 +++++++++++ .../PrometheusConfiguration.java | 6 +++ .../configuration/SecurityConfiguration.java | 53 +++++++++++++++++++ .../SlackLogAppenderConfiguration.java | 13 +++++ 6 files changed, 114 insertions(+) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/HttpsConfiguration.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/HttpsConfiguration.java index ae231958..52c5ebec 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/HttpsConfiguration.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/HttpsConfiguration.java @@ -10,6 +10,12 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +/** + * Https Configuration + *

+ * - Https 설정 + * - Profile이 local이 아닐 때만 동작 + */ @Profile("!local") @Configuration public class HttpsConfiguration { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JpaAuditingConfiguration.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JpaAuditingConfiguration.java index 22d29149..4c49f4a2 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JpaAuditingConfiguration.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JpaAuditingConfiguration.java @@ -3,6 +3,11 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +/** + * JPA Auditing Configuration + *

+ * - JPA Auditing 활성화 + */ @EnableJpaAuditing @Configuration public class JpaAuditingConfiguration { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JwtBeanConfiguration.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JwtBeanConfiguration.java index 884c0085..e53fd186 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JwtBeanConfiguration.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/JwtBeanConfiguration.java @@ -11,22 +11,53 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +/** + * Jwt Bean Configuration + *

+ * - Jwt 관련 Bean 설정 + */ @RequiredArgsConstructor @Configuration public class JwtBeanConfiguration { + /** + * Member Entity Repository + */ private final MemberRepository memberRepository; + /** + * UserDetailsService Bean + *

+ * - 전달받은 username으로 Member Entity를 조회 + * + * @return UserDetailsService + */ @Bean public UserDetailsService userDetailsService() { return username -> memberRepository.findByEmail(username) .orElseThrow(() -> new EntityNotFoundException("Member not found")); } + /** + * PasswordEncoder Bean + *

+ * - BCryptPasswordEncoder 사용 (비밀번호 암호화) + * + * @return PasswordEncoder + */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + /** + * AuthenticationManager Bean + *

+ * - default 설정된 authenticationManager를 사용 + * + * @param config AuthenticationConfiguration + * @return AuthenticationManager + * @throws Exception Exception + */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/PrometheusConfiguration.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/PrometheusConfiguration.java index e6ecf355..57c6f5be 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/PrometheusConfiguration.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/PrometheusConfiguration.java @@ -6,6 +6,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +/** + * Prometheus Configuration + *

+ * - Prometheus 설정 + * - spring.security, spring.data.repository로 시작하는 metric은 제외 + */ @Configuration public class PrometheusConfiguration { @Bean diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SecurityConfiguration.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SecurityConfiguration.java index f1e376b6..cf1a21a2 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SecurityConfiguration.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SecurityConfiguration.java @@ -32,12 +32,20 @@ import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS; import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; +/** + * Security Configuration + *

+ * - Security 설정 + */ @RequiredArgsConstructor @Configuration @EnableWebSecurity @EnableMethodSecurity @Slf4j public class SecurityConfiguration { + /** + * 해당 하는 URL은 JWT 인증을 거치지 않음 + */ private static final String[] WHITE_LIST_URL = { AuthenticationController.AUTH_BASE_API_PATH + "/**", "/docs/index.html", @@ -48,12 +56,37 @@ public class SecurityConfiguration { "/api/boards/categories" }; + /** + * JWT 인증 필터 + */ private final JwtAuthenticationFilter jwtAuthFilter; + + /** + * 로그아웃 핸들러 + */ private final LogoutHandler myLogoutHandler; + /** + * Actuator URL + */ @Value("${actuator.url:/default}") private String actuatorUrl; + /** + * Security Filter Chain + *

+ * - Security Filter Chain 설정 + * - csrf, cors 비활성화 + * - 요청에 따른 권한 설정 + * - session 비활성화 + * - JWT 인증 필터, JWT 예외 처리 필터 추가 + * - HistoryModeFilter 추가 + * - 로그아웃 핸들러 추가 + * + * @param http HttpSecurity + * @return SecurityFilterChain + * @throws Exception Exception + */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -84,10 +117,20 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + /** + * Actuator URL + * + * @return Actuator URL (모든 URL) + */ private String getActuatorAllUrl() { return actuatorUrl + "/**"; } + /** + * Admin만 접근이 가능한 공지사항 API + * + * @return 공지사항 API + */ private RequestMatcher[] getOnlyAdminAccessNoticeApi() { return new RequestMatcher[]{ antMatcher(HttpMethod.POST, "/api/boards/notice"), @@ -96,6 +139,11 @@ private RequestMatcher[] getOnlyAdminAccessNoticeApi() { }; } + /** + * 공지사항, TIL 게시판 조회 API + * + * @return 공지사항, TIL 게시판 조회 API + */ private RequestMatcher[] getReadBoardsApi() { return new RequestMatcher[]{ antMatcher(HttpMethod.GET, "/api/boards/notice/**"), @@ -103,6 +151,11 @@ private RequestMatcher[] getReadBoardsApi() { }; } + /** + * Logout 성공시 처리될 응답 초기화 + * + * @param res Http 응답 + */ private void initializeResponse(final HttpServletResponse res) { res.setStatus(HttpServletResponse.SC_OK); res.setContentType(MediaType.APPLICATION_JSON_VALUE); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SlackLogAppenderConfiguration.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SlackLogAppenderConfiguration.java index bf591635..beeeff39 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SlackLogAppenderConfiguration.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/configuration/SlackLogAppenderConfiguration.java @@ -6,12 +6,25 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +/** + * Slack 관련 Bean 생성 Configuration + */ @Profile("prod") @Configuration public class SlackLogAppenderConfiguration { + /** + * 메세지를 전송할 Slack 채널의 주소 Token + */ @Value("${my-garden.slack.incoming-webhook-token}") String token; + /** + * SlackApi Bean + *

+ * - SlackApi Bean 생성 + * + * @return SlackApi + */ @Bean public SlackApi slackApi() { return new SlackApi("https://hooks.slack.com/services/" + token); From 37782f30f72f9520190353ce14345dc0e90e76ed Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 16:49:42 +0900 Subject: [PATCH 08/12] =?UTF-8?q?chore:=20dailyroutine=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DailyRoutineController.java | 35 ++++++ .../controller/request/GetRequest.java | 6 ++ .../controller/request/PostRequest.java | 8 ++ .../dailyroutine/domain/DailyRoutine.java | 79 ++++++++++++++ .../dailyroutine/domain/RoutineTime.java | 100 +++++++++++++++++- .../dailyroutine/domain/RoutineType.java | 6 ++ .../dailyroutine/domain/TimeSplitter.java | 24 +++++ .../entity/DailyRoutineEntity.java | 52 +++++++++ .../repository/DailyRoutineRepository.java | 11 ++ .../service/DailyRoutineService.java | 75 ++++++++++++- .../response/DailyRoutineResponse.java | 23 ++++ 11 files changed, 417 insertions(+), 2 deletions(-) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/DailyRoutineController.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/DailyRoutineController.java index 6df6cfb5..7b56e8f8 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/DailyRoutineController.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/DailyRoutineController.java @@ -17,17 +17,37 @@ import java.time.LocalDateTime; import java.util.List; +/** + * 데일리 루틴 Controller + */ @RestController @RequestMapping("/api/daily-routine") @RequiredArgsConstructor public class DailyRoutineController { + /** + * 데일리 루틴 Service + */ private final DailyRoutineService dailyRoutineService; + /** + * 데일리 루틴 조회 + * + * @param getRequest 조회 요청 + * @param member 유저 Entity + * @return 데일리 루틴 목록 응답 + */ @GetMapping public ApiResponse> getDailyRoutine(@ModelAttribute @Valid final GetRequest getRequest, @WithLoginUserEntity MemberEntity member) { return ApiResponse.ok(dailyRoutineService.getDailyRoutine(getRequest.startDateTime(), getRequest.endDateTime(), member)); } + /** + * 데일리 루틴 등록 + * + * @param request 데일리 루틴 등록 요청 + * @param member 유저 Entity + * @return 등록한 데일리 루틴 ID 목록 응답 + */ @PostMapping public ApiResponse> postDailyRoutine(@RequestBody @Valid final PostRequest request, @WithLoginUserEntity MemberEntity member) { final List routineTimes = TimeSplitter.split( @@ -43,6 +63,14 @@ public ApiResponse> postDailyRoutine(@RequestBody @Valid final PostRe return ApiResponse.ok(dailyRoutineIds); } + /** + * 데일리 루틴 수정 + * + * @param timeBlockId 수정할 데일리 루틴 ID + * @param request 데일리 루틴 수정 요청 + * @param member 유저 Entity + * @return 수정한 데일리 루틴 ID 응답 + */ @PutMapping("/{timeBlockId}") public ApiResponse putDailyRoutine(@PathVariable final Long timeBlockId, @RequestBody @Valid final PostRequest request, @WithLoginUserEntity MemberEntity member) { final RoutineTime routineTime = RoutineTime.of( @@ -56,6 +84,13 @@ public ApiResponse putDailyRoutine(@PathVariable final Long timeBlockId, @ return ApiResponse.ok(updatedId); } + /** + * 데일리 루틴 삭제 + * + * @param timeBlockId 삭제할 데일리 루틴 ID + * @param member 유저 Entity + * @return 삭제한 데일리 루틴 ID 응답 + */ @DeleteMapping("/{timeBlockId}") public ApiResponse deleteDailyRoutine(@PathVariable final Long timeBlockId, @WithLoginUserEntity MemberEntity member) { final Long deletedId = dailyRoutineService.deleteDailyRoutine(timeBlockId, member); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/GetRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/GetRequest.java index 262f50f8..12920c6e 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/GetRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/GetRequest.java @@ -4,6 +4,12 @@ import java.time.LocalDateTime; +/** + * 데일리 루틴 조회 요청 + * + * @param startDateTime 조회 시작 시간 + * @param endDateTime 조회 종료 시간 + */ public record GetRequest( @NotNull(message = "데일리 루틴의 시작 시간은 null이 될 수 없습니다.") LocalDateTime startDateTime, diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/PostRequest.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/PostRequest.java index e87f49c4..98635d33 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/PostRequest.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/controller/request/PostRequest.java @@ -5,6 +5,14 @@ import jakarta.validation.constraints.Size; import lombok.Builder; +/** + * 데일리 루틴 등록 요청 + * + * @param startDateTime 루틴 시작 시간 + * @param endDateTime 루틴 종료 시간 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + */ @Builder public record PostRequest( @NotBlank(message = "데일리 루틴의 시작 시간은 비어있을 수 없습니다.") diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/DailyRoutine.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/DailyRoutine.java index 5c484acf..25372e71 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/DailyRoutine.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/DailyRoutine.java @@ -6,10 +6,24 @@ import java.time.LocalDateTime; +/** + * 데일리 루틴 Domain + */ @Getter public class DailyRoutine { + /** + * 루틴 시간 + */ private RoutineTime routineTime; + + /** + * 루틴 타입 + */ private RoutineType routineType; + + /** + * 루틴 설명 + */ private String routineDescription; private DailyRoutine(final RoutineTime routineTime, final RoutineType routineType, final String routineDescription) { @@ -20,45 +34,105 @@ private DailyRoutine(final RoutineTime routineTime, final RoutineType routineTyp this.routineDescription = routineDescription; } + /** + * 데일리 루틴 생성 인자 유효성 검증 + * + * @param routineTime 루틴 시간 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + */ private void validateConstructor(final RoutineTime routineTime, final RoutineType routineType, final String routineDescription) { Assert.isTrue(routineTime != null, "데일리 루틴의 시간은 null이 될 수 없습니다."); Assert.isTrue(routineType != null, "데일리 루틴의 타입은 null이 될 수 없습니다."); Assert.isTrue(routineDescription != null, "데일리 루틴의 설명은 null이 될 수 없습니다."); } + /** + * 데일리 루틴 생성 + * + * @param routineTime 루틴 시간 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + * @return DailyRoutine Domain + */ public static DailyRoutine of(final RoutineTime routineTime, final String routineType, final String routineDescription) { validateConstructor(routineType); return of(routineTime, RoutineType.valueOf(routineType), routineDescription); } + /** + * 루틴 타입 유효성 검증 + * + * @param routineType 루틴 타입 + */ private static void validateConstructor(final String routineType) { Assert.hasText(routineType, "데일리 루틴의 타입은 비어있을 수 없습니다."); } + /** + * 데일리 루틴 생성 + * + * @param routineTime 루틴 시간 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + * @return DailyRoutine Domain + */ public static DailyRoutine of(final RoutineTime routineTime, final RoutineType routineType, final String routineDescription) { return new DailyRoutine(routineTime, routineType, routineDescription); } + /** + * 루틴 시작 시간 조회 + * + * @return 루틴 시작 시간 + */ public LocalDateTime getStartDateTime() { return routineTime.getStartDateTime(); } + /** + * 루틴 종료 시간 조회 + * + * @return 루틴 종료 시간 + */ public LocalDateTime getEndDateTime() { return routineTime.getEndDateTime(); } + /** + * 루틴 시작 시간 문자열 조회 + * + * @return 루틴 시작 시간 문자열 + */ public String getStartDateTimeString() { return routineTime.getStartDateTimeString(); } + /** + * 루틴 종료 시간 문자열 조회 + * + * @return 루틴 종료 시간 문자열 + */ public String getEndDateTimeString() { return routineTime.getEndDateTimeString(); } + /** + * 루틴 타입 조회 + * + * @return 루틴 타입 + */ public String getRoutineTypeDescription() { return routineType.getDescription(); } + /** + * 루틴 수정 + * + * @param routineTime 루틴 시간 + * @param routineType 루틴 타입 + * @param description 루틴 설명 + */ public void update(final RoutineTime routineTime, final RoutineType routineType, final String description) { validate(routineTime); @@ -67,6 +141,11 @@ public void update(final RoutineTime routineTime, final RoutineType routineType, this.routineDescription = description; } + /** + * 루틴 시간 유효성 검증 + * + * @param routineTime 루틴 시간 + */ private void validate(final RoutineTime routineTime) { if (this.routineTime.isNotStartAndEndDateEqualTo(routineTime)) { throw new BusinessException("동일한 날짜의 시간으로만 수정할 수 있습니다."); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineTime.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineTime.java index aa790c03..05d503a3 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineTime.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineTime.java @@ -14,12 +14,26 @@ import java.time.format.DateTimeFormatter; import java.util.Objects; +/** + * 루틴 시간 + */ @Getter @Embeddable @NoArgsConstructor(access = AccessLevel.PROTECTED) public class RoutineTime { + /** + * 하루를 초로 환산한 값 + */ private static final int SECONDS_PER_DAY = 60 * 60 * 24; + + /** + * 루틴 시작 시간 + */ private LocalDateTime startDateTime; + + /** + * 루틴 종료 시간 + */ private LocalDateTime endDateTime; @Builder(access = AccessLevel.PRIVATE) @@ -28,6 +42,13 @@ private RoutineTime(final LocalDateTime startDateTime, final LocalDateTime endDa this.endDateTime = endDateTime; } + /** + * 루틴 시간 생성 + * + * @param startDateTime 루틴 시작 시간 + * @param endDateTime 루틴 종료 시간 + * @return RoutineTime Domain + */ public static RoutineTime of(final LocalDateTime startDateTime, final LocalDateTime endDateTime) { validateConstructor(startDateTime, endDateTime); @@ -37,6 +58,12 @@ public static RoutineTime of(final LocalDateTime startDateTime, final LocalDateT .build(); } + /** + * 루틴 시간 생성 인자 유효성 검증 + * + * @param startTime 루틴 시작 시간 + * @param endTime 루틴 종료 시간 + */ private static void validateConstructor(final LocalDateTime startTime, final LocalDateTime endTime) { Assert.isTrue(startTime != null, "시작 시간은 null이 될 수 없습니다."); Assert.isTrue(endTime != null, "종료 시간은 null이 될 수 없습니다."); @@ -50,22 +77,50 @@ private static void validateConstructor(final LocalDateTime startTime, final Loc } } + /** + * 루틴 시작 시간과 종료 시간의 시간 차이가 하루를 초과하는지 확인 + * + * @param startTime 루틴 시작 시간 + * @param endTime 루틴 종료 시간 + * @return 하루를 초과하는지 여부 + */ private static boolean isDurationExceedingOneDay(final LocalDateTime startTime, final LocalDateTime endTime) { return Duration.between(startTime, endTime).toSeconds() > SECONDS_PER_DAY; } + /** + * 시작 날짜와 종료 날짜가 같은지 확인 + * + * @return 시작 날짜과 종료 날짜가 같은지 여부 + */ public boolean isSameDate() { return getStartDate().isEqual(getEndDate()); } + /** + * 시작 날짜 조회 + * + * @return 시작 날짜 + */ public LocalDate getStartDate() { return startDateTime.toLocalDate(); } + /** + * 종료 날짜 조회 + * + * @return 종료 날짜 + */ public LocalDate getEndDate() { return endDateTime.toLocalDate(); } + /** + * equals 메서드 재정의 + * + * @param o 비교 대상 + * @return 비교 결과 + */ @Override public boolean equals(final Object o) { if (this == o) return true; @@ -74,11 +129,21 @@ public boolean equals(final Object o) { return Objects.equals(getStartDateTime(), that.getStartDateTime()) && Objects.equals(getEndDateTime(), that.getEndDateTime()); } + /** + * hashCode 메서드 재정의 + * + * @return 해시 코드 + */ @Override public int hashCode() { return Objects.hash(getStartDateTime(), getEndDateTime()); } + /** + * toString 메서드 재정의 + * + * @return 문자열 + */ @Override public String toString() { return "RoutineTime{" + @@ -87,28 +152,61 @@ public String toString() { '}'; } + /** + * 시작 시간 문자열 조회 + * + * @return 시작 시간 문자열 + */ public String getStartDateTimeString() { return startDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); } + /** + * 종료 시간 문자열 조회 + * + * @return 종료 시간 문자열 + */ public String getEndDateTimeString() { return endDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); } + /** + * 루틴 시간을 비교 + * + * @param routineTime 비교할 루틴 시간 + * @return 두 루틴 시간이 다른지 여부 + */ public boolean isNotStartAndEndDateEqualTo(final RoutineTime routineTime) { return !isStartAndEndDateEqualTo(routineTime); } + /** + * 시작 시간과 종료 시간이 같은지 확인 + * + * @param routineTime 비교할 루틴 시간 + * @return 시작 시간과 종료 시간이 같은지 여부 + */ public boolean isStartAndEndDateEqualTo(final RoutineTime routineTime) { return isSameStartDate(routineTime.getStartDate()) && isSameEndDate(routineTime.getEndDate()); } + /** + * 시작 날짜가 같은지 확인 + * + * @param startDate 비교할 시작 날짜 + * @return 시작 날짜가 같은지 여부 + */ private boolean isSameStartDate(final LocalDate startDate) { return getStartDate().isEqual(startDate); } + /** + * 종료 날짜가 같은지 확인 + * + * @param endDate 비교할 종료 날짜 + * @return 종료 날짜가 같은지 여부 + */ private boolean isSameEndDate(final LocalDate endDate) { return getEndDate().isEqual(endDate); } - } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineType.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineType.java index ce8207b8..2eb1badc 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineType.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/RoutineType.java @@ -2,6 +2,9 @@ import lombok.Getter; +/** + * 루틴 타입 + */ @Getter public enum RoutineType { STUDY("공부"), @@ -12,6 +15,9 @@ public enum RoutineType { MEAL("식사"), GAME("게임"); + /** + * 루틴 타입 설명 + */ private String description; RoutineType(final String description) { diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/TimeSplitter.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/TimeSplitter.java index 6685e476..edb948a4 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/TimeSplitter.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/domain/TimeSplitter.java @@ -6,8 +6,20 @@ import java.time.LocalTime; import java.util.List; +/** + * 루틴 시간 분할 + */ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public class TimeSplitter { + /** + * 루틴 시간 분할 + *

+ * - 루틴 시작 날짜와 종료 날짜가 같으면, 루틴 시간을 그대로 반환 + * - 루틴 시작 날짜와 종료 날짜가 다르면, 루틴 시간을 시작 날짜와 종료 날짜로 분할하여 반환 (Entity 관리의 편의성을 위해) + * + * @param routineTime 루틴 시간 + * @return 분할된 루틴 시간 + */ public static List split(final RoutineTime routineTime) { if (routineTime.isSameDate()) { return List.of(routineTime); @@ -19,6 +31,12 @@ public static List split(final RoutineTime routineTime) { ); } + /** + * 날짜가 다른 경우, 시작 시간은 그대로 사용하고 종료 시간은 시작 날짜의 마지막 시간으로 변경 + * + * @param routineTime 루틴 시간 + * @return 날짜를 분할한 루틴 시간 + */ private static RoutineTime makeAnotherStartDateTime(final RoutineTime routineTime) { final LocalDateTime originalStartTime = routineTime.getStartDateTime(); final LocalDateTime dayEndDateTime = LocalDateTime.of(routineTime.getStartDate(), LocalTime.of(23, 59, 59)); @@ -26,6 +44,12 @@ private static RoutineTime makeAnotherStartDateTime(final RoutineTime routineTim return RoutineTime.of(originalStartTime, dayEndDateTime); } + /** + * 날짜가 다른 경우, 종료 시간은 그대로 사용하고 시작 시간은 종료 날짜의 시작 시간으로 변경 + * + * @param routineTime 루틴 시간 + * @return 날짜를 분할한 루틴 시간 + */ private static RoutineTime makeAnotherEndDateTime(final RoutineTime routineTime) { final LocalDateTime dayStart = LocalDateTime.of(routineTime.getEndDate(), LocalTime.of(0, 0)); final LocalDateTime originalEndTime = routineTime.getEndDateTime(); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/entity/DailyRoutineEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/entity/DailyRoutineEntity.java index f84c447d..cffbbb6d 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/entity/DailyRoutineEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/entity/DailyRoutineEntity.java @@ -11,18 +11,40 @@ import java.util.List; +/** + * 데일리 루틴 Entity + */ @Getter @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Entity public class DailyRoutineEntity extends BaseEntity { + /** + * 데일리 루틴 ID + */ @Id @GeneratedValue(strategy = jakarta.persistence.GenerationType.IDENTITY) private Long id; + + /** + * 루틴 시간 + */ @Embedded private RoutineTime routineTime; + + /** + * 루틴 타입 + */ @Enumerated(EnumType.STRING) private RoutineType routineType; + + /** + * 루틴 설명 + */ private String routineDescription; + + /** + * 멤버 ID + */ @Column(nullable = false) private Long memberId; @@ -33,6 +55,13 @@ private DailyRoutineEntity(final RoutineTime routineTime, final RoutineType rout this.memberId = memberId; } + /** + * 데일리 루틴 생성 + * + * @param dailyRoutine 데일리 루틴 Domain + * @param memberId 멤버 ID + * @return 데일리 루틴 Entity + */ public static DailyRoutineEntity of(final DailyRoutine dailyRoutine, final Long memberId) { Assert.isTrue(dailyRoutine != null, "데일리 루틴은 null이 될 수 없습니다."); Assert.isTrue(memberId != null && memberId > 0, "멤버 아이디는 null이 될 수 없고 0보다 커야 합니다."); @@ -45,6 +74,13 @@ public static DailyRoutineEntity of(final DailyRoutine dailyRoutine, final Long ); } + /** + * 데일리 루틴 목록 생성 + * + * @param dailyRoutines 데일리 루틴 Domain 목록 + * @param memberId 멤버 ID + * @return 데일리 루틴 Entity 목록 + */ public static List of(final List dailyRoutines, final Long memberId) { Assert.isTrue(dailyRoutines != null, "데일리 루틴은 null이 될 수 없습니다."); @@ -53,6 +89,11 @@ public static List of(final List dailyRoutines .toList(); } + /** + * 데일리 루틴 Entity를 Domain으로 변환 + * + * @return 데일리 루틴 Domain + */ public DailyRoutine toDomain() { return DailyRoutine.of( routineTime, @@ -61,12 +102,23 @@ public DailyRoutine toDomain() { ); } + /** + * 데일리 루틴 수정 + * + * @param dailyRoutine 데일리 루틴 Domain + */ public void update(final DailyRoutine dailyRoutine) { this.routineTime = dailyRoutine.getRoutineTime(); this.routineType = dailyRoutine.getRoutineType(); this.routineDescription = dailyRoutine.getRoutineDescription(); } + /** + * 해당 멤버가 데일리 루틴의 소유자인지 확인 + * + * @param memberId 멤버 ID + * @return 데일리 루틴 소유자 여부 + */ public boolean isNotOwner(final Long memberId) { return !this.memberId.equals(memberId); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/repository/DailyRoutineRepository.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/repository/DailyRoutineRepository.java index 24f0ff37..9e599a39 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/repository/DailyRoutineRepository.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/repository/DailyRoutineRepository.java @@ -7,7 +7,18 @@ import java.time.LocalDateTime; import java.util.List; +/** + * 데일리 루틴 Entity Repository + */ public interface DailyRoutineRepository extends JpaRepository { + /** + * 데일리 루틴 Entity를 시간으로 조회 + * + * @param startDateTime 조회 시작 시간 + * @param endDateTime 조회 종료 시간 + * @param memberId 멤버 ID + * @return 데일리 루틴 Entity List + */ @Query("select d from DailyRoutineEntity d where :startDateTime <= d.routineTime.startDateTime and d.routineTime.endDateTime <= :endDateTime and d.memberId = :memberId") List findAllByDateTimeBetween(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final Long memberId); } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/DailyRoutineService.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/DailyRoutineService.java index 1ddf2327..bf636fa4 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/DailyRoutineService.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/DailyRoutineService.java @@ -19,12 +19,27 @@ import java.util.Map; import java.util.stream.Collectors; +/** + * 데일리 루틴 Service + */ @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class DailyRoutineService { + /** + * 데일리 루틴 Entity Repository + */ private final DailyRoutineRepository dailyRoutineRepository; + /** + * 데일리 루틴 등록 + * + * @param routineTimes 루틴 시간 목록 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + * @param member 유저 Entity + * @return 등록한 데일리 루틴 ID 목록 + */ @Transactional public List postDailyRoutine(final List routineTimes, final RoutineType routineType, final String routineDescription, final MemberEntity member) { final List dailyRoutines = convertDailyRoutines(routineTimes, routineType, routineDescription); @@ -32,18 +47,40 @@ public List postDailyRoutine(final List routineTimes, final R return extractIds(dailyRoutineRepository.saveAll(DailyRoutineEntity.of(dailyRoutines, member.getId()))); } + /** + * 루틴 시간 목록을 DailyRoutine 목록으로 변환 + * + * @param routineTimes 루틴 시간 목록 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + * @return DailyRoutine 목록 + */ private List convertDailyRoutines(final List routineTimes, final RoutineType routineType, final String routineDescription) { return routineTimes.stream() .map(routineTime -> DailyRoutine.of(routineTime, routineType, routineDescription)) .toList(); } + /** + * DailyRoutine 목록에서 ID 목록 추출 + * + * @param savedDailyRoutines 저장된 DailyRoutine 목록 + * @return DailyRoutine ID 목록 + */ private List extractIds(final List savedDailyRoutines) { return savedDailyRoutines.stream() .map(DailyRoutineEntity::getId) .toList(); } + /** + * 데일리 루틴 조회 + * + * @param startDateTime 조회 시작 시간 + * @param endDateTime 조회 종료 시간 + * @param member 유저 Entity + * @return 데일리 루틴 목록 + */ public List getDailyRoutine(final LocalDateTime startDateTime, final LocalDateTime endDateTime, final MemberEntity member) { final List dailyRoutineEntities = dailyRoutineRepository.findAllByDateTimeBetween(startDateTime, endDateTime, member.getId()); final Map dailyRoutines = convertMapWithKeyAndDailyRoutine(dailyRoutineEntities); @@ -51,11 +88,23 @@ public List getDailyRoutine(final LocalDateTime startDateT return convertDailyRoutineResponses(dailyRoutines); } + /** + * DailyRoutineEntity 목록을 Key와 DailyRoutine의 Map으로 변환 + * + * @param dailyRoutineEntities DailyRoutineEntity 목록 + * @return Key와 DailyRoutine의 Map + */ private Map convertMapWithKeyAndDailyRoutine(final List dailyRoutineEntities) { return dailyRoutineEntities.stream() .collect(Collectors.toMap(DailyRoutineEntity::getId, DailyRoutineEntity::toDomain)); } + /** + * Key와 DailyRoutine의 Map을 DailyRoutineResponse 목록으로 변환 + * + * @param dailyRoutines Key와 DailyRoutine의 Map + * @return DailyRoutineResponse 목록 + */ private List convertDailyRoutineResponses(final Map dailyRoutines) { return dailyRoutines.entrySet().stream() .map(entry -> DailyRoutineResponse.of(entry.getKey(), entry.getValue())) @@ -63,6 +112,16 @@ private List convertDailyRoutineResponses(final Map new EntityNotFoundException("해당하는 ID의 DailyRoutine이 존재하지 않습니다.")); @@ -84,7 +150,14 @@ private DailyRoutineEntity getDailyRoutineEntity(final Long timeBlockId, final M return dailyRoutineEntity; } - + + /** + * 데일리 루틴 삭제 + * + * @param timeBlockId 삭제할 데일리 루틴 ID + * @param member 유저 Entity + * @return 삭제한 데일리 루틴 ID + */ @Transactional public Long deleteDailyRoutine(final Long timeBlockId, final MemberEntity member) { final DailyRoutineEntity dailyRoutineEntity = getDailyRoutineEntity(timeBlockId, member); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/response/DailyRoutineResponse.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/response/DailyRoutineResponse.java index f9dbbfbb..fd0051dd 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/response/DailyRoutineResponse.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/dailyroutine/service/response/DailyRoutineResponse.java @@ -3,6 +3,15 @@ import lombok.Builder; import org.hyunggi.mygardenbe.dailyroutine.domain.DailyRoutine; +/** + * 데일리 루틴 조회 응답 + * + * @param id 루틴 ID + * @param startDateTime 루틴 시작 시간 + * @param endDateTime 루틴 종료 시간 + * @param routineType 루틴 타입 + * @param routineDescription 루틴 설명 + */ @Builder public record DailyRoutineResponse( Long id, @@ -10,6 +19,13 @@ public record DailyRoutineResponse( String endDateTime, String routineType, String routineDescription) { + + /** + * 데일리 루틴 Domain -> 데일리 루틴 조회 응답 변환 + * + * @param dailyRoutine 데일리 루틴 Domain + * @return 데일리 루틴 조회 응답 + */ public static DailyRoutineResponse of(DailyRoutine dailyRoutine) { return DailyRoutineResponse.builder() .startDateTime(dailyRoutine.getStartDateTimeString()) @@ -19,6 +35,13 @@ public static DailyRoutineResponse of(DailyRoutine dailyRoutine) { .build(); } + /** + * 데일리 루틴 Domain -> 데일리 루틴 조회 응답 변환 (ID 포함) + * + * @param id 루틴 ID + * @param dailyRoutine 데일리 루틴 Domain + * @return 데일리 루틴 조회 응답 + */ public static DailyRoutineResponse of(final Long id, final DailyRoutine dailyRoutine) { return DailyRoutineResponse.builder() .id(id) From 54838c8609209a2df273703a74a880d21e643a8d Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 17:03:44 +0900 Subject: [PATCH 09/12] =?UTF-8?q?chore:=20member=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=9E=88=EB=8A=94=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mygardenbe/member/domain/Member.java | 40 +++++++++++ .../mygardenbe/member/domain/Permission.java | 6 ++ .../mygardenbe/member/domain/Role.java | 18 +++++ .../member/entity/MemberEntity.java | 69 +++++++++++++++++++ .../member/repository/MemberRepository.java | 9 +++ 5 files changed, 142 insertions(+) diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Member.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Member.java index 63684ad4..de99f52e 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Member.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Member.java @@ -3,11 +3,29 @@ import lombok.Getter; import org.hyunggi.mygardenbe.common.exception.BusinessException; +/** + * 유저 Domain + */ @Getter public class Member { + /** + * 유저 이메일 + */ private final String email; + + /** + * 유저 비밀번호 + */ private final String password; + + /** + * 유저 권한 + */ private final Role role; + + /** + * 유저 활성화 여부 + */ private final boolean enabled; public Member(final String email, final String password) { @@ -23,12 +41,24 @@ public Member(final String email, final String password, final Role role, final this.enabled = enabled; } + /** + * 유저 생성 인자 유효성 검증 + * + * @param email 이메일 + * @param password 비밀번호 + * @param role 권한 + */ private void validateConstructor(final String email, final String password, final Role role) { validateEmail(email); validatePassword(password); validateRole(role); } + /** + * 이메일 유효성 검증 + * + * @param email 이메일 + */ private void validateEmail(final String email) { if (email == null || email.isEmpty()) { throw new BusinessException("이메일은 null이거나 비어있을 수 없습니다."); @@ -44,6 +74,11 @@ private void validateEmail(final String email) { } } + /** + * 비밀번호 유효성 검증 + * + * @param password 비밀번호 + */ private void validatePassword(final String password) { if (password == null || password.isEmpty()) { throw new BusinessException("비밀번호는 null이거나 비어있을 수 없습니다."); @@ -62,6 +97,11 @@ private void validatePassword(final String password) { } } + /** + * 권한 유효성 검증 + * + * @param role 권한 + */ private void validateRole(final Role role) { if (role == null) { throw new BusinessException("Role은 null이 될 수 없습니다."); diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Permission.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Permission.java index 2734e327..07d2886f 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Permission.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Permission.java @@ -3,6 +3,9 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +/** + * 유저의 구체적인 허용 권한 + */ @Getter @RequiredArgsConstructor public enum Permission { @@ -12,5 +15,8 @@ public enum Permission { ADMIN_CREATE("ADMIN:CREATE"), ADMIN_DELETE("ADMIN:DELETE"); + /** + * 권한 문자열 + */ private final String permissionString; } diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Role.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Role.java index 669906b4..cb655649 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Role.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/domain/Role.java @@ -9,6 +9,9 @@ import java.util.Set; import java.util.stream.Collectors; +/** + * 유저 권한 + */ @Getter @RequiredArgsConstructor public enum Role { @@ -24,8 +27,18 @@ public enum Role { ), ACTUATOR(Collections.emptySet()); + /** + * 권한이 가지고 있는 구체적인 허용 권한 목록 + */ private final Set permissions; + /** + * 권한 목록을 SimpleGrantedAuthority 목록으로 변환 + *

+ * - 권한 목록에는 권한 이름과 권한이 가지고 있는 구체적인 허용 권한 목록이 포함됨 + * + * @return SimpleGrantedAuthority 목록 + */ public List getAuthorities() { List authorities = extractAuthorityFromPermissions(); @@ -34,6 +47,11 @@ public List getAuthorities() { return authorities; } + /** + * 권한이 가지고 있는 구체적인 허용 권한 목록을 SimpleGrantedAuthority 목록으로 변환 + * + * @return SimpleGrantedAuthority 목록 + */ private List extractAuthorityFromPermissions() { return getPermissions() .stream() diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/entity/MemberEntity.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/entity/MemberEntity.java index ae96b1e0..a4e79fcc 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/entity/MemberEntity.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/entity/MemberEntity.java @@ -13,22 +13,43 @@ import java.util.Collection; +/** + * 유저 Entity + */ @Getter @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Entity public class MemberEntity extends BaseEntity implements UserDetails { + /** + * 유저 ID + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + /** + * 유저 이메일 + */ @Column(length = 50, nullable = false, unique = true) private String email; + + /** + * 유저 비밀번호 + */ @Column(length = 100, nullable = false) private String password; + + /** + * 유저 권한 + */ @Getter @Enumerated(EnumType.STRING) @Column(length = 10, nullable = false) private Role role; + /** + * 유저 활성화 여부 + */ private boolean enabled; @Builder(access = lombok.AccessLevel.PRIVATE) @@ -39,6 +60,13 @@ private MemberEntity(final String email, final String password, final Role role, this.enabled = enabled; } + /** + * 유저 Entity 생성 + * + * @param member 유저 Domain + * @param passwordEncoder 비밀번호 암호화에 사용할 PasswordEncoder + * @return 유저 Entity + */ public static MemberEntity of(final Member member, final PasswordEncoder passwordEncoder) { validate(member, passwordEncoder); @@ -50,6 +78,12 @@ public static MemberEntity of(final Member member, final PasswordEncoder passwor .build(); } + /** + * 유저 Entity 생성 인자 유효성 검증 + * + * @param member 유저 Domain + * @param passwordEncoder 비밀번호 암호화에 사용할 PasswordEncoder + */ private static void validate(final Member member, final PasswordEncoder passwordEncoder) { if (member == null) { throw new IllegalArgumentException("Member는 null이 될 수 없습니다."); @@ -60,36 +94,71 @@ private static void validate(final Member member, final PasswordEncoder password } } + /** + * 해당 유저 Entity의 권한 목록 반환 + * + * @return 권한 목록 + */ @Override public Collection getAuthorities() { return this.role.getAuthorities(); } + /** + * 해당 유저 Entity의 비밀번호 반환 + * + * @return 비밀번호 + */ @Override public String getPassword() { return this.password; } + /** + * 해당 유저 Entity의 이메일 반환 + * + * @return 이메일 + */ @Override public String getUsername() { return this.email; } + /** + * 해당 유저 Entity의 계정 만료 여부 반환 + * + * @return 계정 만료 여부 + */ @Override public boolean isAccountNonExpired() { return true; } + /** + * 해당 유저 Entity의 계정 잠금 여부 반환 + * + * @return 계정 잠금 여부 + */ @Override public boolean isAccountNonLocked() { return true; } + /** + * 해당 유저 Entity의 비밀번호 만료 여부 반환 + * + * @return 비밀번호 만료 여부 + */ @Override public boolean isCredentialsNonExpired() { return true; } + /** + * 해당 유저 Entity의 활성화 여부 반환 + * + * @return 활성화 여부 + */ @Override public boolean isEnabled() { return this.enabled; diff --git a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/repository/MemberRepository.java b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/repository/MemberRepository.java index bcf96069..a0503ffb 100644 --- a/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/repository/MemberRepository.java +++ b/my-garden-be/src/main/java/org/hyunggi/mygardenbe/member/repository/MemberRepository.java @@ -5,6 +5,15 @@ import java.util.Optional; +/** + * 유저 Entity Repository + */ public interface MemberRepository extends JpaRepository { + /** + * 유저 Entity를 이메일로 조회 + * + * @param email 이메일 + * @return 유저 Entity + */ Optional findByEmail(final String email); } From 590d31fb8cc8bcd82ecbd28b909ecf4413c8a4fa Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 17:03:57 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20application.yaml=EC=97=90=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- my-garden-be/src/main/resources/application.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/my-garden-be/src/main/resources/application.yaml b/my-garden-be/src/main/resources/application.yaml index 700d3467..296afe48 100644 --- a/my-garden-be/src/main/resources/application.yaml +++ b/my-garden-be/src/main/resources/application.yaml @@ -1,8 +1,10 @@ +# default profile을 local로 설정 spring: profiles: default: local --- +# local profile 설정 spring: config: @@ -50,6 +52,8 @@ management: port: 9089 # Actuator 엔드포인트가 서비스될 서버의 포트 --- +# prod profile 설정 + application: security: jwt: @@ -58,6 +62,7 @@ application: refresh-token: expiration: ${jwt.refresh-expiration} +# AWS Parameter Store 설정 config: type: aws-parameterstore:/config/myGarden/ @@ -76,6 +81,7 @@ spring: username: ${jdbc.username} password: ${jdbc.password} +# SSL 설정 server: port: 443 ssl: @@ -86,11 +92,13 @@ server: mbeanregistry: enabled: true +# Actuator 설정 actuator: url: ${actuator-param.url} port: ${actuator-param.port} email: ${actuator-param.email} +# Actuator 엔드포인트를 외부에 노출 management: endpoints: web: @@ -100,6 +108,7 @@ management: server: port: ${actuator.port} +# Slack 설정 my-garden: slack: incoming-webhook-token: ${slack-incoming-webhook.url} From b39af578bfed3465e859d7516dc4076cc34959fa Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 18:38:09 +0900 Subject: [PATCH 11/12] =?UTF-8?q?chore:=20vue=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/auth/login/api/api.js | 9 ++- .../src/components/auth/logout/api/api.js | 7 +++ .../src/components/auth/signup/api/api.js | 7 +++ .../components/boards/common/BoardWrite.vue | 9 +++ .../components/boards/common/SearchForm.vue | 3 + .../boards/common/TableContents.vue | 9 +++ .../components/boards/common/WriteButton.vue | 5 ++ .../src/components/boards/common/api/api.js | 10 +++ .../src/components/boards/common/util/util.js | 17 +++++ .../src/components/boards/learn/api/api.js | 24 +++++++ .../src/components/boards/notice/api/api.js | 29 +++++++++ .../src/components/dailyRoutine/api/api.js | 31 +++++++++- .../src/components/dailyRoutine/api/util.js | 62 ++++++++++++++++++- .../dailyRoutine/draw/DrawDailyRoutine.vue | 24 +++++++ .../dailyRoutine/draw/DrawStatisticsChart.vue | 47 +++++++++++--- .../dailyRoutine/draw/RoutineTooltip.vue | 15 +++++ .../dailyRoutine/draw/ScheduleSection.vue | 50 +++++++++++++++ 17 files changed, 347 insertions(+), 11 deletions(-) diff --git a/my-garden-fe/src/components/auth/login/api/api.js b/my-garden-fe/src/components/auth/login/api/api.js index da8139ee..48e58583 100644 --- a/my-garden-fe/src/components/auth/login/api/api.js +++ b/my-garden-fe/src/components/auth/login/api/api.js @@ -1,6 +1,13 @@ import axios from "axios"; import {store} from "@/scripts/store.js"; +/** + * Login API + * + * @param email 이메일 + * @param password 비밀번호 + * @returns {Promise} 성공 시 'success', 실패 시 null + */ export function loginApi(email, password) { return axios.post('/api/auth/login', { email: email, @@ -11,7 +18,7 @@ export function loginApi(email, password) { store.commit('setToken', data); sessionStorage.setItem('token', JSON.stringify(data)); - + return 'success'; }) .catch(error => { diff --git a/my-garden-fe/src/components/auth/logout/api/api.js b/my-garden-fe/src/components/auth/logout/api/api.js index 5d34a210..700bd833 100644 --- a/my-garden-fe/src/components/auth/logout/api/api.js +++ b/my-garden-fe/src/components/auth/logout/api/api.js @@ -1,6 +1,13 @@ import axios from "axios"; import {store} from "@/scripts/store.js"; +/** + * Logout API + * + * @param email 이메일 + * @param password 비밀번호 + * @returns {Promise} 성공 시 'success', 실패 시 null + */ export function logoutApi(email, password) { return axios.post('/api/auth/logout',) .then(res => { diff --git a/my-garden-fe/src/components/auth/signup/api/api.js b/my-garden-fe/src/components/auth/signup/api/api.js index ab00ea9d..3a9550bf 100644 --- a/my-garden-fe/src/components/auth/signup/api/api.js +++ b/my-garden-fe/src/components/auth/signup/api/api.js @@ -1,5 +1,12 @@ import axios from "axios"; +/** + * Signup API + * + * @param email 이메일 + * @param password 비밀번호 + * @returns {Promise} 성공 시 '회원가입에 성공했습니다.', 실패 시 '회원가입에 실패했습니다.\n' + 에러메시지 + */ export function signupApi(email, password) { return axios.post('/api/auth/signup', { email: email, diff --git a/my-garden-fe/src/components/boards/common/BoardWrite.vue b/my-garden-fe/src/components/boards/common/BoardWrite.vue index 74663713..3b6a6241 100644 --- a/my-garden-fe/src/components/boards/common/BoardWrite.vue +++ b/my-garden-fe/src/components/boards/common/BoardWrite.vue @@ -35,12 +35,21 @@ const props = defineProps({ const emit = defineEmits(["saveBoard", "goToBackPage"]); +/** + * 게시글 내용 + */ const content = ref(''); +/** + * 이전 페이지로 이동 + */ function goToBackPage() { emit('goToBackPage', props.boardRouteName, props.boardId) } +/** + * 게시글 저장 + */ function save() { const category = document.getElementById("category").value; const title = document.getElementById("board_writer").value; diff --git a/my-garden-fe/src/components/boards/common/SearchForm.vue b/my-garden-fe/src/components/boards/common/SearchForm.vue index 8ebb812a..23565cf8 100644 --- a/my-garden-fe/src/components/boards/common/SearchForm.vue +++ b/my-garden-fe/src/components/boards/common/SearchForm.vue @@ -15,6 +15,9 @@ const props = defineProps({ const emits = defineEmits(["search"]); +/** + * 검색에 필요한 데이터 + */ const startDate = ref(props.queryParameter.startDate); const endDate = ref(props.queryParameter.endDate); const category = ref(props.queryParameter.category); diff --git a/my-garden-fe/src/components/boards/common/TableContents.vue b/my-garden-fe/src/components/boards/common/TableContents.vue index 21465ef6..923051a9 100644 --- a/my-garden-fe/src/components/boards/common/TableContents.vue +++ b/my-garden-fe/src/components/boards/common/TableContents.vue @@ -20,8 +20,17 @@ const props = defineProps({ const emit = defineEmits(['goToBoardView',]); +/** + * 페이지 번호 오프셋 + */ const pageNumberOffset = ref(0); +/** + * 7일 이내 작성된 글인지 확인 + * + * @param writtenAt 작성일시 + * @returns {boolean} 7일 이내 작성된 글이면 true, 아니면 false + */ function isWrittenIn7days(writtenAt) { const diffTime = Math.abs(new Date() - new Date(writtenAt)); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); diff --git a/my-garden-fe/src/components/boards/common/WriteButton.vue b/my-garden-fe/src/components/boards/common/WriteButton.vue index e4cd88dd..948368a2 100644 --- a/my-garden-fe/src/components/boards/common/WriteButton.vue +++ b/my-garden-fe/src/components/boards/common/WriteButton.vue @@ -16,6 +16,11 @@ const props = defineProps({ } }); +/** + * 페이지 이동 + * + * @param pageName 페이지 이름 + */ function goToPage(pageName) { router.push({ name: pageName, diff --git a/my-garden-fe/src/components/boards/common/api/api.js b/my-garden-fe/src/components/boards/common/api/api.js index 8a5d8648..185e0be6 100644 --- a/my-garden-fe/src/components/boards/common/api/api.js +++ b/my-garden-fe/src/components/boards/common/api/api.js @@ -1,6 +1,16 @@ import axios from "axios"; import {router} from "@/scripts/router.js"; +/** + * 게시글 등록 & 수정 API + *

+ * - 게시글 ID가 있으면 수정, 없으면 등록 + * - 등록 및 수정 성공 시 게시판 목록으로 이동 + * + * @param boardType 게시판 타입 + * @param board 등록할 게시글 정보 + * @param boardId 게시글 ID + */ export function postBoardApi(boardType, board, boardId) { if (boardId) { return axios.put(`/api/boards/${boardType}/${boardId}`, board) diff --git a/my-garden-fe/src/components/boards/common/util/util.js b/my-garden-fe/src/components/boards/common/util/util.js index 620d382b..a389dc68 100644 --- a/my-garden-fe/src/components/boards/common/util/util.js +++ b/my-garden-fe/src/components/boards/common/util/util.js @@ -1,10 +1,22 @@ import {store} from "@/scripts/store.js"; +/** + * 분류 목록에서 분류 코드에 해당하는 분류명을 반환 + * + * @param categories 분류 목록 + * @param categoryCode 분류 코드 + * @returns {string} 분류명 + */ export function convertCategoryCodeToText(categories, categoryCode) { const category = categories.find(category => category.code === categoryCode); return category?.text; } +/** + * 사용자 계정인지 확인 + * + * @returns {boolean} 사용자 계정이면 true, 아니면 false + */ export function isUserAccount() { const roles = store.getters.getRoles; @@ -12,6 +24,11 @@ export function isUserAccount() { || roles.includes("ROLE_ADMIN"); } +/** + * 관리자 계정인지 확인 + * + * @returns {boolean} 관리자 계정이면 true, 아니면 false + */ export function isAdminAccount() { const roles = store.getters.getRoles; diff --git a/my-garden-fe/src/components/boards/learn/api/api.js b/my-garden-fe/src/components/boards/learn/api/api.js index f43192a4..51e39c56 100644 --- a/my-garden-fe/src/components/boards/learn/api/api.js +++ b/my-garden-fe/src/components/boards/learn/api/api.js @@ -1,5 +1,11 @@ import axios from "axios"; +/** + * TIL 목록 조회 API + * + * @param parameters 조회 파라미터 + * @returns {Promise | void>} 성공 시 TIL 목록, 실패 시 alert + */ export function getLearnBoardListApi(parameters) { let queryParameter = ''; if (parameters) { @@ -17,6 +23,12 @@ export function getLearnBoardListApi(parameters) { }); } +/** + * TIL 분류 목록 조회 API + * + * @param boardType 게시판 타입 + * @returns {Promise | void>} 성공 시 TIL 분류 목록, 실패 시 alert + */ export function getLearnBoardCategoryApi(boardType) { return axios.get(`/api/boards/categories?boardType=${boardType}`) .then(({data}) => { @@ -27,6 +39,12 @@ export function getLearnBoardCategoryApi(boardType) { }); } +/** + * TIL 상세 조회 API + * + * @param boardId 게시글 ID + * @returns {Promise | void>} 성공 시 TIL 상세 내용, 실패 시 alert + */ export function getLearnBoardViewApi(boardId) { return axios.get(`/api/boards/learn/${boardId}`) .then(({data}) => { @@ -37,6 +55,12 @@ export function getLearnBoardViewApi(boardId) { }); } +/** + * TIL 삭제 API + * + * @param boardId 게시글 ID + * @returns {Promise | void>} 성공 시 '삭제한 게시글 ID', 실패 시 alert + */ export function deleteLearnBoardApi(boardId) { return axios.delete(`/api/boards/learn/${boardId}`) .then(({data}) => { diff --git a/my-garden-fe/src/components/boards/notice/api/api.js b/my-garden-fe/src/components/boards/notice/api/api.js index a16d3a94..39bcf949 100644 --- a/my-garden-fe/src/components/boards/notice/api/api.js +++ b/my-garden-fe/src/components/boards/notice/api/api.js @@ -1,5 +1,10 @@ import axios from "axios"; +/** + * 공지사항 중요 목록 조회 API + * + * @returns {Promise | void>} 성공 시 중요 공지사항 목록, 실패 시 alert + */ export function getNoticeImportantBoardListApi() { return axios.get('/api/boards/notice/important') .then(({data}) => { @@ -10,6 +15,12 @@ export function getNoticeImportantBoardListApi() { }); } +/** + * 공지사항 목록 조회 API + * + * @param parameters 조회 조건 + * @returns {Promise | void>} 성공 시 공지사항 목록, 실패 시 alert + */ export function getNoticeBoardListApi(parameters) { let queryParameter = ''; if (parameters) { @@ -27,6 +38,12 @@ export function getNoticeBoardListApi(parameters) { }); } +/** + * 공지사항 분류 목록 조회 API + * + * @param boardType 게시판 타입 + * @returns {Promise | void>} + */ export function getNoticeBoardCategoryApi(boardType) { return axios.get(`/api/boards/categories?boardType=${boardType}`) .then(({data}) => { @@ -37,6 +54,12 @@ export function getNoticeBoardCategoryApi(boardType) { }); } +/** + * 공지사항 상세 조회 API + * + * @param boardId 게시글 ID + * @returns {Promise | void>} 성공 시 공지사항 상세 내용, 실패 시 alert + */ export function getNoticeBoardViewApi(boardId) { return axios.get(`/api/boards/notice/${boardId}`) .then(({data}) => { @@ -47,6 +70,12 @@ export function getNoticeBoardViewApi(boardId) { }); } +/** + * 공지사항 삭제 API + * + * @param boardId 게시글 ID + * @returns {Promise | void>} 성공 시 '삭제된 공지사항 ID', 실패 시 alert + */ export function deleteNoticeBoardApi(boardId) { return axios.delete(`/api/boards/notice/${boardId}`) .then(({data}) => { diff --git a/my-garden-fe/src/components/dailyRoutine/api/api.js b/my-garden-fe/src/components/dailyRoutine/api/api.js index 2e85a50d..53f20336 100644 --- a/my-garden-fe/src/components/dailyRoutine/api/api.js +++ b/my-garden-fe/src/components/dailyRoutine/api/api.js @@ -5,6 +5,13 @@ import { updateLastStartDateTime } from "@/components/dailyRoutine/api/util.js"; +/** + * 데일리 루틴 목록 조회 API + * + * @param startDateTime 조회 시작 일자 + * @param endDateTime 조회 종료 일자 + * @returns {Promise<{allDateTimeDataArray: *} | void>} 성공 시 데일리 루틴 목록, 실패 시 alert + */ export function getDailyRoutineApi(startDateTime, endDateTime) { return axios.get(`/api/daily-routine?startDateTime=${startDateTime}&endDateTime=${endDateTime}`) .then(({data}) => { @@ -24,7 +31,14 @@ export function getDailyRoutineApi(startDateTime, endDateTime) { }); } - +/** + * 데일리 루틴 등록 API + * + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @param routineType 루틴 타입 + * @param content 루틴 내용 + */ export function postDailyRoutineApi(startDate, endDate, routineType, content) { axios.post('/api/daily-routine', { startDateTime: startDate, @@ -42,6 +56,15 @@ export function postDailyRoutineApi(startDate, endDate, routineType, content) { }); } +/** + * 데일리 루틴 수정 API + * + * @param id 수정할 데일리 루틴 ID + * @param startDate 시작 일자 + * @param endDate 종료 일자 + * @param routineType 루틴 타입 + * @param content 루틴 내용 + */ export function updateDailyRoutineApi(id, startDate, endDate, routineType, content) { axios.put(`/api/daily-routine/${id}`, { startDateTime: startDate, @@ -62,6 +85,11 @@ export function updateDailyRoutineApi(id, startDate, endDate, routineType, conte }); } +/** + * 데일리 루틴 삭제 API + * + * @param id 삭제할 데일리 루틴 ID + */ export function deleteDailyRoutineApi(id) { axios.delete(`/api/daily-routine/${id}`) .then(() => { @@ -70,6 +98,5 @@ export function deleteDailyRoutineApi(id) { }) .catch(error => { alert("삭제에 실패했습니다."); - }); } diff --git a/my-garden-fe/src/components/dailyRoutine/api/util.js b/my-garden-fe/src/components/dailyRoutine/api/util.js index 6eb4398f..67cbeed7 100644 --- a/my-garden-fe/src/components/dailyRoutine/api/util.js +++ b/my-garden-fe/src/components/dailyRoutine/api/util.js @@ -1,3 +1,9 @@ +/** + * 일자를 받아서 해당 일자의 시작시간과 종료시간을 반환한다. + * + * @param targetDate 대상 일자 + * @returns {{targetStartDateTime: string, targetEndDateTime: string}} 대상 일자의 시작시간과 종료시간 + */ export function getTargetDateTimeRange(targetDate) { const targetStartDateTime = `${targetDate}T00:00:00`; const targetEndDateTime = `${targetDate}T23:59:59`; @@ -5,23 +11,44 @@ export function getTargetDateTimeRange(targetDate) { return {targetStartDateTime, targetEndDateTime}; } +/** + * 해당 하는 일자가 오늘인지 확인한다. + * + * @param targetDate 확인할 대상 일자 + * @returns {boolean} 대상 일자가 오늘인지 여부 + */ export function isToday(targetDate) { return getTodayDate() === targetDate.split('T')[0]; } +/** + * 오늘 날짜를 반환한다. + * + * @returns {string} 오늘 날짜 + */ export function getTodayDate() { return convertDateFormat(new Date()); } +/** + * 일자를 받아서 특정 포맷으로 변환한다. + * + * @param date 변환할 일자 + * @returns {string} 변환된 일자 (yyyy-MM-dd) + */ export function convertDateFormat(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; - } +/** + * 현재 시간을 기준으로 한 달 전의 일자를 특정 포맷으로 반환한다. + * + * @returns {string} 한 달 전의 일자 (yyyy-MM-dd) + */ export function getOneMonthAgoDate() { const currentDate = new Date(); @@ -34,15 +61,33 @@ export function getOneMonthAgoDate() { return `${year}-${month}-${day}`; } +/** + * 해당 시간을 분으로 변환한다. + * + * @param time 변환할 시간 + * @returns {number} 분으로 변환된 시간 + */ export function timeToMinutes(time) { const [hours, minutes] = time.split(':').map(Number); return hours * 60 + minutes; } +/** + * 해당 일자에서 시간을 추출한다. + * + * @param dateTime 추출할 일자 + * @returns {*} 추출된 시간 + */ export function extractTime(dateTime) { return dateTime.split('T')[1].slice(0, 5); } +/** + * 종료 시간이 현재 저장된 마지막 시작 시간보다 늦은지 확인하고 늦다면 마지막 시작 시간을 업데이트한다.

+ * (하루 일과에서 새로운 일과를 등록할 때, 자연스럽게 마지막에 등록된 종료 시간을 다음 일과의 시작 시간으로 사용하기 위함) + * + * @param endDate 종료 시간 + */ export function updateLastStartDateTime(endDate) { const todayLastStartDateTime = localStorage.getItem("todayLastStartDateTime"); @@ -51,6 +96,13 @@ export function updateLastStartDateTime(endDate) { } } +/** + * 마지막 시작 시간을 LocalStorage에 저장한다.

+ * (localStorage에 저장된 마지막 시작 시간이 새로 계산된 마지막 시작 시간과 다르다면, 페이지를 새로고침하여 allDateTimeData를 다시 받아오게 한다.) + * + * @param startDateTime 시작 시간 + * @param allDateTimeDataArray 모든 일과 데이터 + */ export function saveLastStartDateTimeInLocalStorage(startDateTime, allDateTimeDataArray) { const saveStartDateTime = calculateTodayLastStartDateTime(startDateTime, allDateTimeDataArray); @@ -60,6 +112,14 @@ export function saveLastStartDateTimeInLocalStorage(startDateTime, allDateTimeDa } } +/** + * 오늘의 마지막 시작 시간을 계산한다.

+ * (받아온 allDateTimeData가 비어있지 않다면, 해당 데이터의 마지막 종료 시간을 반환한다.) + * + * @param todayStartDateTime 오늘의 시작 시간 + * @param allDateTimeData 모든 일과 데이터 + * @returns {*} 오늘의 마지막 시작 시간 + */ function calculateTodayLastStartDateTime(todayStartDateTime, allDateTimeData) { let returnValue = todayStartDateTime; diff --git a/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue b/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue index 55252199..0f5a258b 100644 --- a/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue +++ b/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue @@ -5,12 +5,27 @@ import {store} from "@/scripts/store.js"; import ScheduleSection from "@/components/dailyRoutine/draw/ScheduleSection.vue"; import {extractTime, getTargetDateTimeRange, timeToMinutes} from "@/components/dailyRoutine/api/util.js"; +/** + * 정오 문자열 + */ const NOON_STRING = '12:00'; +/** + * 정오 시간을 분으로 변환 + */ const noonMin = timeToMinutes(NOON_STRING); +/** + * 오전/오후 스케줄 + */ const morningSchedule = ref([]); const afternoonSchedule = ref([]); +/** + * 루틴 데이터를 시간 블록 배열로 변환 + * + * @param schedule 루틴 데이터 + * @returns {[]} 시간 블록 배열 + */ function convertRoutineToTimeBlockArray(schedule) { const tempTimeBlockArray = []; @@ -40,6 +55,15 @@ function convertRoutineToTimeBlockArray(schedule) { return tempTimeBlockArray; } +/** + * 시간 블록 생성 + * + * @param block 루틴 데이터 + * @param startTime 시작 시간 + * @param endTime 종료 시간 + * @param partOfDay 오전/오후 + * @returns {{id: number, routineType: string, color: string, routineDescription: string, startDate: string, endDate: string, displayStartTime: string, displayEndTime: string, totalMinutes: number}} 시간 블록 + */ function createTimeBlock(block, startTime, endTime, partOfDay) { let startTimeMin = timeToMinutes(startTime); let endTimeMin = timeToMinutes(endTime); diff --git a/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue b/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue index 1fb4dedc..5efde4e1 100644 --- a/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue +++ b/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue @@ -5,10 +5,24 @@ import {Chart} from "chart.js/auto"; import {store} from "@/scripts/store.js"; import ChartDataLabels from 'chartjs-plugin-datalabels'; +/** + * 하루 일과에서 사용하는 색상 맵 + */ const colorMap = store.getters.getColors; + +/** + * 현재 보고 있는 날짜 + */ let viewDate = store.getters.getViewDate; + +/** + * 모든 루틴의 총 시간 합 + */ let allTotalMinutesSum = 0; +/** + * Chart.js의 옵션 + */ const options = { responsive: true, maintainAspectRatio: false, @@ -66,6 +80,9 @@ const options = { } }; +/** + * Chart.js의 데이터 + */ const data = { labels: [], datasets: [{ @@ -74,6 +91,9 @@ const data = { }] }; +/** + * Chart.js의 설정 + */ const config = { type: 'pie', plugins: [ChartDataLabels], @@ -81,15 +101,14 @@ const config = { options: options }; +/** + * Chart.js 객체 + */ let myChart; -onMounted(() => { - myChart = new Chart( - document.getElementById('chartCanvas'), - config - ); -}); - +/** + * Chart.js의 DataSet을 업데이트 + */ function updateDataSetFrom(statisticData) { // 기존에 존재하는 데이터를 모두 삭제 myChart.data.labels.length = 0; @@ -117,6 +136,13 @@ function updateDataSetFrom(statisticData) { myChart.options.plugins.title.text = `일과 통계 [${viewDate}]`; } +/** + * 시간 블록 배열에서 통계 데이터를 계산 + * + * @param timeBlockArray 시간 블록 배열 + * @param statisticData 통계 데이터 + * @returns {number} 총 시간 합 + */ function calculateStatisticDataFromTimeBlockArray(timeBlockArray, statisticData) { let tempSumTotalMinutes = 0; @@ -133,6 +159,13 @@ function calculateStatisticDataFromTimeBlockArray(timeBlockArray, statisticData) return tempSumTotalMinutes; } +onMounted(() => { + myChart = new Chart( + document.getElementById('chartCanvas'), + config + ); +}); + watch(() => store.getters.getTimeBlockArray, (timeBlockArray) => { const statisticData = {}; diff --git a/my-garden-fe/src/components/dailyRoutine/draw/RoutineTooltip.vue b/my-garden-fe/src/components/dailyRoutine/draw/RoutineTooltip.vue index ae98d7a7..344ded17 100644 --- a/my-garden-fe/src/components/dailyRoutine/draw/RoutineTooltip.vue +++ b/my-garden-fe/src/components/dailyRoutine/draw/RoutineTooltip.vue @@ -28,10 +28,16 @@ const props = defineProps({ }, }); +/** + * tooltip의 ref + */ const tooltipRef = ref(null); const tooltipTextRef = ref(null); const tooltipHeightOffset = ref(0); +/** + * tooltip의 class를 업데이트한다. + */ function updateTooltipClass() { return { 'tooltip-text': true, @@ -40,16 +46,25 @@ function updateTooltipClass() { }; } +/** + * tooltip의 visibility를 업데이트한다. + */ function updateVisible() { return { visibility: isTooltipVisible() ? 'visible' : 'hidden', }; } +/** + * tooltip이 보여지는지 여부를 반환한다. + */ function isTooltipVisible() { return props.timeBlockId === props.hoverTimeBlockId; } +/** + * tooltip의 위치를 계산한다. + */ function calculateTooltipOffset() { if (!tooltipRef.value) return; diff --git a/my-garden-fe/src/components/dailyRoutine/draw/ScheduleSection.vue b/my-garden-fe/src/components/dailyRoutine/draw/ScheduleSection.vue index 4fda092d..fc5964db 100644 --- a/my-garden-fe/src/components/dailyRoutine/draw/ScheduleSection.vue +++ b/my-garden-fe/src/components/dailyRoutine/draw/ScheduleSection.vue @@ -15,10 +15,24 @@ const props = defineProps({ }, }); +/** + * hover 중인 시간 블록 id + */ const hoverTimeBlockId = ref(0); + +/** + * 오전, 오후에 해당 하는 문자열 모음 + */ const morningStrings = ['오전', 'AM', 'am', 'morning']; const afternoonStrings = ['오후', 'PM', 'pm', 'afternoon']; +/** + * 시간 블록 스타일 + * + * @param block 시간 블록 + * @param partOfDay 오전/오후 + * @returns {{position: string, backgroundColor: *, top: string, height: string, border: string}} + */ function blockStyle(block, partOfDay) { const duration = calculateDuration(block); const startTop = timeToMinutes(block.displayStartTime); @@ -33,33 +47,69 @@ function blockStyle(block, partOfDay) { }; } +/** + * 시간 블록의 높이 계산 (분 단위, 1분당 1px) + * + * @param block 시간 블록 + * @returns {number} 시간 블록의 높이 + */ function calculateDuration(block) { const start = timeToMinutes(block.displayStartTime); const end = timeToMinutes(block.displayEndTime); return end - start; } +/** + * 시간 블록의 offset 계산 + * + * @param partOfDay 오전/오후 + * @returns {number} offset + */ function getOffset(partOfDay) { // 720 === 1px per minute, 12 hours * 60 minutes return afternoonStrings.includes(partOfDay) ? 720 : 0; // 12:00 ~ 24:00 } +/** + * 시간 블록 텍스트 생성 + * + * @param block 시간 블록 + * @returns {string} 시간 블록 텍스트 + */ function buildTimeText(block) { return `${block.displayStartTime} ~ ${block.displayEndTime} :: ${block.routineType}` } +/** + * 시간 블록이 충분한 높이를 가지고 있는지 확인

+ * (충분한 높이를 가지고 있지 않으면 툴팁을 표시하지 않는다.) + * + * @param block 시간 블록 + * @returns {boolean} 충분한 높이를 가지고 있는지 여부 + */ function isEnoughHeightBlock(block) { const SHOW_TEXT_PIXELS = 30; return calculateDuration(block) >= SHOW_TEXT_PIXELS; } +/** + * 시간 블록 업데이트 + * + * @param block 시간 블록 + */ function updateBlock(block) { block.lastUpdated = new Date().toISOString(); store.commit('setEditBlock', block); } +/** + * 영어로 변환 + * + * @param partOfDay 오전/오후 + * @returns {string} 영어로 변환된 오전/오후 + */ function convertEng(partOfDay) { return morningStrings.includes(partOfDay) ? 'morning' : 'afternoon'; } From 6b34e546fe09adbbad9986932e1f4d6c518f6c71 Mon Sep 17 00:00:00 2001 From: HyunggiPark Date: Thu, 14 Mar 2024 21:08:32 +0900 Subject: [PATCH 12/12] =?UTF-8?q?chore:=20vue=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EC=97=90=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/boards/common/SearchForm.vue | 6 ++ .../dailyRoutine/draw/DrawDailyRoutine.vue | 3 + .../dailyRoutine/draw/DrawStatisticsChart.vue | 3 + .../dailyRoutine/input/ContentInput.vue | 3 + .../dailyRoutine/input/DateInput.vue | 7 +++ .../dailyRoutine/input/InputDailyRoutine.vue | 59 +++++++++++++++---- .../dailyRoutine/input/TypeInput.vue | 29 ++++++--- .../dailyRoutine/popup/SelectDatePopUp.vue | 26 ++++++++ .../statistics/SelectDateWithCalendar.vue | 12 ++++ .../statistics/StatisticTable.vue | 30 ++++++++++ .../src/components/default/DefaultHeader.vue | 17 ++++++ my-garden-fe/src/main.js | 2 + my-garden-fe/src/pages/Login.vue | 22 ++++++- my-garden-fe/src/pages/SignUp.vue | 21 +++++++ .../src/pages/boards/learn/LearnBoardList.vue | 21 +++++++ .../src/pages/boards/learn/LearnBoardView.vue | 9 +++ .../pages/boards/learn/LearnBoardWrite.vue | 30 ++++++++++ .../pages/boards/notice/NoticeBoardList.vue | 24 ++++++++ .../pages/boards/notice/NoticeBoardView.vue | 9 +++ .../pages/boards/notice/NoticeBoardWrite.vue | 29 +++++++++ .../dailyRoutine/DailyRoutineStatistics.vue | 53 ++++++++++++++++- .../src/scripts/axios-interceptors.js | 23 ++++++++ my-garden-fe/src/scripts/parseJwt.js | 5 ++ my-garden-fe/src/scripts/router.js | 23 ++++++++ 24 files changed, 444 insertions(+), 22 deletions(-) diff --git a/my-garden-fe/src/components/boards/common/SearchForm.vue b/my-garden-fe/src/components/boards/common/SearchForm.vue index 23565cf8..6c5ff636 100644 --- a/my-garden-fe/src/components/boards/common/SearchForm.vue +++ b/my-garden-fe/src/components/boards/common/SearchForm.vue @@ -27,6 +27,9 @@ const pageSize = ref(props.queryParameter.pageSize); const sort = ref(props.queryParameter.sort); const order = ref(props.queryParameter.order); +/** + * 검색 + */ function search() { emits("search", { startDate: startDate.value, @@ -40,6 +43,9 @@ function search() { }); } +/** + * 검색 조건 변경 감지 + */ watch(() => props.queryParameter, () => { if (Object.keys(props.queryParameter).length === 0) { return; diff --git a/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue b/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue index 0f5a258b..63aaa7a1 100644 --- a/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue +++ b/my-garden-fe/src/components/dailyRoutine/draw/DrawDailyRoutine.vue @@ -90,6 +90,9 @@ function createTimeBlock(block, startTime, endTime, partOfDay) { return scheduleDefaultData; } +/** + * 조회 날짜가 변경되면, 해당 날짜의 루틴을 조회한다. + */ watch(() => store.getters.getViewDate, (newDate) => { const {targetStartDateTime, targetEndDateTime} = getTargetDateTimeRange(newDate); diff --git a/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue b/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue index 5efde4e1..3a5203f9 100644 --- a/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue +++ b/my-garden-fe/src/components/dailyRoutine/draw/DrawStatisticsChart.vue @@ -166,6 +166,9 @@ onMounted(() => { ); }); +/** + * 시간 블록 배열이 업데이트되면, 통계 데이터를 계산하고, Chart.js의 DataSet을 업데이트한다. + */ watch(() => store.getters.getTimeBlockArray, (timeBlockArray) => { const statisticData = {}; diff --git a/my-garden-fe/src/components/dailyRoutine/input/ContentInput.vue b/my-garden-fe/src/components/dailyRoutine/input/ContentInput.vue index 9c07362f..a1b23e49 100644 --- a/my-garden-fe/src/components/dailyRoutine/input/ContentInput.vue +++ b/my-garden-fe/src/components/dailyRoutine/input/ContentInput.vue @@ -7,6 +7,9 @@ const props = defineProps({ content: String }); +/** + * 일과 내용 + */ const content = ref(props.content); const emit = defineEmits(['changeContent', 'submit']) diff --git a/my-garden-fe/src/components/dailyRoutine/input/DateInput.vue b/my-garden-fe/src/components/dailyRoutine/input/DateInput.vue index 4a134037..8e539e54 100644 --- a/my-garden-fe/src/components/dailyRoutine/input/DateInput.vue +++ b/my-garden-fe/src/components/dailyRoutine/input/DateInput.vue @@ -8,8 +8,15 @@ const props = defineProps({ }); const emit = defineEmits(['changeDate']) + +/** + * 일과 시간 + */ const dateTime = ref(''); +/** + * 일과 시간 변경 감지 + */ watch(() => [props.startDateTime, props.endDateTime], (newValue) => { if (props.inputName === '시작 시간') { dateTime.value = newValue[0]; diff --git a/my-garden-fe/src/components/dailyRoutine/input/InputDailyRoutine.vue b/my-garden-fe/src/components/dailyRoutine/input/InputDailyRoutine.vue index 894ff1a8..dedc6446 100644 --- a/my-garden-fe/src/components/dailyRoutine/input/InputDailyRoutine.vue +++ b/my-garden-fe/src/components/dailyRoutine/input/InputDailyRoutine.vue @@ -8,16 +8,18 @@ import {deleteDailyRoutineApi, postDailyRoutineApi, updateDailyRoutineApi} from import {store} from "@/scripts/store.js"; import {getTodayDate} from "@/components/dailyRoutine/api/util.js"; +/** + * 하루 일과 입력에 필요한 내용 + */ const startDate = ref(''); const endDate = ref(''); const content = ref(''); const routineType = ref('STUDY'); const isUpdateMode = ref(false); -onMounted(() => { - getLastStartDateTime(); -}); - +/** + * 오늘 마지막 시작 시간을 가져온다. + */ function getLastStartDateTime() { let todayLastStartDateTime = localStorage.getItem("todayLastStartDateTime"); const todayDate = getTodayDate(); @@ -30,6 +32,10 @@ function getLastStartDateTime() { startDate.value = todayLastStartDateTime; } +/** + * 버튼을 누르거나, Ctrl + Enter를 눌렀을 때 실행되는 함수
+ * 일과 등록 유효성 검사 후, 일과 등록 또는 수정을 실행한다. + */ function submit() { if (!validate()) { return; @@ -41,6 +47,9 @@ function submit() { postRoutine(); } +/** + * 일과 등록 유효성 검사 + */ function validate() { //시작 날짜가 끝 날짜보다 늦을 수 없다. function validateStartDateIsBeforeEndDate() { @@ -56,7 +65,10 @@ function validate() { return false; } - //두 날짜는 1일이상 차이가 날 수 없다. + /** + * 두 날짜의 차이가 1일 이상 나는지 검증한다. + * @returns {boolean} + */ function validateDateDifferenceIsSmaller1() { // Calculate the time difference in milliseconds const timeDifference = new Date(endDate.value) - new Date(startDate.value); @@ -77,7 +89,9 @@ function validate() { return false; } - //content는 255자를 넘을 수 없다. + /** + * content의 길이가 255자를 넘지 않는지 검증한다. + */ function validateContentLength() { if (content.value.length > 255) { alert("content는 255자를 넘을 수 없습니다.") @@ -90,6 +104,9 @@ function validate() { return validateContentLength(); } +/** + * 일과 등록 + */ function postRoutine() { if (!validate()) { return; @@ -100,6 +117,9 @@ function postRoutine() { localStorage.setItem("todayLastStartDateTime", endDate.value); } +/** + * 일과 수정 + */ function updateRoutine() { if (!updateValidate()) { return; @@ -108,21 +128,27 @@ function updateRoutine() { updateDailyRoutineApi(store.getters.getEditBlock.id, startDate.value, endDate.value, routineType.value, content.value); } +/** + * 일과 수정 유효성 검사 + */ function updateValidate() { if (!validate()) { return false; } - function isNotDateToday() { - //오늘내의 날짜로만 수정이 가능하다. - const todayDate = getTodayDate(); // Assuming getTodayDate() returns a String + /** + * 수정 하려는 날짜가 오늘 날짜인지 검증한다. + * @returns {boolean} 오늘 내의 날짜가 아니면 false, 아니면 true + */ + function isNotTodayDate() { + const todayDate = getTodayDate(); const tempStartDate = startDate.value.split("T")[0]; const tempEndDate = endDate.value.split("T")[0]; return tempStartDate !== todayDate || tempEndDate !== todayDate; } - if (isNotDateToday()) { + if (isNotTodayDate()) { alert("오늘 내의 날짜만 수정이 가능합니다."); return false; } @@ -130,6 +156,9 @@ function updateValidate() { return true; } +/** + * 수정 취소 + */ function cancelUpdate() { isUpdateMode.value = false; getLastStartDateTime(); @@ -138,10 +167,20 @@ function cancelUpdate() { content.value = ''; } +/** + * 일과 삭제 + */ function deleteRoutine() { deleteDailyRoutineApi(store.getters.getEditBlock.id); } +onMounted(() => { + getLastStartDateTime(); +}); + +/** + * 수정 모드로 변경 + */ watch(() => store.getters.getEditBlock, (newVal) => { const updateStartDateTime = newVal.startDate + 'T' + newVal.displayStartTime; diff --git a/my-garden-fe/src/components/dailyRoutine/input/TypeInput.vue b/my-garden-fe/src/components/dailyRoutine/input/TypeInput.vue index 27c061b8..a8a1a694 100644 --- a/my-garden-fe/src/components/dailyRoutine/input/TypeInput.vue +++ b/my-garden-fe/src/components/dailyRoutine/input/TypeInput.vue @@ -7,16 +7,18 @@ const props = defineProps({ }); const emit = defineEmits(['changeType']) -const selectValue = ref(''); -watch(() => props.routineType, (newValue) => { - const type = convertRoutineTypeToSelectValue(newValue); - - selectValue.value = type; - emit('changeType', type) - }, {immediate: true} -); +/** + * 일과 타입 + */ +const selectValue = ref(''); +/** + * 루틴 타입을 select value로 변경 + * + * @param inputString 루틴 타입 + * @returns {*|string} select value + */ function convertRoutineTypeToSelectValue(inputString) { switch (inputString) { case '공부': @@ -38,6 +40,17 @@ function convertRoutineTypeToSelectValue(inputString) { } } +/** + * 일과 타입 변경 감지 + */ +watch(() => props.routineType, (newValue) => { + const type = convertRoutineTypeToSelectValue(newValue); + + selectValue.value = type; + emit('changeType', type) + }, {immediate: true} +); +