Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d7497da
LV 0 : application.yml 파일 생성
jinyp01 May 8, 2026
7a33bda
LV 0 : secretKey 변경
jinyp01 May 8, 2026
9b016e0
LV 1 : ArgumentResolver
jinyp01 May 8, 2026
60c2257
LV 1 : ArgumentResolver
jinyp01 May 8, 2026
d73b821
LV 1 : ArgumentResolver
jinyp01 May 8, 2026
fec0dee
LV 1 : ArgumentResolver
jinyp01 May 8, 2026
3854395
LV 2 : RefactoringQuiz - 1 early return
jinyp01 May 8, 2026
d05f39e
LV 2 : RefactoringQuiz - 2 불필요한 if-else 구문
jinyp01 May 8, 2026
32be086
LV 2 : RefactoringQuiz - 2 불필요한 if-else 구문
jinyp01 May 8, 2026
9a47406
LV 2 : RefactoringQuiz - 3 validation
jinyp01 May 8, 2026
03366db
LV 2 : RefactoringQuiz - 3 validation
jinyp01 May 8, 2026
5372dd0
LV 2 : RefactoringQuiz - 3 validation
jinyp01 May 8, 2026
e5ac6cc
LV 3 : RefactoringQuiz - 3 validation
jinyp01 May 8, 2026
1d5a570
LV 3 : validation
jinyp01 May 8, 2026
5521f60
LV 3 : validation
jinyp01 May 8, 2026
43113be
LV 3 : validation
jinyp01 May 8, 2026
c7ac151
LV 4 : PasswordEncoderTest
jinyp01 May 8, 2026
bc56aaa
LV 4 : 매니저 목록 조회 테스트 수정
jinyp01 May 8, 2026
658dc8d
LV 4 : comment 등록 중 에러 발생 테스트 수정
jinyp01 May 8, 2026
7fb63e5
LV 4 : ManagerService 메서드 수정
jinyp01 May 8, 2026
fe0fee8
LV 5 : Interceptor 클래스 추가
jinyp01 May 10, 2026
21854ae
LV 5 : Interceptor를 사용한 로깅 구현
jinyp01 May 10, 2026
bcb8976
LV 5 : 관리자 검증 로직 오류 수정
jinyp01 May 10, 2026
e13060a
LV 5 : 관리자 인증 성공 시 로그 출력
jinyp01 May 10, 2026
e84ac01
LV 5 : Aspect 클래스 생성
jinyp01 May 10, 2026
3bf63b1
LV 6 : Admin Facade? 적용
jinyp01 May 10, 2026
3b8c49e
LV 6 : Admin 패키지 생성
jinyp01 May 10, 2026
228d15f
LV 6 : RequestMapping 추가
jinyp01 May 10, 2026
0a31f5e
Lv 6 : admin 관련 dto도 admin 패키지로 이동
jinyp01 May 10, 2026
e1a7980
hotfix : 포스트맨에서 권한 변경 요청이 수행 안되는 문제 해결
jinyp01 May 10, 2026
b36f3c2
Lv 5 : AOP를 사용한 로그 출력, 권한별 접근을 위한 SecurityConfig 파일 추가
jinyp01 May 10, 2026
3288cf8
Lv 5 : AOP 맵핑 경로 수정
jinyp01 May 10, 2026
9ec7e2d
Lv 5 : AOP에서 요청본문과 응답본문이 제대로 출력되지 않는 현상 수정
jinyp01 May 11, 2026
52ecfea
Lv 5 : 사용되지 않는 import문 삭제
jinyp01 May 11, 2026
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-security'

// bcrypt
implementation 'at.favre.lib:bcrypt:0.10.2'
Expand Down
16 changes: 9 additions & 7 deletions src/main/java/org/example/expert/client/WeatherClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import org.example.expert.client.dto.WeatherDto;
import org.example.expert.domain.common.exception.ServerException;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
Expand All @@ -26,13 +25,16 @@ public String getTodayWeather() {
ResponseEntity<WeatherDto[]> responseEntity =
restTemplate.getForEntity(buildWeatherApiUri(), WeatherDto[].class);

if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new ServerException(
"날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode()
);
}

WeatherDto[] weatherArray = responseEntity.getBody();
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
} else {
if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}

if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}

String today = getCurrentDate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import org.example.expert.domain.user.enums.UserRole;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {

@Override
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/example/expert/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.example.expert.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());

return http.build();
}
}
31 changes: 31 additions & 0 deletions src/main/java/org/example/expert/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.example.expert.config;

import lombok.RequiredArgsConstructor;
import org.example.expert.domain.common.interceptor.CheckAdminInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final AuthUserArgumentResolver authUserArgumentResolver;
private final CheckAdminInterceptor checkAdminInterceptor;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authUserArgumentResolver);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkAdminInterceptor)
.addPathPatterns("/admin/**");
}


}
24 changes: 24 additions & 0 deletions src/main/java/org/example/expert/domain/admin/AdminFacade.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.example.expert.domain.admin;

import lombok.RequiredArgsConstructor;
import org.example.expert.domain.admin.service.CommentAdminService;
import org.example.expert.domain.admin.dto.UserRoleChangeRequest;
import org.example.expert.domain.admin.service.UserAdminService;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AdminFacade {

private final CommentAdminService commentAdminService;
private final UserAdminService userAdminService;

public void deleteComment(long commentId) {
commentAdminService.deleteComment(commentId);
}

public void changeUserRole(long userId, UserRoleChangeRequest request) {
userAdminService.changeUserRole(userId, request);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.example.expert.domain.admin.controller;

import lombok.RequiredArgsConstructor;
import org.example.expert.domain.admin.AdminFacade;
import org.example.expert.domain.admin.dto.UserRoleChangeRequest;
import org.springframework.web.bind.annotation.*;

@RequestMapping("/admin")
@RestController
@RequiredArgsConstructor
public class AdminController {

private final AdminFacade adminFacade;

@DeleteMapping("/comments/{commentId}")
public void deleteComment(@PathVariable long commentId) {
adminFacade.deleteComment(commentId);
}

@PatchMapping("/users/{userId}")
public void changeUserRole(@PathVariable long userId,
@RequestBody UserRoleChangeRequest request) {
adminFacade.changeUserRole(userId, request);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.example.expert.domain.user.dto.request;
package org.example.expert.domain.admin.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.example.expert.domain.comment.service;
package org.example.expert.domain.admin.service;

import lombok.RequiredArgsConstructor;
import org.example.expert.domain.comment.repository.CommentRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.example.expert.domain.user.service;
package org.example.expert.domain.admin.service;

import lombok.RequiredArgsConstructor;
import org.example.expert.domain.common.exception.InvalidRequestException;
import org.example.expert.domain.user.dto.request.UserRoleChangeRequest;
import org.example.expert.domain.admin.dto.UserRoleChangeRequest;
import org.example.expert.domain.user.entity.User;
import org.example.expert.domain.user.enums.UserRole;
import org.example.expert.domain.user.repository.UserRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ public class AuthService {
@Transactional
public SignupResponse signup(SignupRequest signupRequest) {

if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}

String encodedPassword = passwordEncoder.encode(signupRequest.getPassword());

UserRole userRole = UserRole.of(signupRequest.getUserRole());

if (userRepository.existsByEmail(signupRequest.getEmail())) {
throw new InvalidRequestException("이미 존재하는 이메일입니다.");
}


User newUser = new User(
signupRequest.getEmail(),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.example.expert.domain.common.aspect;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Slf4j
@Component
@RequiredArgsConstructor
public class LoggingAop {

private final ObjectMapper objectMapper;

@Around("execution(* org.example.expert.domain.admin.controller.AdminController.*(..))")
public Object commentAop(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis(); // API 요청 시각

ServletRequestAttributes attr =
(ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();

Object[] args = joinPoint.getArgs();
String requestBody = objectMapper.writeValueAsString(args); // 요청 본문


Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = auth.getName();

Object result = joinPoint.proceed();

String responseBody = objectMapper.writeValueAsString(result); // 응답 본문
log.info(" 유저 아이디 : {}," +
" API 요청 시각 : {}" +
" API 요청 URL : {}" +
" 요청 본문 : {}" +
" 응답 본문 : {}",
userId,
start,
request.getRequestURI(),
requestBody,
responseBody);

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.example.expert.domain.common.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.expert.domain.user.enums.UserRole;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.time.LocalDateTime;

@Slf4j
@Component
@RequiredArgsConstructor
public class CheckAdminInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
UserRole userRole = UserRole.of((String)request.getAttribute("userRole"));
LocalDateTime reqTime = LocalDateTime.now();

if(!(userRole == UserRole.ADMIN)) {
log.warn("관리자 아님. 접근 거부");
response.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자만 접근할 수 있습니다.");
return false;
}
log.info("관리자 인증 성공, 요청 시각 {}, 요청 URL {}", reqTime, request.getRequestURL());

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSa
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));

if(todo.getUser() == null) {
throw new InvalidRequestException("일정을 생성한 유저만 담당자를 지정할 수 있습니다.");
}

if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
throw new InvalidRequestException("일정을 생성한 유저만 담당자를 지정할 수 있습니다.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import org.example.expert.domain.todo.entity.Todo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

Expand All @@ -14,10 +14,6 @@ public interface TodoRepository extends JpaRepository<Todo, Long> {
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

@Query("SELECT t FROM Todo t " +
"LEFT JOIN FETCH t.user " +
"WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

int countById(Long todoId);
@EntityGraph(attributePaths = {"user"})
Optional<Todo> findTodoById(Long todoId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public Page<TodoResponse> getTodos(int page, int size) {

@Transactional(readOnly = true)
public TodoResponse getTodo(long todoId) {
Todo todo = todoRepository.findByIdWithUser(todoId)
Todo todo = todoRepository.findTodoById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));

User user = todo.getUser();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.example.expert.domain.user.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.expert.domain.common.annotation.Auth;
import org.example.expert.domain.common.dto.AuthUser;
Expand All @@ -21,7 +22,7 @@ public ResponseEntity<UserResponse> getUser(@PathVariable long userId) {
}

@PutMapping("/users")
public void changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) {
public void changePassword(@Auth AuthUser authUser, @Valid @RequestBody UserChangePasswordRequest userChangePasswordRequest) {
userService.changePassword(authUser.getId(), userChangePasswordRequest);
}
}
Loading