1.  application-test.properties 파일 생성

# datasource 설정
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:./data/demo
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# H2 데이터베이스 방언 설정
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

 

- username, password 는 기본값으로 설정해줘도 됨

- port 는 기본값이고 path를 지정해주면 localhost:8080/h2-console 로 접속하면됨

- '인메모리 모드' 에서는 url 설정을 jdbc:h2:mem:{db이름} 으로 설정

- 'embedded mode' 에서는 url 설정을 jdbc:h2:{db가 저장될 경로} 로 설정

 

2. Spring Security 적용 중이라면 h2 database 접속시 설정이 안 되도록 코드 입력

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    @ConditionalOnProperty(name = "spring.h2.console.enabled",havingValue = "true")
    public WebSecurityCustomizer configureH2ConsoleEnable() {
        return web -> web.ignoring()
                .requestMatchers(PathRequest.toH2Console());
    }
    
}

 

- @ConditionalOnProperty: 이 애너테이션은 특정 프로퍼티가 설정된 경우에만 해당 빈을 생성. 즉, spring.h2.console.enabled가 true로 설정된 경우에만 configureH2ConsoleEnable() 메서드에서 반환한 빈이 생성됨.

 

- WebSecurityCustomizer: 이 빈은 WebSecurityCustomizer 타입의 빈을 생성하고, 이를 사용하여 특정 URL 패턴에 대해 보안 필터를 무시하도록 설정함. PathRequest.toH2Console()을 사용하여 H2 콘솔의 URL 패턴을 정의하고, 이 패턴에 대해 보안 필터를 무시하게 설정.

 

3. 웹 브라우저 창에서 localhost:8080/h2-console 접속

- properties에서 설정한 driver class, jdbc url, username, password 입력 후 test connection 클릭

- 그 후 connect 클릭하면 쿼리 작성할 수 있는 창이 뜨고, 결과확인 코드를 입력해서 RUN 클릭하면 됨!

 

* 테스트 코드 결과물은 어플리케이션이 종료되면 없어짐!

 

 

내용 참고:

https://dukcode.github.io/spring/h2-console-with-spring-security/

 

Spring Security에서 H2 Console 사용하기

Spring Security에서 H2 Console 사용하기

dukcode.github.io

https://colabear754.tistory.com/193

 

[Spring Boot] H2 DB Embedded Mode 사용하기

목차 들어가기 전에 H2는 굉장히 작고 가벼운 RDBMS로, 그 특성상 제공되는 기능은 제한적이지만 속도가 빠르고 별도의 프로그램 없이 웹브라우저 기반의 DB 콘솔을 사용할 수 있다는 장점도 있다.

colabear754.tistory.com

 


 

1. MySQL 접속:

  ✓ (terminal) root 사용자로 MySQL에 접속

mysql -u root -p

 

2. 데이터베이스 생성:

mysql> CREATE DATABASE 'DB명';

 

3. 기존 사용자 확인:

-- root 계정의 데이터베이스 중 'mysql' 이라는 데이터베이스 선택하기
mysql> use mysql;

-- 'user' 이라는 테이블의 정보에서 사용자 계정 확인하기
mysql> select host, user from user;

 

+-----------+------------------+
| host      | user             |
+-----------+------------------+
| %         | doraflower       |
| %         | springstudent    |
| localhost | mysql.infoschema |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+

 

4. 기존 사용자 삭제:

mysql> drop user 'doraflower'@'%';

 

5. 새 사용자 생성:

mysql> CREATE USER '계정ID'@'localhost' IDENTIFIED BY '비밀번호';

 

6. 사용자에게 권한 부여:

mysql> grant all privileges on DB명.* to 계정ID@localhost identified by '비밀번호';

 

7. 권한 확인:

mysql> show grants for '계정ID'@localhost;

 

8. 새로운 계정으로 접속 후 DB 설정:

// 새로만든 계정으로 접속
mysql -u {계정ID} -p

// root 계정으로 만든 DB 이전
mysql> use {DB명};

 

9. MySQL 새 커넥션 생성:

 

 

✓ MySQL Connections 옆 + 버튼 클릭해서 새로운 connection 생성!

 

 

 

 

 

 

 

 

 

내용 참고 : https://velog.io/@ejayjeon/MYSQL-1.-%EA%B3%84%EC%A0%95-%EC%83%9D%EC%84%B1-%EA%B6%8C%ED%95%9C-%EB%B6%80%EC%97%AC

 

[MYSQL] 1. 계정 생성 / 권한 부여

<small style="color: SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER

velog.io

 


MySQL Workbench에서 새 계정 만드는 방법

1. root 계정으로 접속 후 좌측 Administration 창으로 이동

2. 하단 Users and Privileges 클릭

3. 오른쪽 창 하단에 add account 클릭

4. Login name, password 설정

5. 필요시 Administration Roles 에서 역할 설정 

6. Schema Privileges - Add Entry에서 생성된 DB 추가 가능

 

 

워크벤치 사용 추천... 🙏🏻


@Lombok 

🚀 Lombok 라이브러리는 Getter/Setter, ToString 과 같은 반복적인 자바 코드를 컴파일할 때 자동으로 생성해주는 라이브러리

 

어노테이션 설명
@Getter/Setter 코드를 컴파일할 때 속성들에 대한 Getter/Setter 메소드 작성
@ToString toString() 메소드 작성
@ToString(exclude={”변수명”} 원하지 않는 속성 제외한 toString()메소드 작성
@NonNull 해당 변수가 null 체크, NullPointerException 예외 발생
@EqualsAndHashCode equals()와 hashCode() 메소드 작성
@Builder 빌더 패턴을 이용한 객체 생성
@NoArgsConstructor 파라미터가 없는 기본 생성자 생성
@AllArgsConstructor 모든 속성에 대한 생성자 생성
@RequiredArgsConstructor 초기화되지 않은 Final, @NonNull 어노테이션이 붙은 필드에 대한 생성자 생성
@Log log 변수 자동 생성
@Value 불변(immutable) 클래스 생성
@Data @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor를 합친 어노테이션

 


Entity Mapping 관련 어노테이션

어노테이션 설명
@Entity 클래스를 엔티티로 선언 (JPA에 엔티티 클래스라는 것을 알려줌)
@Table 엔티티와 매핑할 테이블을 지정
@Id 테이블의 기본키에 사용할 속성을 지정
@GeneratedValue 키 값을 생성하는 전략 명시
@Column 필드와 컬럼 매핑
@Lob BLOB, CLOB 타입 매핑
@CreationTimestamp insert 시 시간 자동 저장
@UpdateTimestamp update 시 시간 자동 저장
@Enumerated enum 타입 매핑
@Transient 해당 필드 데이터베이스 매핑 무시
@Temporal 날짜 타입 매핑
@CreateDate 엔티티가 생성되어 저장될 때 시간 자동 저장
@LastModifiedDate 조회한 엔티티의 값을 변경할 때 시간 자동 저장

 

💡 CLOB과 BLOB 의미
     CLOB 이란 사이즈가 큰 데이터를 외부 파일로 저장하기 위한 데이터 타입. 문자형 대용량 파일을 저장하는데 사용
     BLOB 이란 바이너리 데이터를 DB 외부에 저장하기 위한 타입. 이미지, 사운드, 비디오 같은 멀티미디어 데이터를 다룰 때 사용

 


@Column 속성

속성 설명 기본값
name 필드와 매핑할 컬럼의 이름 설정 객체의 필드 이름
unique(DDL) 유니크 제약 조건 설정  
insertable insert 가능 여부 true
updatable update 가능 여부 true
length String 타입의 문자 길이 제약조건 설정 255
nullable(DDL) null 값의 허용 여부 설정. false 설정 시 DDL 생성 시에 not null 제약 조건 추가  
columnDefinition 데이터베이스 컬럼 정보 직접 기술

ex.
@Column(columnDefinition = "varchar(5) default'10' not null")
 
precision, scale(DDL) BigDecimal 타입에서 사용(BigInteger 가능) precision은 소수점을 포함한 전체 자리수이고, scale은 소수점 자리수.
Double과 float 타입에는 적용되지 않음.
 

 

💡 DDL(Data Definition Language)
     테이블, 스키마, 인덱스, 뷰, 도메인을 정의, 변경, 제거할 때 사용하는 언어.
     가령, 테이블을 생성하거나 삭제하는 CREATE, DROP 등이 이에 해당

 


@GeneratedValue

🚀  Entity 클래스는 반드시 기본키를 가져야 함

생성 전략 설명
GenerationType.AUTO(default) JPA 구현체가 자동으로 생성 전략 결정
GenerationType.IDENTITY 기본키 생성을 DB에 위임
ex. MySQL 의 경우 AUTO_INCREMENT를 사용하여 기본키 생성
GenerationType.SEQUENCE DB 시퀀스 오브젝트를 이용한 기본키 생성
@SequenceGenerator를 사용하여 시퀀스 등록 필요
GenerationType.TABLE 키 생성용 테이블 사용. @TableGenerator 필요

 

 

📍   관련글    

2024.02.24 - [Database/MySQL] - [MySQL] 제약조건 | 식별자, 기본키, 복합키, 유니크, 체크

2024.02.24 - [Database/MySQL] - [MySQL] 제약조건 | 일련번호, 시퀀스, AUTO_INCREMENT

 

 

 

 

 

 

* 내용 참고 : 책 '스프링부트 쇼핑몰 프로젝트 with JPA'


1.  댓글 등록

댓글 추가는 모달창을 이용해서 처리
  ⚡️  모달창을 이용해서 새로운 댓글이 필요한 replyText나 replyWriter를 입력하도록 구성하고, 자바스크립트의 객체로 POST 호출하도록 구성

reply.js에 새로운 댓글을 등록하는 기능을 추가


  📍 파라미터를 JS의 객체로 받아서 axios.post()를 이용해서 전달해 줌

async function addReply(replyObj) {
    const response = await axios.post(`/api/replies/`, replyObj);
    return response;
}

 

  ✓  addReply()가 정상적으로 처리되면 서버에서는 '{'rno':11}'과 같은 JSON 데이터를 전송하게 됨
  ✓  이를 이용해서 댓글이 추가되면 경고창을 보여주고 마지막 페이지로 이동해서 등록된 댓글을 볼 수 있게 구성

 

read.html에는 댓글 등록을 위한 모달창을 <div layout:fragment="content">가 끝나기 전에 추가
<!-- register modal -->
<div class="modal registerModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Register Reply</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            
            <div class="modal-body">
                <div class="input-group mb-3">
                    <span class="input-group-text">Reply Text</span>
                    <input type="text" class="form-control replyText" >
                </div>
                <div class="input-group mb-3">
                    <span class="input-group-text">Reply Writer</span>
                    <input type="text" class="form-control replyWriter" >
                </div>
            </div>
        
            <div class="modal-footer">
                <button type="button" class="btn btn-primary registerBtn">Register</button>
                <button type="button" class="btn btn-outline-dark closeRegisterBtn" >Close</button>
            </div>
        </div>
    </div>
</div>
<!-- // register modal -->

 

  ✓  모달창의 class 속성값은 registerModal이라고 지정하고 <input> 태그들은 replyText와 replyWriter 속성값을 지정
  ✓  모달창의 버튼들도 구분하기 위해서 class 속성값을 registerBtn, closeRegisterBtn 등으로 지정해서 사용

 

read.html의 <script> 부분에는 자주 사용하는 DOM 객체들을 미리 변수로 처리
// 등록 작업 관련
const registerModal = new bootstrap.Modal(document.querySelector('.registerModal'));

const registerBtn = document.querySelector('.registerBtn');
const replyText = document.querySelector('.replyText');
const replyWriter = document.querySelector('.replyWriter');
const closeRegisterBtn = document.querySelector('.closeRegisterBtn');

 

ADD REPLY 버튼을 눌렀을 때 모달창을 보여주도록 이벤트 처리와 모달창의 Close 버튼에 대한 처리
document.querySelector('.addReplyBtn').addEventListener('click', function (e) {
    registerModal.show();
});

closeRegisterBtn.addEventListener('click', function (e) {
    registerModal.hide();
});

 

모달창 오른쪽 하단의 Register 버튼을 눌렀을 때 이벤트 처리를 추가

 

  📍  reply.js의 addReply()를 호출하고 경고창을 통해서 추가된 댓글의 번호를 보여줌
  📍  경고창이 닫히면 마지막 페이지를 다시 호출해서 등록된 댓글이 화면에 보일 수 있도록 구성

registerBtn.addEventListener('click', function (e) {
    // 매개변수로 사용할 객체 생성
    const replyObj = { bno: bno, replyText: replyText.value, replyWriter: replyWriter.value }
    addReply(replyObj).then(result => { // 등록이 된 후 결과처리
        alert(result.data.rno);
        registerModal.hide();
        replyText.value = '';
        replyWriter.value = '';
        printReplies(1, 10, true); // 댓글 목록 갱신
    }).catch(e => {
        alert('Exception');
    });
});


2.  댓글 페이지 번호 클릭

새로운 댓글이 추가되면 자동으로 마지막 페이지로 이동하기는 하지만, 댓글의 페이지 번호를 누를 때도 이동할 수 있으므로 수정 / 삭제 전에 페이지 이동 처리는 먼저 진행
  ⚡️  화면에서 페이지 번호를 구성하는 부분은 <li> 태그 내에 존재하는 <a> 태그이고 페이지 번호가 'data-page' 속성값으로 지정되어 있음

  ⚡️  페이지 번호는 매번 새로이 구성하므로 이벤트 처리할 때는 항상 고정되어 있는 <ul>을 대상으로 이벤트 리스너를 등록하는 방식을 이용

 

read.html에 코드 추가
// 3. 페이징 클릭
let page = 1;
let size = 10;

replyPaging.addEventListener('click', function (e) {
    e.preventDefault();
    e.stopPropagation();

    const target = e.target;
    if (!target || target.tagName !== 'A') {
        return;
    }

    page = target.getAttribute('data-page');
    printReplies(page, size);
});

 

  ✓  page와 size를 별도의 변수로 처리하는 것은 나중에 댓글 수정과 같은 작업에서 현재 페이지 번호를 유지 해야 할 가능성이 있기 때문
  ✓  이벤트 처리가 완료되면 댓글의 페이지 이동이 가능해 짐

 


3.  댓글 조회와 수정

댓글을 조회한다는 것은 댓글을 수정하거나 삭제하기 위함
댓글 조회는 등록과 유사하게 모달창을 이용해서 수정이나 삭제가 가능한 버튼들을 보여주는 형태로 구성

 

1)  Axios 통신 부분

  👾  reply.js에는 특정한 번호의 댓글을 조회하고 수정할 수 있는 기능을 구성
  👾  댓글 조회는 GET 방식으로 처리되고, 댓글 수정은 PUT 방식으로 호출

async function getReply(rno) {
    const response = await axios.get(`/api/replies/${rno}`);
    return response.data;
}

async function modifyReply(replyObj) {
    const response = await axios.put(`/api/replies/${replyObj.rno}`, replyObj);
    return response.data;
}

 

2)  read.html의 모달창 처리

  👾  댓글 수정 후에는 다시 현재 페이지를 호출해서 최대한 목록에서 수정된 댓글을 확인할 수 있도록 함

 

read.html에 등록 모달창 아래 영역을 새롭게 추가
<!-- modify modal -->
<div class="modal modifyModal" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title replyHeader"></h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
        
            <div class="modal-body">
                <div class="input-group mb-3">
                    <span class="input-group-text">Reply Text</span>
                    <input type="text" class="form-control modifyText" >
                </div>
            </div>

            <div class="modal-footer">
                <button type="button" class="btn btn-info modifyBtn">Modify</button>
                <button type="button" class="btn btn-danger removeBtn">Remove</button>
                <button type="button" class="btn btn-outline-dark closeModifyBtn">Close</button>
            </div>
        </div>
    </div>
</div>
<!-- // modify modal -->

 

  ✓  <div class="modifyModal">의 경우 class 속성값이 replyHeader 영역을 이용해서 선택한 댓글의 번호를 보여주도록 하고, modifyText 부분에는 댓글의 내용을 수정할 수 있도록 구성

 

read.html에 변수들을 추가해서 제어가 가능하도록 구성
const modifyModal = new bootstrap.Modal(document.querySelector('.modifyModal'));
const replyHeader = document.querySelector('.replyHeader');
const modifyText = document.querySelector('.modifyText');
const modifyBtn = document.querySelector('.modifyBtn');
const removeBtn = document.querySelector('.removeBtn');
const closeModifyBtn = document.querySelector('.closeModifyBtn');

 

특정한 댓글을 눌렀을 때 모달창을 보여주도록 replyList에 이벤트 처리를 추가

 

  📍  댓글 목록 역시 매번 브라우저에서 새로 만들어지기 때문에 이벤트는 항상 존재하는 바깥쪽 <ul>을 대상으로 처리

// 댓글 내용을 클릭했을 때 수정 모달창 띄우기
replyList.addEventListener('click', function (e) {
    e.preventDefault();
    e.stopPropagation();

    const target = e.target;
    if (!target || target.tagName !== 'SPAN') {
        return;
    }

    const rno = target.getAttribute('data-rno');
    if (!rno) {
        return;
    }
    
    getReply(rno).then(reply => { // 댓글의 내용을 모달창에 채워서 보여줌
        console.log(reply)
        replyHeader.innerHTML = reply.rno;
        modifyText.value = reply.replyText;
        modifyModal.show();
    }).catch(e => alert('error'));
});

 


3)  댓글 수정과 화면 갱신

  👾  댓글 수정은 화면에 있는 댓글의 내용과 번호, 게시물의 번호를 이용해서 처리할 수 있음
  👾  신경써야 할 부분은 댓글의 수정 후 처리.

         ➡️  수정된 댓글은 결국 목록에서 확인하게 되기 때문에 만일 사용자가 댓글을 수정하는 사이에 댓글이 많이 추가되면 확인할 방법이 없음

  👾  최대한 자신이 보고 있었던 페이지를 유지하는 수준으로 구현하도록 함

 

화면에서 Modify 버튼에 대한 이벤트 처리와 Close 버튼의 이벤트 처리를 작성
// 모달창의 수정 버튼 클릭
modifyBtn.addEventListener('click', function (e) {
    console.log('modifyBtn... click');
    const replyObj = {bno: bno, rno: replyHeader.innerHTML, replyText: modifyText.value};

    modifyReply(replyObj).then(result => {// 수정 처리 후
        alert(result.rno + '댓글이 수정되었습니다.');
        modifyText.value = '';
        modifyModal.hide();
        printReplies(page, size);
    }).catch(e => {
    console.log(e)
    });
});

// 모달창 close 버튼 클릭
closeModifyBtn.addEventListener('click', function () {
    modifyModal.hide();
});

 

  ✓  댓글이 수정된 후에는 다시 현재 페이지를 볼 수 있도록 printReplies()를 호출하므로 수정한 후에 현재 페이지를 다시 갱신


4)  댓글 삭제

reply.js에 삭제 처리 추가
async function removeReply(rno) {
    const response = await axios.delete(`/api/replies/${rno}`);
    return response.data;
}

 

read.html에는 모달 창의 Remove 버튼을 눌러서 댓글을 삭제

 

  📍 삭제 후에는 경고창을 통해서 댓글이 삭제되었음을 알려주고 댓글의 1페이지로 이동

/*
5. 삭제 관련
*/
removeBtn.addEventListener('click', function (e) {
    removeReply(replyHeader.innerHTML).then(result => {
        alert(result.rno + ' 댓글이 삭제되었습니다.');
        modifyText.value = '';
        modifyModal.hide();
        page = 1; // 해당 댓글이 삭제되었으니 1페이지로 이동
        printReplies(page, size);
    }).catch(e => {
    console.log(e)
    });
});

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  비동기 처리와 Axios

대부분 프로그래밍의 시작은 항상 동기화된 방식을 이용.

result1 = doA();
result2 = doB(result1);
result3 = doC(result2);


  ⚡️  흔하게 볼 수 있는 동기화된 코드인데 doA()를 실행해서 나온 결과로 result1을 이용해서 doB()를 호출하는 방식
  ⚡️  코드는 doA() -> doB() -> doC()의 순서대로 호출됨

  📍  동기화된 방식의 단점은 doA()의 실행이 완료되어야만 doB()의 실행이 가능함. 즉 doA()의 결과를 반환할 때까지 다른 작업은 실행되지 않기 때문에 동시에 여러 작업을 처리할 수없음

  📍  비동기 방식은 커피 가게에 여러 명의 점원이 있는 상황과 유사. 점원이 여러 명이면 한 명은 주문을 계속 받고, 다른 점원은 계속해서 커피를 제조할 수 있음. 
    ➡️  비동기 방식의 핵심은 '통보'. 비동기는 여러 작업을 처리하기 때문에 나중에 결과가 나오면 이를 '통보'해 주는 방식을 이용.
         이러한 방식을 전문용어로는 콜백 callback이라고 함.

  📍  비동기 방식은 'doA()'를 호출할 때 doB()를 해 줄 것을 같이 파라미터로 전달

function doA(callback) {
...
result1 = ....
callback(result1)
}


  ⚡️  파라미터로 전달되는 콜백을 내부에서 호출하는 코드

 

  📍 자바 스크립트에서 함수는 '일급 객체 first-class object'로 일반 객체와 동일한 위상을 가지고 있으므로 파라미터가 되거나 리턴타입이 될 수 있음
  📍  비동기 방식에서 콜백을 이용하는 것이 해결책이 되기는 하지만 동기화된 코드에 익숙한 개발자들에게는 조금만 단계가 많아져도 복잡한 코드를 만들어야 하는 불편함이 있음
    ➡️  자바 스크립트에서는 Promise하는 개념을 도입해서 '비동기 호출을 동기화된 방식'으로 작성할 수 있는 문법적인 장치가 있는데 Axios는 이를 활용하는 라이브러리
    ➡️  Axios를 이용하면 Ajax를 호출하는 코드를 작성할 때 마치 동기화된 방식으로 작성할 수 있어서 자바 스크립트를 기반으로 하는 프레임워크 (Angula)나 라이브러리 (React, Vue)에서 많이 사용

 

시작하기 | Axios Docs

시작하기 브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리 Axios란? Axios는 node.js와 브라우저를 위한 Promise 기반 HTTP 클라이언트 입니다. 그것은 동형 입니다(동일한 코

axios-http.com

 


2.  Axios를 위한 준비

Axios를 활용해 Ajax를 이용하기 위해서는 댓글 처리가 필요한 화면에 Axios 라이브러리를 추가해주어야 함
자바 스크립트 코드의 경우 read.html에서는 주로 이벤트 관련된 부분을 처리하도록 하고 별도의 JS 파일을 작성해서 Axios를 이용하는 통신을 처리하도록 구성

static 폴더에 있는 js 폴더에 reply.js 파일을 추가
  • read.html의 <div layout:fragment="content">가 끝나기 전에 Axios 라이브러리를 추가 하고 reply.js 파일도 같이 추가
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js">
</script>
<script src="/js/reply.js"></script>

 

read.html에 댓글과 관련된 화면 구성을 위한 코드 추가
  • 댓글 작성 버튼
  • 댓글 목록
  • 댓글 목록 페이징
<div class="row mt-3">
    <div class="col-md-12">
        <div class="my-4">
        <!-- 댓글 작성 버튼 -->
            <button class="btn btn-info addReplyBtn">ADD REPLY</button>
        </div>
        <!-- 댓글 목록 -->
            <ul class="list-group replyList">

            </ul>
        </div>
        <div class="row mt-3">
            <div class="col">
            <!-- 댓글 목록 페이징 -->
                <ul class="pagination replyPaging">

                </ul>
            </div>
    </div>
</div>

 


1)  Axios 호출해 보기

  👾  reply.js에 간단하게 Axios를 이용하는 코드를 추가. Axios를 이용할 때 async / await를 같이 이용하면 비동기 처리를 동기화된 코드처럼 작성할 수 있음
      ⚡️  async는 함수 선언 시에 사용하는 데 해당 함수가 비동기 처리를 위한 함수라는 것을 명시하기 위해서 사용하고,
            await는 async 함수 내에서 비동기 호츨하는 부분에 사용

async function get1(bno) {
    const result = await axios.get(`/api/replies/list/${bno}`);
    console.log(result);
}

 

read.html에 get1()을 호출하는 코드를 작성
<script layout:fragment="script" th:inline="javascript">
    const bno = [[${dto.bno}]];
    get1(bno);
</script>

 

브라우저에서 댓글이 많이 달린 조회 페이지로 이동

 

 

A.  비동기 함수의 반환

 

  ✓  화면에서 결과가 필요하다면 Axios의 호출 결과를 반환받아야 하기 때문에 reply.js에서 코드 수정

async function get1(bno) {
    const result = await axios.get(`/api/replies/list/${bno}`);
    //console.log(result);
    return result.data;
}
<script layout:fragment="script" th:inline="javascript">
    const bno = [[${dto.bno}]];
    console.log(get1(bno));
</script>

 


3.  댓글 처리와 자바스크립트

1)  댓글 목록 처리

  👾  가장 먼저 개발하려는 기능은 댓글 목록 기능
  👾  댓글도 페이징 처리가 가능하도록 구성

reply.js에서 개발하려는 함수의 이름은 getList()라 하고, 파라미터는 다음과 같이 결정
  • bno : 현재 게시물 번호
  • page : 페이지 번호
  • size : 페이지당 사이즈
  • goLast : 마지막 페이지 호출 여부

    ⚡️  이 중에서 goLast는 조금 특별한 용도로 사용. 댓글의 경우 한 페이지에서 모든 동작이 이루어지므로 새로운 댓글이 등록되어도 화면에는 아무런 변화가 없다는 문제가 생김
    ⚡️  또한 페이징 처리가 되면 새로 등록된 댓글이 마지막 페이지에 있기 때문에 댓글의 결과를 볼 수 있다는 문제가 생김

    📍  goLast변수를 이용해서 강제적으로 마지막 댓글 페이지를 호출하도록 함

reply.js에 getList() 함수를 작성
async function getList({bno, page, size, goLast}) {
    const result = await axios.get(`/api/replies/list/${bno}?page=${page}`, {params: {page, size}})
    return result.data
}

 

read.html에는 getList()를 호출하는 함수와 현재 페이지가 로딩되면 해당 함수를 호출하도록 작성
function printReplies(page, size, goLast) {
    getList({bno, page, size, goLast}).then(
        data => {console.log(data);}
    ).catch(e => {
        console.error();
    });
}
printReplies(1, 10); // 무조건 호출

 

  👾  결과 데이터는 dtoList로 화면에 목록(replyList)을 처리하고, 나머지 정보들로 페이지 번호를 출력

 

read.html에는 댓글 목록을 출력하는 printList()와 페이지 번호를 출력하는 printPages() 함수를 작성하고 Axios의 결과를 출력하도록 수정
const bno = [[${dto.bno}]];
// console.log(get1(bno));

function printReplies(page, size, goLast) {
    getList({bno, page, size, goLast}).then(
    data => { 
        console.log(data);
        printList(data.list); // 목록 처리
        printPages(data); // 페이지 처리
    }
    ).catch(e => {
        console.error();
    });
}
printReplies(1, 10); // 무조건 호출

const replyList = document.querySelector('.replyList'); // 댓글 목록 DOM
const replyPaging = document.querySelector('.replyPaging'); // 페이지 목록 DOM

function printList(dtoList) { // 댓글 목록 출력
    let str = '';
    if(dtoList && dtoList.length > 0) {
        for (const dto of dtoList) {
            str += ` <li class="list-group-item d-flex replyItem">
                <span class="col-2">${dto.rno}</span>
                <span class="col-6" data-rno="${dto.rno}">${dto.replyText}</span>
                <span class="col-2">${dto.replyWriter}</span>
                <span class="col-2">${dto.regDate}</span>
            </li>`;
        }
    }
    replyList.innerHTML = str;
}

function printPages(data) { // 페이지 목록 출력
    // pagination
    page = data.page;
    let pageStr = '';
    
    if(data.prev) {
        pageStr += `<li class="page-item">
            <a class="page-link" data-page="${data.start - 1}">PREV</a></li>`;
    }
    for(let i = data.start; i <= data.end; i++) {
        pageStr += `<li class="page-item ${i === data.page ? "active" : ""}">
            <a class="page-link" data-page="${i}">${i}</a></li>`;
    }
    if(data.next) {
        pageStr += `<li class="page-item">
            <a class="page-link" data-page="${data.end + 1}">NEXT</a></li>`;
    }
    // console.log(pageStr);
    replyPaging.innerHTML = pageStr;
}

 


A. @JsonFormat, @JsonIgnore


  📍 출력된 댓글의 모양을 보면 등록 시간 regDate 부분이 배열로 처리되어서 지저분해 보임. 

         ➡️  RelyDTO에 @JsonFormat을 이용해서 JSON 처리 시에 포맷팅을 지정
  📍 댓글 수정 시간 modDate의 경우 화면에서 전혀 출력할 일이 없으므로 JSON 으로 변환될 때 제외하도록 @JsonIgnore를 적용

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime regDate;

@JsonIgnore
private LocalDateTime modDate;

 


B. 마지막 페이지로 이동


  📍 댓글 페이징은 새로 글이 추가되는 상황이 발생하면 마지막으로 등록되기 때문에 확인이 어려운 문제가 있음
        ➡️  이를 처리하려면 댓글 목록 데이터의 total을 이용해서 다시 마지막 페이지를 호출해야 함

    ⚡️  현재 게시물의 댓글에 마지막 페이지를 알아낸 후, 마지막 페이지를 다시 호출하는 방식으로 동작
          ✓  마지막 페이지의 호출은 total 값과 size 값을 이용해서 마지막 페이지를 계산하고 다시 Axios로 호출하는방식

 

reply.js의 getList()는 마지막 페이지로 호출할 수 있는 goLast 변수를 추가해서 수정
async function getList({bno, page, size, goLast}){

    const result = await axios.get(`/api/replies/list/${bno}?page=${page}`, {params: {page, size}})
    if(goLast) {
        const total = result.data.total
        const lastPage = parseInt(Math.ceil(total/size))

        return getList({bno:bno, page:lastPage, size:size})
    }

    return result.data
}

 

  📍  read.html에서 처음부터 댓글의 마지막 페이지를 보고 싶다면 printReplies()를 호출할 때 true 값을 추가

    //printReplies(1, 10); // 무조건 호출
    printReplies(1, 10, true);

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]


1.  컨트롤러 계층 구현

컨트롤러 영역에서는 Swagger UI를 이용해서 테스트와 함께 필요한 기능을 개발
ReplyController는 ReplyService를 주입 받도록 설계

@RestController
@RequestMapping("/api/replies")
@Log4j2
@RequiredArgsConstructor
public class ReplyController {
    private final ReplyService replyService;

 


1)  등록 기능 확인

  👩🏻‍🚀  ReplyController의 등록 기능은 이미 개발된 코드에 JSON 처리를 위해서 추가 코드가 필요

@Operation(summary = "Replies Post", description = "POST 방식으로 댓글 등록")
@PostMapping(value = "/", consumes = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> register(@Valid @RequestBody ReplyDTO replyDTO,
    BindingResult bindingResult) throws BindException {
    
    log.info(replyDTO);

    if (bindingResult.hasErrors()) {
        throw new BindException(bindingResult);
    }

    Map<String, Long> map = new HashMap<>();
    Long rno = replyService.register(replyDTO);
    map.put("rno", rno);

    return map;
}

 

  ✓  프로젝트를 실행하고 Swagger UI를 통해서 테스트를 진행. 등록 작업에서 주의할 점은 bno가 존재하는 게시물 번호여야 함

  ✓  정상적으로 동작하면 {"rno": 6}과 같은 결과가 전송되는 것을 확인

 

에러에 대한 처리

 

  📍  @Valid는 이미 처리를 했지만 연관 관계를 가진 엔티티를 처리할 때마다 항상 문제가 되는 것은 연관된 객체의 안전성을 확보하는 것

        ➡️  예를 들어 앞선 테스트에서 bno 값을 사용할 수 없는 번호로 작성하면  다음과 같은 문제가 발생

[(conn=1337) Cannot add or update a child row: a foreign key constraint fails (`boot_ex_app_01_2405`.`reply`, CONSTRAINT `FKr1bmblqir7dalmh47ngwo7mcs` FOREIGN KEY (`board_bno`) REFERENCES `board` (`bno`))]

 

  ⚡️  서버에 기록된 로그를 보면 SQL Exception이긴 하지만, org.springframework.dao.DataIntegrityViolationException 예외가 발생. 예외가 발생한다는 것은 분명 정상적인 결과이지만 서버의 상태 코드는 500으로 '서버 내부의 오류'로 처리

  ⚡️  외부에서 Ajax로 댓글 등록 기능을 호출했을 때 500 에러가 발생한다면 호출한 측에서는 현재 서버의 문제라고 생각할 것

         ➡️  클라이언트에 서버의 문제가 아니라 데이터의 문제가 있다고 전송하기 위해서는 @RestControllerAdvice를 이용하는 CustomRestAdvice에 DataIntegrityViolationException를 만들어서 사용자에게 예외 메시지를 전송하도록 구성

 

@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleFKException(Exception ex) {
    log.error(ex);

    Map<String, String> errorsMap = new HashMap<>();
    errorsMap.put("time", "" + System.currentTimeMillis());
    errorsMap.put("msg", "constraint fails");

    return ResponseEntity.badRequest().body(errorsMap);
}

 

  ✓  추가한 handlerFKException()는 DataIntegrityViolationException이 발생하면 "constraint fails" 메시지를 클라이언트로 전송

 


2)  특정 게시물의 댓글 목록

  👩🏻‍🚀  특정한 게시물의 댓글 목록 처리는 '/api/replies/list/{bno}' 경로를 이용하도록 구성. 이때 bno는 게시물의 번호를 의미.
  👩🏻‍🚀  스프링에서는 @PathVariable이라는 어노테이션을 이용해서 호출하는 경로의 값을 직접 파라미터의 변수로 처리할 수 있는 방법을 제공

ReplyController에 메서드 추가
@Operation(summary = "Replies of Board", description = "GET 방식으로 특정 게시물의 댓글 목록")
@GetMapping(value = "/list/{bno}")
public PageResponseDTO<ReplyDTO> getList(@PathVariable("bno") Long bno, PageRequestDTO pageRequestDTO) {
    PageResponseDTO<ReplyDTO> responseDTO = replyService.getListOfBoard(bno, pageRequestDTO);
    return responseDTO;
}

 

   ✓  getList()에서 bno 값은 경로에 있는 값을 취해서 사용할 것이므로 @PathVariable을 이용하고, 페이지와 관련된 정보는 일반 쿼리 스트링을 이용
  ✓  Swagger UI로 ReplyController를 호출해 보면 PageResponseDTO가 JSON으로 처리된 결과를 볼 수 있음

 


3)  특정 댓글 조회

  👩🏻‍🚀  특정한 댓글을 조회할 때는 Reply의 rno를 경로로 이용해서 GET 방식으로 처리

ReplyController에 getReplyDTO() 메서드를 추가
@Operation(summary = "Read Reply", description = "GET 방식으로 특정 댓글 조회")
@GetMapping(value = "/{rno}")
public ReplyDTO getReplyDTO(@PathVariable("rno") Long rno) {
    ReplyDTO replyDTO = replyService.read(rno);
    return replyDTO;
}

 

  ✓ 정상적인 rno 값이 전달되면 ReplyDTO가 JSON으로 처리

 

데이터가 존재하지 않는 경우의 처리

 

  📍  getReplyDTO()와 같이 특정한 번호를 이용해서 조회할 때 문제가 되는 부분은 해당 데이터가 존재하지 않는 경우
  📍  'NoSuchElementException' 예외 전송을 위해 CustomRestAdvice에 기능을 추가

@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handleNoSuchElementException(Exception ex) {
    log.error(ex);

    Map<String, String> errorsMap = new HashMap<>();
    errorsMap.put("time", "" + System.currentTimeMillis());
    errorsMap.put("msg", "No Such Element Exception");

    return ResponseEntity.badRequest().body(errorsMap);
}


4)  특정 댓글 삭제

  👩🏻‍🚀  일반적으로 REST 방식에서 삭제 작업은 GET / POST 가 아닌 DELETE 방식을 이용해서 처리

ReplyController에 remove() 메서드를 추가
@Operation(summary = "Delete Reply", description = "DELETE 방식으로 특정 댓글 삭제")
@DeleteMapping(value = "/{rno}")
public Map<String, Long> remove(@PathVariable("rno") Long rno) {
    replyService.remove(rno);
    Map<String, Long> map = new HashMap<>();
    map.put("rno", rno);
    return map;
}

 

존재하지 않는 번호의 삭제 예외
@ExceptionHandler({NoSuchElementException.class, EmptyResultDataAccessException.class})
@ResponseStatus(HttpStatus.EXPECTATION_FAILED)
public ResponseEntity<Map<String, String>> handlerNoSuchElementException(Exception e) {

    log.error(e);

    Map<String, String> errorMap = new HashMap<>();

    errorMap.put("time", "" + System.currentTimeMillis());
    errorMap.put("msg", "No Such Element Exception");

    return ResponseEntity.badRequest().body(errorMap);
}

 


5)  특정 댓글 수정

  👩🏻‍🚀  댓글 수정은 PUT방식으로 처리

ReplyController에 modify() 메서드를 추가

 

  📍  주의할 점은 수정할 때도 등록과 마찬가지로 JSON 문자열이 전송되므로 이를 처리하도록 @RequestBody를 적용한다는 점

@Operation(summary = "Modify Reply", description = "PUT 방식으로 특정 댓글 수정")
@PutMapping(value = "/{rno}", consumes = MediaType.APPLICATION_JSON_VALUE)
public Map<String, Long> modify(@PathVariable("rno") Long rno, @RequestBody ReplyDTO replyDTO) {

    replyDTO.setRno(rno);
    replyService.modify(replyDTO);
    
    Map<String, Long> map = new HashMap<>();
    map.put("rno", rno);
    
    return map;
}

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts