Contents
7. 회원가입 및 로그인1) UserRequest.java2) UserController.java3) UserService.java8. Optional 적용1) UserRepository.java - 수정2) UserRepositoryTest.java - 수정3) BoardRepository.java - 수정4) BoardService.java - 수정5) BoardRepositoryTest.java - 수정9. Board 와 User의 관계 정리1) Board.java - 수정2) data.sql - 더미데이터 수정3) BoardRepository.java - 메서드 추가10. ORM 전략1) Board.java - 수정11. 인증 및 권한 체크1) BoardController.java - 수정2) BoardService.java - 수정3) header.mustache - 수정 (로그인 여부에 따른 버튼)4) detail.mustache - 수정 (프론트엔드에서 권한 체크)7. 회원가입 및 로그인
1) UserRequest.java
package com.example.boardv1.user;
import lombok.Data;
public class UserRequest {
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
}
@Data
public static class LoginDTO {
private String username;
private String password;
}
}2) UserController.java
package com.example.boardv1.user;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
private final HttpSession session;
// 조회인데, 예외로 post 요청
@PostMapping("/login")
public String login(UserRequest.LoginDTO reqDTO) {
// HttpSession session = req.getSession();
User sessionUser = userService.로그인(reqDTO.getUsername(), reqDTO.getPassword());\
session.setAttribute("sessionUser", sessionUser);
// http Response header에 Set-Cookie: sessionKey 저장되서 응답됨.
return "redirect:/";
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO reqDTO){
userService.회원가입(reqDTO.getUsername(), reqDTO.getPassword(), reqDTO.getEmail());
return "redirect:/login-form";
}
@GetMapping("/login-form")
public String loginForm(){
return "user/login-form";
}
@GetMapping("/join-form")
public String joinForm(){
return "user/join-form";
}
}3) UserService.java
package com.example.boardv1.user;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public void 회원가입(String username, String password, String email) {
// 1. username 중복 체크
Optional<User> optUser = userRepository.findByUsername(username);
if (optUser.isPresent()) {
throw new RuntimeException("유저네임이 중복되었습니다");
}
// 2. 비영속 객체
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
// 3. save() 호출
userRepository.save(user);
}
public User 로그인(String username, String password) {
User findUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("username을 찾을 수 없어요"));
if (!findUser.getPassword().equals(password)) {
throw new RuntimeException("패스워드가 일치하지 않아요");
}
return findUser;
}
}8. Optional 적용
1) UserRepository.java - 수정
package com.example.boardv1.user;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Repository
public class UserRepository {
// DI
private final EntityManager em;
// 회원가입할 때 insert
public User save(User user) {
em.persist(user);
return user;
}
// 로그인할때 username으로 조회해서 password 검증
public Optional<User> findByUsername(String username) {
return em.createQuery("select u from User u where u.username = :username", User.class)
.setParameter("username", username)
.getResultStream()
.findFirst();
}
public Optional<User> findById(int id) {
User findUser = em.find(User.class, id);
return Optional.ofNullable(findUser);
}
}2) UserRepositoryTest.java - 수정
package com.example.boardv1.user;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
@Import(UserRepository.class)
@DataJpaTest // EntityManger가 ioc에 등록됨
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
public void findById_test() {
int id = 5;
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("해당 아이디로 유저를 찾을 수 없어요"));
System.out.println("user : " + user);
}
@Test
public void save_fail_test() {
// given
User user = new User(); // 비영속 객체
user.setUsername("cos");
user.setPassword("1234");
user.setEmail("cos@nate.com");
// when
User findUser = userRepository.save(user); // 영속화됨
// eye
System.out.println(findUser);
}
@Test
public void save_test() {
// given
User user = new User(); // 비영속 객체
user.setUsername("love");
user.setPassword("1234");
user.setEmail("love@nate.com");
// when
User findUser = userRepository.save(user); // 영속화됨
// eye
System.out.println(findUser);
}
@Test
public void findByUsername_test() {
// given
String username = "good";
// when (ssar, 1234)
User findUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("해당 user를 찾을 수 없어요"));
// eye
System.out.println(findUser);
}
}3) BoardRepository.java - 수정
package com.example.boardv1.board;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
/**
* 하이버네이트 기술
*/
@RequiredArgsConstructor // final이 붙어 있는 모든 필드를 초기화하는 생성자를 만들어줌.
@Repository
public class BoardRepository {
private final EntityManager em;
// DI = 의존성 주입 (의존하고 있는게 IoC에 떠있어야됨)
// public BoardRepository(EntityManager em) {
// this.em = em;
// }
public Optional<Board> findById(int id) {
// select * from board_tb where id = 1;
// ResultSet rs -> Board 객체 옮기기 (Object Mapping)
// Board board = new Board();
// board.id = rs.getInt("id");
Board board = em.find(Board.class, id);
return Optional.ofNullable(board);
}
public List<Board> findAll() {
return em.createQuery("select b from Board b order by b.id desc", Board.class)
.getResultStream().toList();
}
public Board save(Board board) {
em.persist(board); // 영속화(영구히 저장하다.)
return board;
}
public void delete(Board board) {
em.remove(board);
}
}4) BoardService.java - 수정
package com.example.boardv1.board;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
public List<Board> 게시글목록() {
return boardRepository.findAll();
}
public Board 상세보기(int id) {
return boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
}
@Transactional // update, delete, insert 할때 붙이세요!!
public void 게시글수정(int id, String title, String content) {
//
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("수정할 게시글을 찾을 수 없어요"));
board.setTitle(title);
board.setContent(content);
}
// 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback)
// 트랜잭션 종료시 flush 됨.
@Transactional
public void 게시글쓰기(String title, String content) {
// 1. 비영속 객체
Board board = new Board();
board.setTitle(title);
board.setContent(content);
System.out.println("before persist " + board.getId());
// 2. persist
boardRepository.save(board);
System.out.println("after persist " + board.getId());
}
@Transactional
public void 게시글삭제(int id) {
// 영속화
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("삭제할 게시글을 찾을 수 없어요"));
boardRepository.delete(board);
} // 자동 flush
}5) BoardRepositoryTest.java - 수정
package com.example.boardv1.board;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import jakarta.persistence.EntityManager;
@Import(BoardRepository.class)
@DataJpaTest // EntityManger가 ioc에 등록됨
public class BoardRepositoryTest {
@Autowired // 어노테이션 DI 기법
private BoardRepository boardRepository;
@Autowired
private EntityManager em;
@Test
public void save_test() {
// given
Board board = new Board();
board.setTitle("title7");
board.setContent("content7");
System.out.println("===before persist");
System.out.println(board);
// when
boardRepository.save(board);
// eye (board객체가 DB데이터와 동기화 되었음.)
System.out.println("===after persist");
System.out.println(board);
}
@Test
public void findById_test() {
// given
int id = 1;
// when
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
// boardRepository.findById(1);
// eye
System.out.println(board);
}
@Test
public void findAll_test() {
// given
// when
List<Board> list = boardRepository.findAll();
// eye
for (Board board : list) {
System.out.println(board);
}
}
@Test
public void delete_test() {
// given
int id = 1;
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
// when
boardRepository.delete(board);
// eye
em.flush();
}
@Test
public void update_test() {
// given
int id = 1;
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요요"));
// when
board.setTitle("title1-update");
// eye
em.flush();
List<Board> list = boardRepository.findAll();
// eye
for (Board b : list) {
System.out.println(b);
}
}
@Test
public void findByIdV2_test() {
// given
int id = 1;
// when
boardRepository.findById(id);
em.clear();
boardRepository.findById(id);
}
}9. Board 와 User의 관계 정리
1) Board.java - 수정
package com.example.boardv1.board;
import java.sql.Timestamp;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티
*/
@NoArgsConstructor // 디폴트 생성자
@Data // getter, setter, toString
@Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성
@Table(name = "board_tb") // 테이블명 설정
public class Board { // user 1, Board N
@Id // id를 primary key로 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정
private Integer id;
private String title;
private String content;
// private Integer userId;
@ManyToOne // FK지정 / Board가 many, User가 one
private User user;
@CreationTimestamp
private Timestamp createdAt; // import 주의!! (java.sql)
}2) data.sql - 더미데이터 수정
insert into user_tb (username, password, email, created_at) values ('ssar', '1234', 'ssar@nate.com', now());
insert into user_tb (username, password, email, created_at) values ('cos', '1234', 'cos@nate.com', now());
insert into board_tb (user_id, title, content, created_at) values (1, 'title1', 'content1', now());
insert into board_tb (user_id, title, content, created_at) values (1, 'title2', 'content2', now());
insert into board_tb (user_id, title, content, created_at) values (1, 'title3', 'content3', now());
insert into board_tb (user_id, title, content, created_at) values (2, 'title4', 'content4', now());
insert into board_tb (user_id, title, content, created_at) values (2, 'title5', 'content5', now());
insert into board_tb (user_id, title, content, created_at) values (2, 'title6', 'content6', now());3) BoardRepository.java - 메서드 추가
package com.example.boardv1.board;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import com.example.boardv1.reply.Reply;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
/**
* 하이버네이트 기술
*/
@RequiredArgsConstructor // final이 붙어 있는 모든 필드를 초기화하는 생성자를 만들어줌.
@Repository
public class BoardRepository {
private final EntityManager em;
// DI = 의존성 주입 (의존하고 있는게 IoC에 떠있어야됨)
// public BoardRepository(EntityManager em) {
// this.em = em;
// }
public Optional<Board> findByIdJoinUser(int id) {
Query query = em.createQuery("select b from Board b join fetch b.user u where b.id = :id", Board.class);
query.setParameter("id", id);
try {
Board board = (Board) query.getSingleResult();
return Optional.of(board);
} catch (Exception e) {
return Optional.ofNullable(null);
}
}
public Optional<Board> findById(int id) {
// select * from board_tb where id = 1;
// ResultSet rs -> Board 객체 옮기기 (Object Mapping)
// Board board = new Board();
// board.id = rs.getInt("id");
Board board = em.find(Board.class, id);
return Optional.ofNullable(board);
}
public List<Board> findAll() {
return em.createQuery("select b from Board b order by b.id desc", Board.class)
.getResultList();
}
public Board save(Board board) {
em.persist(board); // 영속화(영구히 저장하다.)
return board;
}
public void delete(Board board) {
em.remove(board);
}
}10. ORM 전략
1) Board.java - 수정
package com.example.boardv1.board;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.CreationTimestamp;
import com.example.boardv1.reply.Reply;
import com.example.boardv1.user.User;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 데이터베이스 세상의 테이블을 자바 세상에 모델링한 결과 = 엔티티
*/
@NoArgsConstructor // 디폴트 생성자
@Getter
@Setter
@Entity // 해당 어노테이션을 보고 컴퍼넌트 스캔 후 데이터베이스에 테이블을 생성
@Table(name = "board_tb") // 테이블명 설정
public class Board { // user 1, Board N
@Id // id를 primary key로 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto increment 설정
private Integer id;
private String title;
private String content;
// private Integer userId;
@ManyToOne(fetch = FetchType.EAGER) // FK지정 / Board가 many, User가 one
private User user; // user_id = 1 (select * from user_tb where id = 1)
@CreationTimestamp
private Timestamp createdAt; // import 주의!! (java.sql)
@Override
public String toString() {
return "Board [id=" + id + ", title=" + title + ", content=" + content + ", user=" + user + ", createdAt="
+ createdAt + "]";
}
}11. 인증 및 권한 체크
1) BoardController.java - 수정
package com.example.boardv1.board;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.boardv1.reply.ReplyRequest;
import com.example.boardv1.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌
@Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점)
public class BoardController {
private final BoardService boardService;
private final HttpSession session;
// title : title=title7&content=content7 (x-www-form)
@PostMapping("/boards/save")
public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함)
// 인증(v) 권한(x)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId());
return "redirect:/";
}
// body : title=제목&content=내용
@PostMapping("/boards/{id}/update")
public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId());
return "redirect:/boards/"+id;
}
@GetMapping("/")
public String index(HttpServletRequest req){
List<Board> list = boardService.게시글목록();
req.setAttribute("models", list);
return "index";
}
@GetMapping("/boards/save-form")
public String saveForm(){
// 인증(v) 권한(x)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
return "board/save-form";
}
@GetMapping("/boards/{id}/update-form")
public String updateForm(@PathVariable("id") int id, HttpServletRequest req){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
Board board = boardService.수정폼게시글정보(id, sessionUser.getId());
req.setAttribute("model", board);
return "board/update-form";
}
@GetMapping("/boards/{id}")
public String detail(@PathVariable("id") int id, HttpServletRequest req){
User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId);
req.setAttribute("model", dto);
return "board/detail"; // mustache 파일의 경로
}
@PostMapping("/boards/{id}/delete")
public String delete(@PathVariable("id") int id){
// 인증(v) 권한(v)
// 인증
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null)
throw new RuntimeException("인증되지 않았습니다.");
boardService.게시글삭제(id, sessionUser.getId());
return "redirect:/";
}
@GetMapping("/api/boards/{id}")
public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
Integer sessionUserId = sessionUser == null ? null : sessionUser.getId();
BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId);
return dto;
}
}2) BoardService.java - 수정
package com.example.boardv1.board;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.boardv1.reply.Reply;
import com.example.boardv1.user.User;
import com.example.boardv1.user.UserRepository;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
// 책임 : 트랜잭션 관리 + DTO 만들기 + 권한 체크(DB정보가 필요하기 때문)
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
private final EntityManager em;
public List<Board> 게시글목록() {
return boardRepository.findAll();
}
public Board 수정폼게시글정보(int id, int sessionUserId) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
// 권한
if (sessionUserId != board.getUser().getId())
throw new RuntimeException("수정할 권한이 없습니다");
return board;
}
public BoardResponse.DetailDTO 상세보기(int id, Integer sessionUserId) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("게시글을 찾을 수 없어요"));
return new BoardResponse.DetailDTO(board, sessionUserId);
}
@Transactional // update, delete, insert 할때 붙이세요!!
public void 게시글수정(int id, String title, String content, int sessionUserId) {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("수정할 게시글을 찾을 수 없어요"));
// 권한
if (sessionUserId != board.getUser().getId())
throw new RuntimeException("수정할 권한이 없습니다");
board.setTitle(title);
board.setContent(content);
}
// 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback)
// 트랜잭션 종료시 flush 됨.
@Transactional
public void 게시글쓰기(String title, String content, int sessionUserId) {
User user = em.getReference(User.class, sessionUserId);
// 1. 비영속 객체
Board board = new Board();
board.setTitle(title);
board.setContent(content);
board.setUser(user);
System.out.println("before persist " + board.getId());
// 2. persist
boardRepository.save(board);
System.out.println("after persist " + board.getId());
}
@Transactional
public void 게시글삭제(int id, int sessionUserId) {
// 영속화
Board board = boardRepository.findById(id)
.orElseThrow(() -> new RuntimeException("삭제할 게시글을 찾을 수 없어요"));
// 권한
if (sessionUserId != board.getUser().getId())
throw new RuntimeException("삭제할 권한이 없습니다");
boardRepository.delete(board);
} // 자동 flush
}3) header.mustache - 수정 (로그인 여부에 따른 버튼)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
<nav class="navbar navbar-expand-sm" style="background-color: grey;">
<div class="container-fluid">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/" style="font-weight: bold; color: white">Metacoding</a>
</li>
{{#sessionUser}}
<li class="nav-item">
<a class="nav-link" href="/boards/save-form" style="color: white">글쓰기</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout" style="color: white">로그아웃</a>
</li>
{{/sessionUser}}
{{^sessionUser}}
<li class="nav-item">
<a class="nav-link" href="/join-form" style="color: white">회원가입</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login-form" style="color: white">로그인</a>
</li>
{{/sessionUser}}
</ul>
</div>
</nav>
</div>4) detail.mustache - 수정 (프론트엔드에서 권한 체크)
{{> header}}
<div class="container p-5">
{{#model.isOwner}}
<!-- 수정삭제버튼 -->
<div class="d-flex justify-content-end">
<a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a>
<form action = "/boards/{{model.id}}/delete" method="post">
<button class="btn btn-outline-secondary">삭제</button>
</form>
</div>
{{/model.isOwner}}
<!-- 게시글내용 -->
<div>
<h2><b>{{model.title}}</b></h2>
<hr />
<div class="d-flex justify-content-end">
작성자 : {{model.username}}
</div>
<div class="m-4 p-2">
{{model.content}}
</div>
</div>
<!-- 댓글 -->
<div class="card mt-3">
<!-- 댓글등록 -->
<div class="card-body">
<form action="/replies/save" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<textarea id="comment" class="form-control" rows="2" name="comment"></textarea>
<div class="d-flex justify-content-end">
<button class="btn btn-secondary mt-1">
댓글등록
</button>
</div>
</form>
</div>
<!-- 댓글목록 -->
<div class="card-footer">
<b>댓글리스트</b>
</div>
<div class="list-group">
{{#model.replies}}
<!-- 댓글아이템 -->
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex">
<div class="px-1 me-1 bg-secondary text-white rounded">{{replyUsername}}</div>
<div>{{comment}}</div>
</div>
{{#isReplyOwner}}
<form action="/replies/{{id}}/delete?boardId={{model.id}}" method="post">
<input type="hidden" name="boardId" value="{{model.id}}">
<button class="btn">🗑</button>
</form>
{{/isReplyOwner}}
</div>
{{/model.replies}}
</div>
</div>
</div>
{{^sessionUser}}
<script>
// 1. 조건 (querySelector 사용, textArea에 comment라는 id 하나 추가하기)
const textArea = document.querySelector("#comment");
// 2. textArea에 마우스로 클릭했을 때의 이벤트 리스너가 필요함
textArea.addEventListener("click", function () {
// 3. 이 때, 행위가 하나 필요함 -> 알림창을 띄워서 '로그인하세요'라고 알려줘야 함
alert("로그인하세요");
});
</script>
{{/sessionUser}}
</body>
</html>Share article