0. Request
@Getter
public class ServiceCenterQnaReq {
private String title;
private String type;
private String content;
private boolean isSecret;
}
해당 요청을 받아 저장 및 수정 작업을 수행한다.
1. 컨트롤러
@RestController
@RequiredArgsConstructor
@RequestMapping("/service-qna")
public class ServiceCenterQnaController {
private final ServiceCenterQnaService serviceCenterQnaService;
@PostMapping
public ResponseEntity<String> save(@RequestBody ServiceCenterQnaReq serviceCenterQnaReq) {
serviceCenterQnaService.save(serviceCenterQnaReq);
return ResponseEntity.ok().body("저장이 완료되었습니다.");
}
@PatchMapping("/{post-id}")
public ResponseEntity<String> update(ServiceCenterQnaReq serviceCenterQnaReq, @PathVariable("post-id") Long id) {
Long updatedId = serviceCenterQnaService.update(serviceCenterQnaReq, id);
return ResponseEntity.ok().body("갱신 완료: " + updatedId);
}
//해당 글의 세부정보 열람
@GetMapping("/{post-id}")
public ResponseEntity<ServiceCenterQnaDto> showPostDetail(@PathVariable("post-id") Long id) {
return ResponseEntity.ok().body(serviceCenterQnaService.showPostDetail(id));
}
@DeleteMapping("/{post-id}")
public ResponseEntity<String> delete(@PathVariable("post-id") Long id) {
Long deletedId = serviceCenterQnaService.delete(id);
return ResponseEntity.ok().body("삭제 완료: " + deletedId);
}
@GetMapping("/list")
public ResponseEntity<Page<ServiceCenterQnaSimpleDto>> getServiceQnaList(
@PageableDefault(sort="id", direction = Sort.Direction.DESC) Pageable pageable, @RequestParam(required = false) String keyword) {
Page<ServiceCenterQnaSimpleDto> results;
if (keyword == null) {
results = serviceCenterQnaService.find(pageable);
} else {
results = serviceCenterQnaService.find(pageable, keyword);
}
return ResponseEntity.ok().body(results);
}
}
각 CRUD에 해당하는 메서드를 서비스에 요청하고, 결과를 반환했다.
2. 서비스
package sideproject.webClass.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sideproject.webClass.domain.ServiceCenterQNA;
import sideproject.webClass.domain.member.Member;
import sideproject.webClass.dto.ServiceCenterQnaDto;
import sideproject.webClass.dto.ServiceCenterQnaSimpleDto;
import sideproject.webClass.repository.MemberRepository;
import sideproject.webClass.repository.ServiceCenterQnaRepository;
import sideproject.webClass.request.ServiceCenterQnaReq;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ServiceCenterQnaService {
private final ServiceCenterQnaRepository serviceCenterQnaRepository;
private final MemberRepository memberRepository;
/**
* 사용자를 가져오는 방법?->스프링 시큐리티 이용?
* SecurityContextHolder.getContext().getAuthentication().getName();->userId 리턴
*/
@Transactional
public void save(ServiceCenterQnaReq serviceCenterQnaReq) {
//현재 세션 사용자 ID(로그인 시 사용했던 userId) 가져오기
String id = SecurityContextHolder.getContext().getAuthentication().getName();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
/*
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iter = authorities.iterator();
GrantedAuthority auth = iter.next();
String role = auth.getAuthority();
log.info("role: {}", role);
*/
//현재 세션의 아이디로 사용자 찾기
Member author = Optional.of(memberRepository.findByUserId(id).get()).orElseThrow(NoSuchElementException::new);
ServiceCenterQNA qna = ServiceCenterQNA.builder()
.title(serviceCenterQnaReq.getTitle())
.type(serviceCenterQnaReq.getType())
.content(serviceCenterQnaReq.getContent())
.isSecret(serviceCenterQnaReq.isSecret())
.author(author)
.build();
serviceCenterQnaRepository.save(qna);
log.info("save: {}", qna.getId());
}
public ServiceCenterQnaDto showPostDetail(Long id) {
return Optional.of(serviceCenterQnaRepository.findById(id)).get().map(m -> ServiceCenterQnaDto.builder()
.id(m.getId())
.title(m.getTitle())
.type(m.getType())
.content(m.getContent())
.createdDate(m.getCreatedDate())
.isSecret(m.isSecret())
.state(m.getState())
.authorName(m.getAuthor().getName())
.build())
.orElseThrow(NoSuchElementException::new);
}
public Page<ServiceCenterQnaSimpleDto> find(Pageable pageable) {
return serviceCenterQnaRepository.findAll(pageable).map(m->ServiceCenterQnaSimpleDto.builder()
.id(m.getId())
.title(m.getTitle())
.type(m.getType())
.createdDate(m.getCreatedDate())
.isSecret(m.isSecret())
.state(m.getState())
.authorName(m.getAuthor().getName())
.build());
}
//검색 시 연산
public Page<ServiceCenterQnaSimpleDto> find(Pageable pageable, String keyword) {
return serviceCenterQnaRepository.findByTitleContaining(pageable, keyword).map(m-> ServiceCenterQnaSimpleDto.builder()
.id(m.getId())
.title(m.getTitle())
.type(m.getType())
.createdDate(m.getCreatedDate())
.isSecret(m.isSecret())
.state(m.getState())
.authorName(m.getAuthor().getName())
.build());
}
@Transactional
public Long update(ServiceCenterQnaReq serviceCenterQnaReq, Long id) {
ServiceCenterQNA targetPost = Optional.of(serviceCenterQnaRepository.findById(id).get()).orElseThrow(NoSuchElementException::new);
//현재 세션 사용자 ID(로그인 시 사용했던 userId) 가져오기
String authorId = SecurityContextHolder.getContext().getAuthentication().getName();
//현재 사용자가 작성자와 같은지 확인
if (targetPost.getAuthor().equals(Optional.of(memberRepository.findByUserId(authorId).get()))) {
//수정 함수를 만들어서 해당 부분만 수정
targetPost.edit(serviceCenterQnaReq);
}
return id;
}
@Transactional
public Long delete(Long id) {
ServiceCenterQNA targetPost = Optional.of(serviceCenterQnaRepository.findById(id).get()).orElseThrow(NoSuchElementException::new);
//현재 세션 사용자 ID(로그인 시 사용했던 userId) 가져오기
String authorId = SecurityContextHolder.getContext().getAuthentication().getName();
//현재 사용자가 작성자와 같은지 확인
if (targetPost.getAuthor().equals(Optional.of(memberRepository.findByUserId(authorId).get()))) {
serviceCenterQnaRepository.delete(targetPost);
}
return id;
}
}
기본적인 crud는 리포지토리에서 해당 기능을 사용한다.
ServiceCenterQNA.java
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
@Getter
public class ServiceCenterQNA {
@Id
@GeneratedValue
@Column(name = "serv_qna_id")
private Long id;
private String title;
private String type;
@Column(length = 50000)
private String content;
private boolean isSecret;
@Builder.Default
private LocalDateTime createdDate= now();
@Builder.Default
private QnaState state= WAIT;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member author;
public void edit(ServiceCenterQnaReq req) {
this.title = req.getTitle();
this.type = req.getType();
this.content = req.getContent();
this.isSecret = req.isSecret();
}
}
해당 함수를 추가하여 setter 관련 기능을 더욱 안전하게 사용할 수 있도록 하나의 메서드로 묶었다.
수정, 삭제 같은 경우에는 해당 글의 작성자만 가능하도록 해야 한다.
//현재 세션 사용자 ID(로그인 시 사용했던 userId) 가져오기
String authorId = SecurityContextHolder.getContext().getAuthentication().getName();
//현재 사용자가 작성자와 같은지 확인
if (targetPost.getAuthor().equals(Optional.of(memberRepository.findByUserId(authorId).get()))) {
//수정 및 삭제
}
첫 줄의 코드로 현재 세션을 사용하는 사용자의 userId를 가져온다.
이후 이 아이디를 사용하여 member를 조회하고, 해당 게시글의 작성자와 비교하여 같으면 해당 연산을 수행한다.
회원 가입 시 아이디 중복 여부를 확인하므로 findByUserId의 결과는 두 개 이상 나올 수 없다.
3. 리포지토리
@Repository
public interface ServiceCenterQnaRepository extends JpaRepository<ServiceCenterQNA, Long> {
/**
*
* Like: select ... like :username
*
* StartingWith: select ... like :username%
*
* EndingWith: select ... like %:username
*
* Containing: select ... like %:username%
*/
public Page<ServiceCenterQNA> findByTitleContaining(Pageable pageable, String keyword);
}
역시 스프링 데이터 jpa를 활용하여 필요한 기능을 구현하였다.
디버깅
SecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((auth) -> auth //특정 요청에서 요구하는 권한 설정 가능
.requestMatchers(HttpMethod.POST,"/service-qna").hasAnyAuthority("STUDENT", "TEACHER")
.requestMatchers(HttpMethod.PATCH,"/service-qna").hasAnyAuthority("STUDENT", "TEACHER")
.requestMatchers(HttpMethod.DELETE,"/service-qna").hasAnyAuthority("STUDENT", "TEACHER")
.requestMatchers("/**").permitAll()
.anyRequest().authenticated())
.formLogin(formLogin -> formLogin //로그인 페이지 지정
.loginProcessingUrl("/login")
.usernameParameter("userId")
.failureHandler(((request, response, exception) -> {
response.sendError(401);
}))
.permitAll()) //로그인 페이지는 누구나 접근 가능
.logout(logout -> logout
.logoutUrl("/logout"))
.csrf(auth -> auth.disable()); //개발 환경에서 임시적으로 비활성화
return http.build();
}
해당 함수에서 발생하였다.
- hasAnyAuthority(): 해당 권한 중 하나의 권한이라도 가지고 있으면 요청을 허용한다.
- hasAnyRole(): 해당 권한 중 하나의 권한이라도 가지고 있으면 요청을 허용한다. 단 여기에 적은 role은 앞에 "ROLE_"가 자동으로 추가되어 반환된다.
회원가입 구현 시 enum을 {STUDENT, TEACHER} 로 설정하였고, 여태 hasRole 계열의 함수를 사용했었다. 그래서 ROLE_STUDENT, STUDENT가 일치하지 않아 403 에러가 계속 떴었다.
테스트
로그인 이전에 글을 저장할때

로그인 이후에 글을 저장할 때


이후 해당 계정의 이름으로 글이 잘 저장되는 걸 확인할 수 있었다.
'Proj > webClass' 카테고리의 다른 글
| 로그인 구현 (0) | 2024.04.02 |
|---|---|
| 회원가입 개발 및 간단한 리팩토링 (0) | 2024.03.28 |
| Hello Spring Security (1) | 2024.03.27 |