Skip to content

Commit

Permalink
Weekly 9 관리자 api 구현 (#205)
Browse files Browse the repository at this point in the history
* Weekly/8/issue#144 동시성 제어 (#189)

* Feat: RedissonConfig 설정

* Feat: Redisson Lock 구현(임시)

* Fix: RaceCondition 테스트 오류 수정

1. RedissonLock의 value로 hostId만 넘기기
2. RaceConditionTest를 SpringBootTest로 테스트
3. BeforeEach로 Redis 초기화

* Refactor: GroupMember 조회 fetch join 적용

* Refactor: random GroupMember 조회 fetch join 적용

* Refactor: 방문자 조회수 비동기 로직 추가

* Feat: 질문 지목 시 count 증가 동시성 제어

* Refactor: merge weekly9

* Refactor: 누락된 코드 추가

* Refactor: 누락된 코드 추가

* Refactor: 코드 수정

* Refactor: redissonClient 제거

* Refactor: redis 설정 변경

* Refactor: redis 설정 변경

* HotFix: 이미지 파일 resize 비율 조정

* Refactor: RedissonClient 사용 테스트 ActiveProfile 설정

* Refactor: RedissonClient 사용 테스트 Profile 설정

* Fix: Profile() 메서드에 적용

* Fix: ProfileIntegrationTest, RaceConditionTest 주석처리

* Fix: 충돌 해결

* Refactor: Random Question Response 수정

* Chore: 불필요한 Import 제거

* Import: 충돌 해결

---------

Co-authored-by: hjinshin <gudwls818@gmail.com>
Co-authored-by: Kwon Da woon <82216606+momnpa333@users.noreply.github.com>

* Refactor: 알림 타임아웃 수정

* Refactor: 프로필 질문 createdAt 형식 변경

* Fix: 포인트 결제 method 변경

* Feat: 관리자 api 구현

* Feat: alarm 연결 시 로그

---------

Co-authored-by: yso8296 <66588512+yso8296@users.noreply.github.com>
Co-authored-by: Kwon Da woon <82216606+momnpa333@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 9, 2024
1 parent 05e9100 commit 480d147
Show file tree
Hide file tree
Showing 23 changed files with 315 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package supernova.whokie.alarm.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import supernova.whokie.alarm.constants.AlarmConstants;
Expand All @@ -9,6 +10,7 @@
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Service
@RequiredArgsConstructor
public class AlarmService {
Expand All @@ -17,6 +19,7 @@ public class AlarmService {
public SseEmitter connect(Long userId) {
SseEmitter emitter = new SseEmitter(AlarmConstants.SSE_TIMEOUT);
emitters.put(userId, emitter);
log.info("Connected to Alarm UserId: {}", userId);

// SseEmitter가 complate 됐을 경우
emitter.onCompletion(() -> emitters.remove(userId));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package supernova.whokie.global.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminAuthenticate {

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import supernova.whokie.global.interceptor.JwtInterceptor;
import supernova.whokie.global.auth.JwtProvider;
import supernova.whokie.global.resolver.IpArgumentResolver;
import supernova.whokie.global.resolver.LoginAdminArgumentResolver;
import supernova.whokie.global.resolver.LoginUserArgumentResolver;

import java.util.List;
Expand Down Expand Up @@ -39,6 +40,11 @@ public LoginUserArgumentResolver loginUserArgumentResolver() {
return new LoginUserArgumentResolver();
}

@Bean
public LoginAdminArgumentResolver loginAdminArgumentResolver() {
return new LoginAdminArgumentResolver();
}

@Bean
public IpArgumentResolver ipArgumentResolver() {
return new IpArgumentResolver();
Expand All @@ -56,6 +62,7 @@ public void addInterceptors(InterceptorRegistry registry) {
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver());
resolvers.add(ipArgumentResolver());
resolvers.add(loginAdminArgumentResolver());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package supernova.whokie.global.resolver;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import supernova.whokie.global.annotation.AdminAuthenticate;
import supernova.whokie.global.exception.AuthenticationException;
import supernova.whokie.global.exception.ForbiddenException;
import supernova.whokie.user.Role;

public class LoginAdminArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AdminAuthenticate.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String userId = (String) request.getAttribute("userId");
String role = (String) request.getAttribute("role");

if (userId == null) {
throw new AuthenticationException("로그인 후 이용해 주세요.");
}
if (!role.equals(Role.ADMIN.toString())) {
throw new ForbiddenException("관리자만 이용할 수 있습니다.");
}
return Long.parseLong(userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package supernova.whokie.group.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import supernova.whokie.global.annotation.AdminAuthenticate;
import supernova.whokie.global.dto.PagingResponse;
import supernova.whokie.group.controller.dto.GroupResponse;
import supernova.whokie.group.service.GroupService;
import supernova.whokie.group.service.dto.GroupModel;

@RestController
@RequestMapping("/api/admin/group")
@RequiredArgsConstructor
public class AdminGroupController {

private final GroupService groupService;

@GetMapping("")
public PagingResponse<GroupResponse.Info> getAllGroups(
@AdminAuthenticate Long userId,
@PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.ASC) Pageable pageable
) {
Page<GroupModel.Info> models = groupService.getAllGroupPaging(pageable);
Page<GroupResponse.Info> response = models.map(GroupResponse.Info::from);
return PagingResponse.from(response);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public GroupInfoWithMemberCount getGroupInfoWithMemberCountByGroupId(Long groupI
public boolean isGroupExist(Long groupId) {
return groupRepository.existsById(groupId);
}

@Transactional(readOnly = true)
public Page<Groups> getALlGroupPaging(Pageable pageable) {
return groupRepository.findAll(pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,10 @@ public GroupModel.InviteCode inviteGroup(Long userId, Long groupId) {
LocalDateTime.now().plusDays(7));
return GroupModel.InviteCode.from(inviteCode);
}

@Transactional(readOnly = true)
public Page<GroupModel.Info> getAllGroupPaging(Pageable pageable) {
Page<Groups> entities = groupReaderService.getALlGroupPaging(pageable);
return entities.map(GroupModel.Info::from);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@

public class PointRecordRequest {

@Builder
public record Purchase(
@NotNull @Min(0)
int point
) {

}

@Builder
public record Earn(
@NotNull @Min(0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package supernova.whokie.question.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import supernova.whokie.global.annotation.AdminAuthenticate;
import supernova.whokie.global.dto.GlobalResponse;
import supernova.whokie.global.dto.PagingResponse;
import supernova.whokie.question.controller.dto.QuestionRequest;
import supernova.whokie.question.controller.dto.QuestionResponse;
import supernova.whokie.question.service.QuestionService;
import supernova.whokie.question.service.dto.QuestionModel;

@RestController
@RequestMapping("/api/admin/question")
@RequiredArgsConstructor
public class AdminQuestionController {

private final QuestionService questionService;

@GetMapping("")
public PagingResponse<QuestionResponse.Admin> getAllQuestionPaging(
@AdminAuthenticate Long userId,
@PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.ASC) Pageable pageable
) {
Page<QuestionModel.Admin> models = questionService.getAllQuestionPaging(pageable);
Page<QuestionResponse.Admin> response = models.map(QuestionResponse.Admin::from);
return PagingResponse.from(response);
}

@PostMapping("")
public GlobalResponse postCommonQuestion(
@AdminAuthenticate Long userId,
@RequestBody @Valid QuestionRequest.CommonCreate request
) {
questionService.createCommonQuestion(userId, request.toCommand());
return GlobalResponse.builder().message("질문이 등록되ㅐ었습니다.").build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ public QuestionResponse.GroupQuestions getGroupQuestionList(

@PostMapping("/group/question")
public GlobalResponse createGroupQuestion(
@RequestBody @Valid QuestionRequest.Create request,
@RequestBody @Valid QuestionRequest.GroupCreate request,
@Authenticate Long userId
) {
questionService.createQuestion(userId, request.toCommand());
questionService.createGroupQuestion(userId, request.toCommand());
return GlobalResponse.builder().message("질문이 성공적으로 생성되었습니다.").build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import supernova.whokie.group.constants.GroupConstants;
import supernova.whokie.question.service.dto.QuestionCommand;

public class QuestionRequest {

public record Create(
public record CommonCreate(
@NotNull
String content
) {

public QuestionCommand.Create toCommand() {
return QuestionCommand.Create.builder()
.groupId(GroupConstants.COMMON_GROUPS_ID)
.content(content)
.build();
}
}

public record GroupCreate(
@NotNull @Min(1)
Long groupId,
@NotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import supernova.whokie.question.QuestionStatus;
import supernova.whokie.question.service.dto.QuestionModel;

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

public class QuestionResponse {
Expand Down Expand Up @@ -88,7 +88,7 @@ public record Info(
Long groupId,
QuestionStatus status,
String writer,
LocalDate createdAt
LocalDateTime createdAt
) {
public static QuestionResponse.Info from(QuestionModel.Info info) {
return Info.builder()
Expand All @@ -100,6 +100,24 @@ public static QuestionResponse.Info from(QuestionModel.Info info) {
.createdAt(info.createdAt())
.build();
}
}

@Builder
public record Admin(
Long questionId,
String questionContent,
Long groupId,
QuestionStatus status,
LocalDateTime createdAt
) {
public static QuestionResponse.Admin from(QuestionModel.Admin model) {
return Admin.builder()
.questionId(model.questionId())
.questionContent(model.questionContent())
.groupId(model.groupId())
.status(model.status())
.createdAt(model.createdAt())
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ public Question getQuestionByIdAndGroupId(Long questionId, Long groupId) {
() -> new EntityNotFoundException(MessageConstants.QUESTION_NOT_FOUND_MESSAGE));
}

@Transactional(readOnly = true)
public Page<Question> getAllQuestionPaging(Pageable pageable) {
return questionRepository.findAll(pageable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import supernova.whokie.question.QuestionStatus;
import supernova.whokie.question.service.dto.QuestionCommand;
import supernova.whokie.question.service.dto.QuestionModel;
import supernova.whokie.user.Users;
import supernova.whokie.user.service.UserReaderService;

import java.util.List;

Expand All @@ -23,6 +25,7 @@ public class QuestionService {
private final GroupMemberReaderService groupMemberReaderService;
private final QuestionReaderService questionReaderService;
private final QuestionWriterService questionWriterService;
private final UserReaderService userReaderService;

@Transactional(readOnly = true)
public List<QuestionModel.CommonQuestion> getCommonQuestion(Pageable pageable) {
Expand Down Expand Up @@ -62,7 +65,7 @@ public List<QuestionModel.GroupQuestion> getGroupQuestions(Long userId, Long gro
}

@Transactional
public void createQuestion(Long userId, QuestionCommand.Create command) {
public void createGroupQuestion(Long userId, QuestionCommand.Create command) {
GroupMember groupMember = groupMemberReaderService.getByUserIdAndGroupId(userId,
command.groupId());
groupMember.validateApprovalStatus();
Expand All @@ -72,6 +75,14 @@ public void createQuestion(Long userId, QuestionCommand.Create command) {
questionWriterService.save(question);
}

@Transactional
public void createCommonQuestion(Long userId, QuestionCommand.Create command) {
Users admin = userReaderService.getUserById(userId);
Question question = command.toEntity(admin);

questionWriterService.save(question);
}

@Transactional
public void approveQuestion(Long userId, QuestionCommand.Approve command) {
GroupMember groupMember = groupMemberReaderService.getByUserIdAndGroupId(userId,
Expand All @@ -83,4 +94,10 @@ public void approveQuestion(Long userId, QuestionCommand.Approve command) {

question.changeStatus(command.status());
}

@Transactional(readOnly = true)
public Page<QuestionModel.Admin> getAllQuestionPaging(Pageable pageable) {
Page<Question> entities = questionReaderService.getAllQuestionPaging(pageable);
return entities.map(QuestionModel.Admin::from);
}
}
Loading

0 comments on commit 480d147

Please sign in to comment.