본문 바로가기

Proj/webClass

고객센터 Q&A crud 구현

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