댓글 기능구현
● 댓글 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 |