Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
```

== API Docs Link
=== user
- link:auth.html[Auth Api 문서]
- link:book.html[Book Api 문서]
- link:book-log.html[Book Log Api 문서]
- link:book-log-like.html[Book Log Like Api 문서]
- link:my-page.html[My Page Api 문서]
- link:demo-day.html[Demo Day Api 문서]

=== admin
- link:user-book-log-count.html[User Book Log Count Api 문서]
18 changes: 18 additions & 0 deletions src/docs/asciidoc/user-book-log-count.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
= UserBookLog Api 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== UserBookLog Count Api

=== Request
include::{snippets}/read-user-book-log-count-in-week/request-headers.adoc[]
include::{snippets}/read-user-book-log-count-in-week/query-parameters.adoc[]
include::{snippets}/read-user-book-log-count-in-week/http-request.adoc[]

=== Response
==== BodyField
include::{snippets}/read-user-book-log-count-in-week/response-fields.adoc[]
include::{snippets}/read-user-book-log-count-in-week/http-response.adoc[]
9 changes: 9 additions & 0 deletions src/main/java/dayone/dayone/auth/application/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import dayone.dayone.auth.token.TokenProvider;
import dayone.dayone.user.entity.User;
import dayone.dayone.user.entity.repository.UserRepository;
import dayone.dayone.user.entity.value.Role;
import dayone.dayone.user.exception.UserErrorCode;
import dayone.dayone.user.exception.UserException;
import io.jsonwebtoken.Claims;
Expand Down Expand Up @@ -51,6 +52,14 @@ public Long validUserByAccessToken(final String accessToken) {
return userId;
}

public boolean validUserIsAdmin(final String accessToken) {
final Claims claims = tokenProvider.parseClaims(accessToken);
final Long userId = claims.get("memberId", Long.class);
return userRepository.findById(userId)
.orElseThrow(() -> new UserException(UserErrorCode.NOT_EXIST_USER))
.isAdmin();
}

@Transactional
public void deleteToken(final Long userId, final String refreshToken) {
final AuthToken authToken = authTokenRepository.findByUserIdAndRefreshToken(userId, refreshToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public enum AuthErrorCode implements ErrorCode {
HAVE_NOT_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 4002, "refresh 토큰이 존재하지 않습니다."),
NOT_LOGIN_USER(HttpStatus.UNAUTHORIZED, 4003, "로그인되지 않은 유저입니다."),
HAVE_WRONG_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, 4004, "잘못된 refresh 토큰을 가지고 있습니다."),
ALREADY_LOGIN(HttpStatus.BAD_REQUEST, 4005, "이미 로그인되어 있습니다.");
ALREADY_LOGIN(HttpStatus.BAD_REQUEST, 4005, "이미 로그인되어 있습니다."),
NOT_ADMIN_USER(HttpStatus.FORBIDDEN, 4006, "admin 권한이 없습니다.");

private final HttpStatus httpStatus;
private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

@RequiredArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor {
public class AuthenticationInterceptor implements HandlerInterceptor {

private final AuthService authService;
private final AuthContext authContext;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dayone.dayone.auth.ui.interceptor;

import dayone.dayone.auth.application.AuthService;
import dayone.dayone.auth.exception.AuthErrorCode;
import dayone.dayone.auth.exception.AuthException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Optional;

@RequiredArgsConstructor
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {

private final AuthService authService;

@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
if (request.getMethod().equals(HttpMethod.OPTIONS.name())) {
return true;
}

final Optional<String> token = TokenExtractor.extractToken(request);
if (token.isEmpty()) {
throw new AuthException(AuthErrorCode.NOT_LOGIN_USER);
}

if (!authService.validUserIsAdmin(token.get())) {
throw new AuthException(AuthErrorCode.NOT_ADMIN_USER);
}

return true;
}
}
1 change: 0 additions & 1 deletion src/main/java/dayone/dayone/booklog/entity/BookLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ public BookLog(final Long id, final Passage passage, final Comment comment, fina
this.createdAt = createdAt;
}

// TODO : 추후에 user 객체도 고려하기
public BookLog(final Long id, final Passage passage, final Comment comment, final Book book, final User user) {
this.id = id;
this.passage = passage;
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/dayone/dayone/global/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package dayone.dayone.global.config;

import dayone.dayone.auth.ui.argumentresolver.AuthArgumentResolver;
import dayone.dayone.auth.ui.interceptor.AuthInterceptor;
import dayone.dayone.auth.ui.interceptor.AuthenticationInterceptor;
import dayone.dayone.auth.ui.interceptor.AuthorizationInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
Expand All @@ -14,13 +15,20 @@
@Configuration
public class WebConfig implements WebMvcConfigurer {

private final AuthInterceptor authInterceptor;
private final AuthenticationInterceptor authenticationInterceptor;
private final AuthorizationInterceptor authorizationInterceptor;
private final AuthArgumentResolver authArgumentResolver;

@Override
public void addInterceptors(final org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
registry.addInterceptor(authenticationInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/v1/admin/**")
.excludePathPatterns("/api/v1/auth/login")
.excludePathPatterns("/api/v1/auth/reissue-token");

registry.addInterceptor(authorizationInterceptor)
.addPathPatterns("/api/v1/admin/**")
.excludePathPatterns("/api/v1/auth/login")
.excludePathPatterns("/api/v1/auth/reissue-token");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ public class CommonExceptionHandler {
public ResponseEntity<CommonResponseDto<ExceptionResponse>> handleException(final CommonException exception) {
return ExceptionResponse.toResponse(exception);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<CommonResponseDto<ExceptionResponse>> handleRuntimeException(final IllegalArgumentException exception) {
return ExceptionResponse.toResponse(exception);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dayone.dayone.global.exception;

import dayone.dayone.global.response.CommonResponseDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

public record ExceptionResponse(
Expand All @@ -12,4 +13,9 @@ public static ResponseEntity<CommonResponseDto<ExceptionResponse>> toResponse(fi
return ResponseEntity.status(exception.getHttpStatus())
.body(CommonResponseDto.forFailure(exception.getCode(), exception.getMessage()));
}

public static ResponseEntity<CommonResponseDto<ExceptionResponse>> toResponse(final IllegalArgumentException exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(CommonResponseDto.forFailure(exception.getMessage()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ public static <D> CommonResponseDto<D> forFailure(final int code, final String m
public static <D> CommonResponseDto<D> forFailure(final int code, final String message) {
return new CommonResponseDto<>(code, message, null);
}

public static <D> CommonResponseDto<D> forFailure(final String message) {
return new CommonResponseDto<>(-1, message, null);
}
}
20 changes: 17 additions & 3 deletions src/main/java/dayone/dayone/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dayone.dayone.user.entity;

import dayone.dayone.global.entity.BaseEntity;
import dayone.dayone.user.entity.value.Role;
import dayone.dayone.user.entity.value.RoleConverter;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
Expand Down Expand Up @@ -29,22 +32,33 @@ public class User extends BaseEntity {

private String profileImage;

public User(final Long id, final String email, final String password, final String name, final String profileImage) {
private int generation;

@Convert(converter = RoleConverter.class)
private Role role;

public User(final Long id, final String email, final String password, final String name, final int generation, final String profileImage, final Role role) {
this.id = id;
this.email = email;
this.password = password;
this.name = name;
this.profileImage = profileImage;
this.generation = generation;
this.role = role;
this.createdAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS);
this.updatedAt = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS);
}

// TODO : 이미지는 추후에 처리하기
public static User forSave(final String email, final String password, final String name) {
return new User(null, email, password, name, "기본 이미지");
public static User forSave(final String email, final String password, final String name, final int generation) {
return new User(null, email, password, name, generation, "기본 이미지", Role.MEMBER);
}

public void updateProfileImage(final String profileImage) {
this.profileImage = profileImage;
}

public boolean isAdmin() {
return this.role == Role.ADMIN;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import dayone.dayone.user.entity.User;
import dayone.dayone.user.entity.repository.dto.UserBookInfo;
import dayone.dayone.user.entity.repository.dto.UserBookLogCountInfo;
import dayone.dayone.user.entity.repository.dto.UserBookLogInfo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -34,4 +36,14 @@ ORDER BY MAX(bl.createdAt) DESC
ORDER BY bl.createdAt DESC
""")
List<UserBookLogInfo> findUserBookLogInfo(@Param("userId") final Long userId, @Param("bookId") final Long bookId);

@Query("""
SELECT u.name as name, COUNT(bl.id) as count
FROM Users u
LEFT JOIN BookLog bl ON bl.user = u
AND bl.createdAt BETWEEN :startDate AND :endDate
WHERE u.generation = :generation
GROUP BY u.name
""")
List<UserBookLogCountInfo> findByGenerationAndStartDateAndEndDate(@Param("generation") final int generation, @Param("startDate") final LocalDateTime startDate, @Param("endDate") final LocalDateTime endDate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dayone.dayone.user.entity.repository.dto;

public interface UserBookLogCountInfo {

String getName();

int getCount();
}
19 changes: 19 additions & 0 deletions src/main/java/dayone/dayone/user/entity/value/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dayone.dayone.user.entity.value;

import dayone.dayone.user.exception.UserErrorCode;
import dayone.dayone.user.exception.UserException;

import java.util.Arrays;

public enum Role {

MEMBER,
ADMIN;

public static Role from(final String input) {
return Arrays.stream(values())
.filter(role -> role.name().equals(input))
.findFirst()
.orElseThrow(() -> new UserException(UserErrorCode.NOT_EXIST_ROLE));
}
}
26 changes: 26 additions & 0 deletions src/main/java/dayone/dayone/user/entity/value/RoleConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dayone.dayone.user.entity.value;

import dayone.dayone.user.exception.UserErrorCode;
import dayone.dayone.user.exception.UserException;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class RoleConverter implements AttributeConverter<Role, String> {

@Override
public String convertToDatabaseColumn(Role role) {
if (role == null) {
throw new UserException(UserErrorCode.ROLE_BLANK_AND_NULL);
}
return role.name();
}

@Override
public Role convertToEntityAttribute(String roleType) {
if (roleType == null) {
throw new UserException(UserErrorCode.ROLE_BLANK_AND_NULL);
}
return Role.from(roleType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ public enum UserErrorCode implements ErrorCode {

EMAIL_BLANK_AND_NULL(HttpStatus.BAD_REQUEST, 2001, "이메일 정보는 null이거나 빈 값일 수 없습니다."),
INVALID_EMAIL_FORM(HttpStatus.BAD_REQUEST, 2002, "올바른 이메일 형식이 아닙니다."),
NOT_EXIST_USER(HttpStatus.BAD_REQUEST, 2003, "존재하지 않는 유저입니다.");
NOT_EXIST_USER(HttpStatus.BAD_REQUEST, 2003, "존재하지 않는 유저입니다."),
ROLE_BLANK_AND_NULL(HttpStatus.BAD_REQUEST, 2004, "role 정보는 null이거나 빈 값일 수 없습니다."),
NOT_EXIST_ROLE(HttpStatus.BAD_REQUEST, 2005, "존재하지 않는 role입니다.");

private final HttpStatus httpStatus;
private final int code;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/dayone/dayone/user/service/UserBookService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

import dayone.dayone.user.entity.repository.UserRepository;
import dayone.dayone.user.entity.repository.dto.UserBookInfo;
import dayone.dayone.user.entity.repository.dto.UserBookLogCountInfo;
import dayone.dayone.user.entity.repository.dto.UserBookLogInfo;
import dayone.dayone.user.exception.UserErrorCode;
import dayone.dayone.user.exception.UserException;
import dayone.dayone.user.service.dto.UserBookListResponse;
import dayone.dayone.user.service.dto.UserBookLogCountInWeekResponse;
import dayone.dayone.user.service.dto.UserBookLogListResponse;
import dayone.dayone.user.service.dto.UsersBookLogCountRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;

@Transactional(readOnly = true)
Expand All @@ -35,4 +39,12 @@ public UserBookLogListResponse getUserBookLogs(final Long userId, final Long boo
List<UserBookLogInfo> userBookLogInfos = userRepository.findUserBookLogInfo(userId, bookId);
return UserBookLogListResponse.from(userBookLogInfos);
}

public UserBookLogCountInWeekResponse getUserBookLogCountInWeek(final UsersBookLogCountRequest request) {
final LocalDateTime startDate = request.startDate().atStartOfDay();
final LocalDateTime endDate = request.endDate().plusDays(1).atStartOfDay();

final List<UserBookLogCountInfo> userBookLogCountInfos = userRepository.findByGenerationAndStartDateAndEndDate(request.generation(), startDate, endDate);
return UserBookLogCountInWeekResponse.from(userBookLogCountInfos);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dayone.dayone.user.service.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import dayone.dayone.user.entity.repository.dto.UserBookLogCountInfo;

import java.util.List;

public record UserBookLogCountInWeekResponse(

@JsonProperty("user_book_log_counts")
List<UserBookLogCountResponse> userBookLogCounts
) {

public static UserBookLogCountInWeekResponse from(final List<UserBookLogCountInfo> userBookLogCountInfos) {
final List<UserBookLogCountResponse> userBookLogCountResponses = userBookLogCountInfos.stream()
.map(UserBookLogCountResponse::from)
.toList();
return new UserBookLogCountInWeekResponse(userBookLogCountResponses);
}
}
Loading