본문 바로가기

Spring Boot 수업 정리

Spring Boot 5일차

댓글 기능구현

 

● 댓글 DB구성 

create table comment(
cno bigint auto_increment,
bno bigint not null,
writer varchar(200),
content text,
reg_date datetime default now(),
primary key (cno));

 

● 댓글 구성에 맞게 VO를 만들어주고 매퍼까지 쭉 연결해서 생성 

 

● detail 화면 아래에 댓글 등록, 출력 표시라인을 만들어줌. ( 전에했던거 가져옴 )

       <!--/* comment line */-->
       <!--/* post */-->
       <div class="input-group mb-3">
           <span class="input-group-text" id="cmtWriter">NickName</span>
           <input type="text"  id="cmtText" class="form-control" placeholder="Add Comment" aria-label="Comment" aria-describedby="basic-addon1">
           <button type="button" id="cmtAddBtn"  class="btn btn-outline-success">post</button>
       </div>

       <!--/* spread */-->
       <ul class="list-group list-group-flush" id="cmtListArea">
           <li class="list-group-item">
               <div class="ms-2 me-auto">
                   <div class="fw-bold">writer</div>
                   Content
               </div>
               <span class="badge text-bg-primary rounded-pill">regDate</span>
           </li>
       </ul>

 

● 댓글 스크립트 파일을 만들고 post로 등록부터  기능을 만들어줌

 

- 게시글번호를 이용해야하므로 스크립트가 bno를 인식할 수 있게 일단 bno를 bnoVal로 받아 저장해두고

       <script th:inline="javascript">
           const bnoVal = [[${bvo.bno}]];
           console.log(bnoVal);
       </script>

 

-  일단 등록버튼을 눌렀을때 작성자와 내용을 받아서 데이터로 만들어둠.

document.getElementById('cmtAddBtn').addEventListener('click', () => {
    const cmtText = document.getElementById('cmtText');
    const cmtWriter = document.getElementById('cmtWriter');

    let cmtData = {
        bno : bnoVal,
        writer : cmtWriter.innerText,
        content : cmtText.value
    }
  }

 

-  비동기로 post했을때의 isOk 텍스트형식으로 받아오는 펑션을 작성함.

async function postCommentToServer(cmtData) {
    try {
        const url = "/comment/post"
        const config = {
            method: 'post',
			headers: {
				'Content-Type': 'application/json; charset=utf-8'
			},
			body: JSON.stringify(cmtData)
        };
        const resp = await fetch(url, config);
        console.log(resp);
        const result = await resp.text(); 
        return result;
    } catch (error) {
        console.log(error);
    }
}

 

- 컨트롤러에서부터 매퍼까지 연결해서 구현 

    @PostMapping("/post")
    public ResponseEntity<String> post(@RequestBody CommentVO cvo){
        int isOk = csv.post(cvo);

        return isOk > 0 ?
                new ResponseEntity<String>("1", HttpStatus.OK) : new ResponseEntity<String>("0",HttpStatus.INTERNAL_SERVER_ERROR);
    }

 

    <insert id="post">
        insert into comment(bno, writer, content)
        values(#{bno},#{writer},#{content})
    </insert>

 

- 등록버튼을 눌렀을때 만들어든 펑션이 작동해서 result가 1이면 등록성공, 0이면 실패를 알리도록 구현 추가.

postCommentToServer(cmtData).then(result => {
        if(result == '1'){
            alert("댓글 등록 성공");
            cmtText.value = "";
        } else {
            alert("댓글 등록 실패");
        }

  

 

  등록했으니 댓글이 표시되는 출력라인을 구현하기위해 댓글을 뿌려주는 기능구현 (더보기 버튼까지)

 

- 서버에서 번호랑 페이지를 가져와 댓글의 리스트를 가져올 수 있도록 구현

async function getCommentFromServer(bno, page) {
    try {
        const resp = await fetch("/comment/"+ bno + "/" + page)
        const result = await resp.json();
        return result;
    } catch (error) {
        console.log(error);
    }
}

 

- 페이징 핸들러에 댓글객체의 리스트와 댓글용 생성자를 추가해줌.

 private List<CommentVO> cmtList;
 
 public PagingHandler(PagingVO pgvo, int totalCount, List<CommentVO> cmtList) {
     this(pgvo, totalCount);
     this.cmtList = cmtList;  
   }

 

- 컨트롤러에서 비동기 펑션 구현

    @GetMapping(value = "/{bno}/{page}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity <PagingHandler> list(@PathVariable("bno") long bno, @PathVariable("page") int page ) {

        PagingVO pgvo = new PagingVO(page, 5);  // DB에 설정할 값 설정 limit 0,5
        PagingHandler ph = csv.getList(bno,pgvo);

        log.info(">>> ph > {}", ph);

        return new ResponseEntity<PagingHandler>(ph, HttpStatus.OK);
    }

 

- 서비스 구현체에서 댓글리스트와 댓글의 총개수를 가져와서 페이징핸들러 객체를 생성해 리턴할 수 있도록 구현

    @Transactional
    @Override
    public PagingHandler getList(long bno, PagingVO pgvo) {
        List<CommentVO> cmtList =  commentMapper.getList(bno,pgvo);
        int totalCount = commentMapper.getTotalCount(bno);
        PagingHandler ph = new PagingHandler(pgvo,totalCount,cmtList);
        return ph;
    }

 

- 매퍼로 연결할 때 파라미터가 두개이상이므로 @Param을 사용해주어야함

List<CommentVO> getList(@Param("bno") long bno,@Param("pgvo") PagingVO pgvo);

 

- 매퍼에 sql 쿼리문을 작성해주는데 시작페이지는 GetStartIndex에서 (getter) get을 생략하고 get 뒤를 소문자로해서 사용가능

    <select id="getList" resultType="com.ezen.spring.domain.CommentVO">
        select * from comment where bno = #{bno}
        order by cno desc
        limit #{pgvo.startIndex}, #{pgvo.qty}
    </select>
    <select id="getTotalCount" resultType="int">
        select count(cno) from comment where bno = #{bno}
    </select>

 

- 이제 가져온 페이지핸들러의 객체를  for문으로 댓글 하나의 객체로 빼서 화면에 원하는 방식으로 출력되도록 구현해주는 function을 만들어서 사용하면됨.

function spreadComment(bno, page = 1){
    getCommentFromServer(bno,page).then(result => {
        console.log(result);
        const ul = document.getElementById('cmtListArea');
        if( result.cmtList.length > 0 ){
            if( page == 1){
                ul.innerHTML = ""; 
            }
            for(let cvo of result.cmtList){
                let li = `<li class="list-group-item" data-cno=${cvo.cno}>`;
                li += `<div class="ms-2 me-auto">${cvo.cno}.`;
                li += `<div class="fw-bold">${cvo.writer}</div>`;
                li += `${cvo.content}`;
                li += `</div>`;
                li += `<span class="badge text-bg-primary rounded-pill">${cvo.regDate}</span>`;
                // 수정 삭제 버튼 추가   
                li += `<div class="d-grid gap-2 d-md-flex justify-content-md-end">`;
                li += `<button type="button" data-cno=${cvo.cno} class="btn btn-outline-warning btn-sm mod" data-bs-toggle="modal" data-bs-target="#myModal">%</button>`;
                li += `<button type="button" data-cno=${cvo.cno} class="btn btn-outline-danger btn-sm del">X</button>`;
                li += `</div>`;
                li += `</li>`;
                ul.innerHTML += li;           
            } 
            let moreBtn = document.getElementById('moreBtn');
            if(result.pgvo.pageNo < result.realEndPage){
                moreBtn.style.visibility = 'visible';
                moreBtn.dataset.page = page + 1 ; 
            } else{
                moreBtn.style.visibility = 'hidden';
            }
        } else{
            ul.innerHTML = `<li class="list-group-item">Comment List Empty</li>`;
        }
    })

}

 

- 댓글을 뿌려주는 메서드를 등록버튼을 눌렀을때와 detail 화면에 추가해줌.

spreadComment(bnoVal);

 

- 댓글 더보기 기능이 작동하도록 화면전체에 클릭 addEventListener를 걸고 moreBtn에 눌리면 댓글을 더 뿌려주도록 구현 

document.addEventListener('click', (e) => {

    if(e.target.id == "moreBtn"){
        let page = parseInt(e.target.dataset.page);
        spreadComment(bnoVal, page);
    }
})

 

●  댓글 삭제기능 구현

 

- 화면 전체에 애드이벤트리스너 걸어둔 부분에 클래스에 del이 포함되어있다면 closest를 이용해서 가장 가까운 li의 cno를 가져와서 삭제하게 구현

    if(e.target.classList.contains('del')){
        let cno = e.target.closest('li').dataset.cno;
        deleteCommentToServer(cno).then(result => {
            if(result >0){
                alert("삭제 성공");
                spreadComment(bnoVal);
            } else{
                alert("삭제 실패");
            }
        })

 

- cno를 받아서 해당 댓글을 삭제하는 펑션 구현

async function deleteCommentToServer(cno) {
    try{
        const url = "/comment/delete/"+ cno;
        const config = {
            method:'delete'
        }
        const resp = await fetch(url,config);
        const result = await resp.text();
        return result;

    } catch(error){
        console.log(error);
    }
}

 

- 컨트롤러에서 delete 구현해서 매퍼까지 연결해서 해당 cno의 댓글 DB에서 삭제

    @ResponseBody
    @DeleteMapping("/delete/{cno}")
    public String delete(@PathVariable("cno") long cno){

        int isOk = csv.delete(cno);

        return isOk > 0 ? "1" : "0";
    }

 

● 댓글 수정기능 구현 

 

- mod를 가지고있는 버튼을 눌렀을때 가장가까운 li를 가져와서 내가 구현해둔 화면에 cmtWriter 부분을 가져와서 innerText로 저장해주고 cmtText부분을 가져와서 벨류를 넣어 수정버튼을 누르면 댓글 작성자와 내용이 작성해둔 것과 일치할 수 있도록 구현.

if(e.target.classList.contains('mod')){

        let li = e.target.closest('li');
        
        let cmtWriter = li.querySelector('.fw-bold').innerText;
        document.getElementById('cmtWriterMod').innerHTML = cmtWriter;

        let cmtText = li.querySelector('.fw-bold').nextSibling;
        document.getElementById('cmtTextMod').value = cmtText.nodeValue;

        document.getElementById('cmtModBtn').setAttribute("data-cno", li.dataset.cno);

    }

 

- 모달창에서 수정완료버튼을 눌렀을때 댓글번호와 수정된 내용을 가져와 데이터로 구성하고 데이터를 받았을때 update가 성공하면 1 실패하면 0을보내는 메서드를 사용해서 수정성공과 실패를 알릴 수 있도록 함.

수정이 끝나면 모달창을 닫을 수 있는 효과를주고 다시 댓글을 뿌림.

 if(e.target.id == 'cmtModBtn'){
        let cmtData = {
            cno: e.target.dataset.cno,  
            content: document.getElementById('cmtTextMod').value
        }
        console.log(cmtData);
        modifyCommentToServer(cmtData).then(result => {
            if(result == '1'){
                alert("댓글 수정 성공");
            } else{
                alert("댓글 수정 실패");
            }    
            // 모달창 닫기
            document.querySelector('.btn-close').click();
            // 댓글 뿌리기
            spreadComment(bnoVal);
        })
    }

 

- cno와 수정된 내용의 데이터를 받아 update의 isOk를 텍스트형식으로 받는 메서드 구현

async function modifyCommentToServer(cmtData) {
    try {
        const url = "/comment/modify";
        const config ={
            method : "put",
            headers : {
                'Content-Type' : 'application/json; charset=utf-8'
            },
            body : JSON.stringify(cmtData)
        };
        const resp = await fetch(url,config);
        const result = await resp.text();

       return result; 

    } catch (error) {
        console.log(error);
    }
}

 

 - 컨트롤러에서 해당되는 cno와 수정된 데이터를 받은 댓글 객체를  매퍼까지 연결해서 update 해서 성공 실패를 isOk로 리턴하게 구현하면 끝

    @ResponseBody
    @PutMapping("/modify")
    public String modify(@RequestBody CommentVO cvo){

        int isOK = csv.modify(cvo);

        return isOK > 0 ? "1" : "0";
    }
    <update id="modify">
        update comment set content = #{content}, reg_date = now()
        where cno = #{cno}
    </update>

 


 시큐리티 구현

● 디펜던시 추가

	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

 

●  유저, 권환 DB생성

create table user(
email varchar(200),
pwd varchar(256),
nick_name varchar(200),
reg_date datetime default now(),
last_login datetime default now(),
primary key(email));

create table auth(
id int auto_increment,
email varchar(200),
auth varchar(50),
primary key(id),
foreign key(email) references user (email));

 

- DB구성에맞게 UserVO, AuthVO를 생성하고 매퍼까지 쭉생성

UserVO에는 유저는 권한을 여러개가질 수도 있으므로  AuthVO의 리스트를 추가.

 

● SecurityConfig를 만들어서 설정을 해주어야함.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    /* springSecurity6 => bcEncoder => createDelegationPasswordEncoder */

    @Bean
    PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    // SecurityFilterChain 객체로 설정
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        return http.csrf(csrf -> csrf.disable()).
                authorizeHttpRequests(authorize -> authorize.requestMatchers("/user/list").hasAnyRole("ADMIN")
                        .requestMatchers("/","/index","/js/**","/upload/**","/user/login","/user/register","/board/list","/comment/list").permitAll()
                        .anyRequest().authenticated())
                .formLogin(login -> login
                        .usernameParameter("email")
                        .passwordParameter("pwd")
                        .loginPage("/user/login")
                        .defaultSuccessUrl("/board/list").permitAll())
                .logout(logout -> logout
                        .logoutUrl("/user/logout")
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID")
                        .logoutSuccessUrl("/"))
                .build();
    }
    // userDetailsService : spring에서 만든 클래스와 같은 객체

    @Bean
    public UserDetailsService userDetailsService(){
        return  new CustomUserService();
    }

    // authenticationManager 객체
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    
}

 

인코딩, 유저에게 권한별 허용되는 경로, 로그인 , 로그아웃 설정 등

 

● 회원가입, 로그인 페이지를 만들고 헤더에도 추가

 

회원가입

<div layout:fragment="content" class="container-md">
    <h1>SignUp</h1>
    <hr>
    <form action="/user/register" method="post">
        <div class="mb-3">
            <label for="e" class="form-label">email</label>
            <input type="text"
                   class="form-control" id="e" name="email" placeholder="email...">
        </div>
        <div class="mb-3">
            <label for="p" class="form-label">password</label>
            <input type="text"
                   class="form-control" id="p" name="pwd" placeholder="password...">
        </div>
        <div class="mb-3">
            <label for="n" class="form-label">nickName</label>
            <input type="text"
                   class="form-control" id="n" name="nickName" placeholder="nickName...">
        </div>
        <button type="submit" class="btn btn-primary">JOIN</button>
    </form>

</div>

 

로그인

<div layout:fragment="content" class="container-md">
    <h1>SignUp</h1>
    <hr>
    <form action="/user/login" method="post">
        <div class="mb-3">
            <label for="e" class="form-label">email</label>
            <input type="text"
                   class="form-control" id="e" name="email" placeholder="email...">
        </div>
        <div class="mb-3">
            <label for="p" class="form-label">password</label>
            <input type="text"
                   class="form-control" id="p" name="pwd" placeholder="password...">
        </div>
        <button type="submit" class="btn btn-primary">Login</button>
    </form>

</div>

 

헤더에 추가되는 라인

          <li class="nav-item">
            <a class="nav-link" href="/user/register">SignUp</a>
          </li>
          <li class="nav-item">
            <a class="nav-link" href="/user/list">Login</a>
          </li>

 

- 일단 컨트롤러에서 GetMapping으로 화면에 연결까지 구현

    @GetMapping("/register")
    public void register(){}

    @GetMapping("/login")
    public String login(){
        return "/user/login";
    }

 

● 회원가입 구현

 

- 컨트롤러에서 PostMapping으로 구현해주는데 비밀번호는 인코딩을 해서 보내줘야함.

   @PostMapping("/register")
    public String register(UserVO userVO){
        userVO.setPwd(passwordEncoder.encode(userVO.getPwd()));
        int isOk = usv.register(userVO);

        log.info(" userVO >>> {} ", userVO);

        return "/index";
    }

 

- ServiceImpl 에서 매퍼로 연결해줄 때 user만 insert하지말고 권한테이블에 이메일과 권한도 insert해주도록 구현

    @Transactional
    @Override
    public int register(UserVO userVO) {
        userMapper.register(userVO);
        return userMapper.insertAuth(userVO.getEmail());
    }

 

- 매퍼에서 둘 다 insert해서 해당 아이디에 권한도 함께 넣어주어야함. 

    <insert id="register">
        insert into user(email,pwd,nick_name) values(#{email},#{pwd},#{nickName})
    </insert>

    <insert id="insertAuth">
        insert into auth(email,auth) values(#{email}, 'ROLE_USER')
    </insert>

 

● 로그인 구현

 

- UserDetailsService를 implements한 CustomUserService에서 구현해주어야하기에 일단 UserMapper를 @Autowired를 이용해서 가져와주고

@Autowired
private UserMapper userMapper;

 

- username을 넣어서 유저객체와 권한을가져오고 유저객체의 AuthList안에  set해줌

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //userName 주고 UserVO 객체를 리턴(authList 같이)
        UserVO userVO = userMapper.selectEmail(username);

        userVO.setAuthList(userMapper.selectAuths(username));

        return new AuthUser(userVO);
    }

 

-  AuthUser가 없으니까 생성해주어야함.

@Getter
public class AuthUser extends User {

    private UserVO userVO;

    public AuthUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    public AuthUser(UserVO userVO){
        super(userVO.getEmail(), userVO.getPwd(),
                userVO.getAuthList().stream().map(authVO -> new SimpleGrantedAuthority(authVO.getAuth()))
                        .collect(Collectors.toList())
                );
        this.userVO = userVO;
    }
}

 

Collection은 바로 바로 받아올 수 없으니까 stream으로 펼쳐서 map으로 변환해서 Collectors.toList로 리스트로 변환해서 받아줌.

'Spring Boot 수업 정리' 카테고리의 다른 글

Spring boot 6일차  (1) 2024.11.19
Spring Boot 4일차  (0) 2024.11.15
Spring boot 3일차  (3) 2024.11.14
Spring Boot 2일차  (1) 2024.11.13
Spring Boot 1일차  (0) 2024.11.13