<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>어리바리 신입 개발자의 얼렁뚱땅 개발 기록 ✨</title>
    <link>https://leeri-study.tistory.com/</link>
    <description>공부 어떻게 하는 거더라</description>
    <language>ko</language>
    <pubDate>Sat, 25 Jul 2026 13:34:32 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>낫쏘링</managingEditor>
    <image>
      <title>어리바리 신입 개발자의 얼렁뚱땅 개발 기록 ✨</title>
      <url>https://tistory1.daumcdn.net/tistory/6146836/attach/81ec150549f6426191696ad457526bf6</url>
      <link>https://leeri-study.tistory.com</link>
    </image>
    <item>
      <title>[ JAVA / JPA ] JPA(Java Persistence API)를 사용하는 이유</title>
      <link>https://leeri-study.tistory.com/165</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;JPA(Java&amp;nbsp;Persistence&amp;nbsp;API)&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- 자바의 ORM(Object-Relational Mapping) 표준 스펙을 정의하는 인터페이스&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;의 모음&lt;br /&gt;- ORM : 객체와 관계형 데이터베이스(RDB) 사이의 데이터 변환을 자동화하기 위한 기술&lt;br /&gt;- 즉, 자바의 객체와 RDB를 어떻게&amp;nbsp;매핑하고&amp;nbsp;동작해야하는지&amp;nbsp;정의하고&amp;nbsp;있는&amp;nbsp;것이&amp;nbsp;JPA&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;b&gt;[ 장점 ]&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- SQL문을 사용하지 않고 메서드를 통해 DB를 조작하기 때문에 개발자는 비즈니스 로직을 구성하는 것에만 집중할 수 있다.&lt;br /&gt;&amp;nbsp; (어떤 쿼리를 실행해야 하는지만 생각하면 된며, 예측도 쉽다.)&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- 부수적인&amp;nbsp;코드를&amp;nbsp;줄이고&amp;nbsp;객체에&amp;nbsp;대한&amp;nbsp;코드를&amp;nbsp;작성하기&amp;nbsp;때문에&amp;nbsp;코드의&amp;nbsp;가독성을&amp;nbsp;높일&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&amp;nbsp; (반복적인 CRUD 쿼리를 처리해준다.)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- 객체&amp;nbsp;지향적인&amp;nbsp;코드를&amp;nbsp;작성할&amp;nbsp;수&amp;nbsp;있기&amp;nbsp;때문에&amp;nbsp;생산성이&amp;nbsp;증가한다.&lt;br /&gt;- 매핑하는 정보가 class로 명시 기 때문에 ERD에 대한 의존도를 낮출수 있고, 유지보수와 리팩토링에 용이하다.&lt;br /&gt;- 만약 이전 DBMS에서 다른 DBMS로 데이터를 이전할 경우 해당 DBMS에 맞는 쿼리를 다시 작성해야 하지만, &lt;br /&gt;&amp;nbsp; ORM을 사용하면 쿼리를 사용하지 않기 때문에 유지보수와 리팩토링에 용이한 것.&lt;br /&gt;- 자바&amp;nbsp;각체에는&amp;nbsp;상속관계가&amp;nbsp;존재하는데&amp;nbsp;데이터베이스에는&amp;nbsp;상속관계가&amp;nbsp;존재하지&amp;nbsp;않는&amp;nbsp;문제를&amp;nbsp;해결해준다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 단점 ]&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;- 설계가&amp;nbsp;잘못됐을&amp;nbsp;경우에&amp;nbsp;속도&amp;nbsp;저하와&amp;nbsp;일관성을&amp;nbsp;무너뜨리는&amp;nbsp;등의&amp;nbsp;문제가&amp;nbsp;발생할&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;- 복잡하고 무거운 쿼리는 별도의 최적화를 필요로 하기 때문에 결국 쿼리를 사용해야할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #7e98b1;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 구현체 ]&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;- JPA는 인터페이스 모음이기 때문에 구현할 구현체가 필요하다.&lt;br /&gt;- EclipseLink&amp;nbsp;&lt;br /&gt;- Hibernate &lt;br /&gt;- DataNucleus&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Back - end/JAVA</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/165</guid>
      <comments>https://leeri-study.tistory.com/165#entry165comment</comments>
      <pubDate>Mon, 16 Oct 2023 20:27:04 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful ]  RESTful한 게시판 만들기10 / 첨부파일 존재하면 제목에 클립 이미지 보이기, 제목 기본값 넣기</title>
      <link>https://leeri-study.tistory.com/160</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 첨부파일의 존재여부를 확인하기 위한 방법 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;1. 이미 구현해둔 첨부파일 정보 받아오는 쿼리를 그대로 사용한다. &lt;br /&gt;&amp;nbsp; &amp;nbsp; (파일과 관련된 모든 정보를 가져오기 때문에 효율적이지 않은 것 같다.)&lt;br /&gt;2. dto와 mapper에 첨부파일의 존재여부를 확인하는 새로운 필드와 쿼리를 생성한다.&lt;br /&gt;3. 기존 게시글 목록 조회 쿼리에 첨부파일의 존재여부를 확인하는 쿼리를 추가한다.&lt;br /&gt;&lt;br /&gt;- 새로 생성하거나 추가한다면 COUNT와 EXISTS 중 무슨 쿼리를 사용할 것인가?&lt;br /&gt;&amp;nbsp; COUNT는 데이터의 수를 EXISTS는 데이터의 존재여부를 확인한다.&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&amp;nbsp; &amp;nbsp;EXISTS를 사용해 파일의 여부만 체크하기로 함(존재하면 1 존재하지않으면 0 반환)&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&amp;nbsp; &lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;어차피 파일 여부를 확인하는 쿼리는 게시글 조회시에만 필요할 것 같아서 기존 쿼리에 추가하기로 함&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;이미지는 미리 파일에 담아둔다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 1. 쿼와 BoardDto 수정]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1695109069833&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;selectBoardList&quot; parameterType=&quot;PagingVo&quot; resultType=&quot;BoardDto&quot;&amp;gt;
    /* 게시글 목록 조회 */
    &amp;lt;![CDATA[
        SELECT
            board_idx,
            title,
            CASE
                WHEN EXISTS(
                    SELECT 
                        1 
                    FROM 
                        t_file
                    WHERE
                       board_idx = t_board.board_idx
                    AND
                       deleted_yn = 'N'
                )THEN 1
                ELSE 0
            END AS is_file,
            contents,
            hit_cnt,
            creator_id,
            created_datetime
        FROM
            t_board
        WHERE
            deleted_yn = 'N'
        ORDER BY board_idx DESC		
    ]]&amp;gt;		
    &amp;lt;if test=&quot;startIndex != null and startIndex &amp;gt; -1 &quot;&amp;gt;
        LIMIT #{startIndex},#{rowPerPage};
    &amp;lt;/if&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1695109147890&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// BoardDto
private int isFile;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #456771;&quot;&gt; 2&lt;/span&gt;. BoardList.html 수정 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1695040856254&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 기존 html --&amp;gt;
&amp;lt;td class=&quot;title&quot;&amp;gt;
    &amp;lt;a th:href=&quot;'/board/' + ${list.boardIdx}&quot;  th:text=&quot;${list.title}&quot;&amp;gt;&amp;lt;/a&amp;gt;
&amp;lt;/td&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1695040894665&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 수정 후 html 제목 출력하는 a태그 뒤에 img 태그 추가해주면 된다. --&amp;gt;
&amp;lt;td class=&quot;title&quot;&amp;gt;
    &amp;lt;a th:href=&quot;'/board/' + ${list.boardIdx}&quot;  th:text=&quot;${list.title}&quot;&amp;gt;&amp;lt;/a&amp;gt;
    &amp;lt;img th:if=&quot;${list.isFile} == 1&quot; src=&quot;/img/clip.png&quot; class=&quot;clip_img&quot;&amp;gt;
&amp;lt;/td&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2h0Ue/btsugR8htII/DA4kkAF7NXeaNtwOI4rBR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2h0Ue/btsugR8htII/DA4kkAF7NXeaNtwOI4rBR0/img.png&quot; data-alt=&quot;첨부파일 표시 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2h0Ue/btsugR8htII/DA4kkAF7NXeaNtwOI4rBR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2h0Ue%2FbtsugR8htII%2FDA4kkAF7NXeaNtwOI4rBR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;90&quot; data-origin-width=&quot;484&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;첨부파일 표시 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;게시글 작성할 때 제목을 안 쓰고 등록하니 제목이 텅 비어서 클릭이 안된다! - 삭제도 못 함...&lt;br /&gt;결국 데이터 베이스에 직접 접근해서 지워줬다.&lt;br /&gt;not null로 설정해둬도 공백이나 빈 문자열이 인식돼서 db에 등록이 되는 듯 하다.&lt;br /&gt;처음에는 유효성 검사를 할까 했는데 그냥 데이터베이스 자체에 기본 값을 넣어주기로 했다.&lt;br /&gt;
&lt;pre id=&quot;code_1695041792248&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ALTER TABLE t_board ALTER COLUMN title SET DEFAULT '[제목 없음]';​&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;그런데 여전히 제목이 텅텅 비어있다. &lt;br /&gt;당연하다. 빈 문자열이 값으로 인식돼서 not null도 안통하는데 default가 통할리가.....&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;그래서 자바스크립트를 통해 빈 문자열이거나 공백이면 제목없음으로 세팅해서 서버로 전달하도록 했다.&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;굳이 value에 .trim()을 해주는 이유 공백일 경우 length가 0이 아니기 때문에 공백을 잘라주고 비교해준다.&lt;br /&gt;
&lt;pre id=&quot;code_1695045260594&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;insertBtn.addEventListener('click', e =&amp;gt; {
        let title = document.querySelector('#title');
        let titleValue = title.value;
        title.value = titleValue.trim().length === 0 ? '제목 없음' : title;
        
        ... 기존에 있던 첨부파일 크기 체크 로직 ...
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;수정 버튼 이벤트핸들러에도 똑같이 추가해준다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1695118827005&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;modifyBtn.addEventListener('click', () =&amp;gt; {
    let title = document.querySelector('#title');
    let titleValue = title.value;
    title.value = titleValue.trim().length === 0 ? '제목 없음' : title;
            ... 기존에 있던 첨부파일 크기 체크 로직 ...
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;null 상태일 경우 데이터베이스에서 기본 값으로 제목 없음 넣어줄거기 때문에 굳이 null 비교는 안했다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kYw3P/btsuCJ3lCPu/5PiuJcfhH0Y1cjV4FqfkgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kYw3P/btsuCJ3lCPu/5PiuJcfhH0Y1cjV4FqfkgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kYw3P/btsuCJ3lCPu/5PiuJcfhH0Y1cjV4FqfkgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkYw3P%2FbtsuCJ3lCPu%2F5PiuJcfhH0Y1cjV4FqfkgK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;159&quot; data-origin-width=&quot;1238&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/160</guid>
      <comments>https://leeri-study.tistory.com/160#entry160comment</comments>
      <pubDate>Thu, 14 Sep 2023 01:02:47 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기9 / 게시글 목록에 페이징 추가하기 (DTO와</title>
      <link>https://leeri-study.tistory.com/159</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;[ DTO VS VO]&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;- DTO (Data Transfer Object) / 가변 객체&lt;/b&gt;&lt;br /&gt;&amp;nbsp; 데이터를 계층간 교환하기 위해 사용한다. 즉, 로직을 가지지 않는 순수하게 데이터를 담고 전달하는 객체이다. &lt;br /&gt;&amp;nbsp; 때문에 getter, setter, toString 정도의 메서드만 갖고 있다.&lt;br /&gt;&lt;b&gt;- VO (Value Object) / 불변 객체&lt;/b&gt;&lt;br /&gt;&amp;nbsp; Read-Only의 특징을 지닌다. 즉, 데이터를 전달만 할 뿐 setter가 존재하지 않는다.&amp;nbsp;&lt;br /&gt;&amp;nbsp; 불변 객체라는 게 무슨 뜻이지...? 값이 바뀌지 않고 오직 Read만 한다고..?&lt;br /&gt;&amp;nbsp; 값이 바뀌지 않는다면 그냥 상수를 말하는 거 아닌가? &lt;br /&gt;&amp;nbsp; 상수를 쓸거면 굳이 VO를 쓸 이유가 있나? 라는 생각을했는데 &lt;br /&gt;&amp;nbsp; VO는 setter가 아니라 반듯이 생성자를 생성해서 사용한다는 점에서 불변 객체라는거였다.&lt;br /&gt;&amp;nbsp; 이게 무슨 뜻이냐면 아래 처럼 처음 사용할 때 생성자를 통해서 값을 넣어주면 &lt;br /&gt;&amp;nbsp; 그 이후로 setter를 사용할 수 없기 때문에 처음 생성한 그 상태에서 불변한 객체라는 뜻이다.&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1695027906262&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PagingVo paging = new PagingVo(1,3);​&lt;/code&gt;&lt;/pre&gt;
&amp;nbsp; 만약 새로운 값이 필요하면 또 아래처럼 새롭게 생성자를 생성해서 새로운 객체를 생성해야 한다.&lt;br /&gt;
&lt;pre id=&quot;code_1695028155295&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PagingVo paiging2 = new PagingVo(4,9);​&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;지금까지는 DTO를 사용해서 데이터를 주고 받았다.&lt;br /&gt;페이징을 구현하기 위해 VO를 사용해 볼 생각이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 1. 게시글 개수 조회하기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;
&lt;pre id=&quot;code_1694953888264&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;selectCount&quot; resultType=&quot;int&quot;&amp;gt;
    /* 게시글 개수 조회 */
    &amp;lt;![CDATA[
        SELECT
            COUNT(*)
        FROM
            t_board
        WHERE
            deleted_yn = 'N'
    ]]&amp;gt;        
&amp;lt;/select&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694974962411&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 게시글 개수 조회 mapper */
int selectCount() throws Exception;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 2. PagingVo 생성 ]&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;
&lt;pre id=&quot;code_1694974522789&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@ToString
public class PagingVo {
	private int rowsCount;
	private int currentPage;
	private int totalPage;
	private int startPage;
	private int endPage;
	private int startIndex;
	private int rowPerPage = 10;
	public static final int PAGE_NUM = 10;
	
	/** 생성자 메서드*/
	public PagingVo (int currentPage, int rowsCount) throws Exception{
		this.currentPage = currentPage;
		
		// 몇 번째 행 부터 조회할 것인지 계산
		startIndex = (currentPage-1)*rowPerPage;
		
		// 전체 게시글 개수를 한 페이지에 보여질 행의 개수로 나누어서 페이징의 가장 마지막 숫자(총 페이징 수)를 계산한다.
		// double로 계산 후에 소숫점을 올림 처리하기 위해 Math.ceil 사용 후 int로 형변환
		totalPage = (int) Math.ceil((double)rowsCount / rowPerPage);

		// 페이징의 가장 처음 번호(처음에는 당연히 1 -&amp;gt; currentPage가 바뀔 때마다 바뀔 예정)
		startPage = 1;

		// 화면에 보이는 마지막 페이징 숫자(currentPage가 바뀔 때마다 바뀔 예정)
		// rowPerPage는 한 페이지에 보일 행의 개수 / endPage는 한 페이지에 보일 페이징의 마지막 번호
		endPage = (totalPage &amp;lt; PAGE_NUM)? totalPage : PAGE_NUM;
		
		// currentPage가 바뀔 때마다 startPge와 endPage가 바뀐다.
		// 페이징이 처음에 1~10이기 때문에 currentPage가 6이되는 순간 startPager가 2로, endPage가 11로 바뀐다.
		// totalPage에도 조건을 거는 이유는 만약 게시글이 적어서 totalPage가 10보다 적을 경우
		// 페이징이 시작번호와 끝 번호가 바뀔 이유가 없기 때문
		if(currentPage &amp;gt;= 6 &amp;amp;&amp;amp; totalPage &amp;gt; 10) {
	    	startPage = currentPage - 5;
	    	endPage = currentPage + 4;
	        // 현재 페이지에 보이는 페이징 번호가 마지막 페이징 숫자보다 같거나 클 경우
	        // 더 이상 시작 번호와 끝 번호가 바뀌지 않도록 한다.
	        if(endPage &amp;gt;= totalPage) {
	        	startPage = totalPage - 9;
	        	endPage = totalPage;
	        }
	    }
	}
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 3. BoardPagingDto 생성 ]&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;
&lt;pre id=&quot;code_1694974626159&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
@ToString
@AllArgsConstructor
public class BoardPagingDto {
	private List&amp;lt;BoardDto&amp;gt; boardList;
    private PagingVo paging;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 4. 게시글 조회 쿼리 수정 ]&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694959164902&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;selectBoardList&quot; resultType=&quot;BoardDto&quot;&amp;gt;
    /* 기존 게시글 내역 조회 */
    &amp;lt;![CDATA[
        SELECT
            board_idx,
            title,
            hit_cnt,
            created_datetime
        FROM
            t_board
        WHERE
            deleted_yn = 'N'
        ORDER BY board_idx DESC		
    ]]&amp;gt;		
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;pre id=&quot;code_1694959141979&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;selectBoardList&quot; parameterType=&quot;PagingVo&quot; resultType=&quot;BoardDto&quot;&amp;gt;
    /* 수정 후 게시글 내역 조회 */
    &amp;lt;![CDATA[
        SELECT
            board_idx,
            title,
            hit_cnt,
            created_datetime
        FROM
            t_board
        WHERE
            deleted_yn = 'N'
        ORDER BY board_idx DESC		
    ]]&amp;gt;		
    &amp;lt;if test=&quot;startIndex != null and startIndex &amp;gt; -1 &quot;&amp;gt;
        LIMIT #{startIndex},#{rowPerPage};
    &amp;lt;/if&amp;gt;
&amp;lt;/select&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694974989820&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 게시글 내역 조회 mapper 매개변수 수정 */
List&amp;lt;BoardDto&amp;gt; selectBoardList(PagingVo paging) throws Exception;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 5. BoardServie 추가 및 수정 ]&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;
&lt;pre id=&quot;code_1694974801241&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 페이징 */
public PagingVo selectPaging(int currentPage) throws Exception{
     int rowsCount = boardMapper.selectCount();	
     PagingVo paging = new PagingVo(currentPage, rowsCount);

     return paging;		
}

/** 게시글 내역 조회 */
public BoardPagingDto selectBoardList(int currentPage) throws Exception{
    PagingVo paging = selectPaging(currentPage);
    List&amp;lt;BoardDto&amp;gt; boardList = boardMapper.selectBoardList(paging);		
    BoardPagingDto boardPaging = new BoardPagingDto(boardList, paging);		

    return boardPaging;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 6. BoardController 수정 ]&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694974905852&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 기존 게시판 목록 조회 화면 */
@GetMapping(&quot;/board&quot;)
public String openBoardList(Model model) throws Exception{
    BoardDto boardList = boardService.selectBoardList();

    model.addAttribute(&quot;title&quot;,&quot;게시판 목록&quot;);
    model.addAttribute(&quot;boardList&quot;, boardList);

    return &quot;/board/restBoardList&quot;;		
}&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;pre id=&quot;code_1694974846002&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 수정 후 게시판 목록 조회 화면 */
@GetMapping(&quot;/board&quot;)
public String openBoardList(@RequestParam(value=&quot;currentPage&quot;, required = false ,defaultValue = &quot;1&quot;) int currentPage, 
                             Model model) throws Exception{
    BoardPagingDto boardPaging = boardService.selectBoardList(currentPage);

    model.addAttribute(&quot;title&quot;,&quot;게시판 목록&quot;);
    model.addAttribute(&quot;boardPaging&quot;, boardPaging);

    return &quot;/board/restBoardList&quot;;		
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 7. html 수정 ]&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;
&lt;pre id=&quot;code_1694975025554&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 페이징 구현 태그 추가 --&amp;gt;
&amp;lt;!-- 가장 첫 페이지거나 마지막 페이지일 경우 혹은 현재 페이지일 경우 label 태그를 그린다. --&amp;gt;
&amp;lt;!-- 그 외의 경우 a 태그를 그린다. --&amp;gt;
&amp;lt;tfoot&amp;gt;
     &amp;lt;tr&amp;gt;
        &amp;lt;td colspan=&quot;4&quot; class=&quot;text-center&quot; th:with=&quot;paging=${boardPaging.paging}&quot;&amp;gt;						
            &amp;lt;a class=&quot;btn&quot; th:if=&quot;${paging.currentPage&amp;gt;1}&quot; th:href=&quot;@{/board}&quot;&amp;gt;처음으로&amp;lt;/a&amp;gt;
            &amp;lt;label th:unless=&quot;${paging.currentPage&amp;gt;1}&quot;&amp;gt;처음으로&amp;lt;/label&amp;gt;
            &amp;lt;a class=&quot;btn&quot; th:if=&quot;${paging.currentPage&amp;gt;1}&quot; th:href=&quot;@{/board(currentPage=${paging.currentPage-1})}&quot;&amp;gt;이전&amp;lt;/a&amp;gt;
            &amp;lt;label th:unless=&quot;${paging.currentPage&amp;gt;1}&quot;&amp;gt;이전&amp;lt;/label&amp;gt;
                &amp;lt;th:block th:each=&quot;num : ${#numbers.sequence(paging.startPage,paging.endPage)}&quot; &amp;gt;
                    &amp;lt;a class=&quot;btn &quot; th:if=&quot;${paging.currentPage != num}&quot; th:href=&quot;@{/board(currentPage=${num})}&quot; th:text=&quot;${num}&quot;&amp;gt;&amp;lt;/a&amp;gt;
                    &amp;lt;label class=&quot;current-page-btn&quot; th:if=&quot;${paging.currentPage == num}&quot; th:text=&quot;${num}&quot;&amp;gt;&amp;lt;/label&amp;gt;
                &amp;lt;/th:block&amp;gt;
            &amp;lt;a class=&quot;btn&quot; th:if=&quot;${paging.currentPage&amp;lt;paging.totalPage}&quot; th:href=&quot;@{/board(currentPage=${paging.currentPage+1})}&quot;&amp;gt;다음&amp;lt;/a&amp;gt;
            &amp;lt;label th:unless=&quot;${paging.currentPage&amp;lt;paging.totalPage}&quot;&amp;gt;다음&amp;lt;/label&amp;gt;
            &amp;lt;a class=&quot;btn&quot; th:if=&quot;${paging.currentPage&amp;lt;paging.totalPage}&quot; th:href=@{/board(currentPage=${paging.totalPage})}&amp;gt;마지막으로&amp;lt;/a&amp;gt;
            &amp;lt;label th:unless=&quot;${paging.currentPage&amp;lt;paging.totalPage}&quot;&amp;gt;마지막으로&amp;lt;/label&amp;gt;
        &amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/tfoot&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694975444774&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!--tbody에서 ${#lists.size(boardList)}를 ${#lists.size(boardPaging.boardList)}로 수정 --&amp;gt;
&amp;lt;!--서버에서 받아오는 데이터의 형태가 바뀌었기 때문에 boardList 앞에 boardPaging. 추가해줘야한다. --&amp;gt;
&amp;lt;tbody&amp;gt;
    &amp;lt;!-- th:if - 타임리프 문법을 사용 할 경우에 쓰는 if 문 --&amp;gt;
    &amp;lt;!-- if문 내의 조건이 일치해야만 해당 tr이 생성된다. --&amp;gt;
    &amp;lt;!-- th:each - 타임리프 문법을 사용할 경우에 쓰는 반복문 ( ${list}에 들어있는 값을 모두 순회하면서 tr 태그를 생성한다. )--&amp;gt;
    &amp;lt;tr th:if=&quot;${#lists.size(boardPaging.boardList)} &amp;gt; 0&quot; th:each=&quot;list : ${boardPaging.boardList}&quot;&amp;gt;
        &amp;lt;td th:text=&quot;${list.boardIdx}&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;!-- th:href -  타임리프 문법을 사용할 경우에 쓰는 href 속성 --&amp;gt;
        &amp;lt;!-- href=&quot;/board/&quot; th:attrappend=&quot;href=${list.boardIdx}&quot; 로도 작성 가능하다. --&amp;gt;
        &amp;lt;td class=&quot;title&quot;&amp;gt;&amp;lt;a th:href=&quot;'/board/' + ${list.boardIdx}&quot;  th:text=&quot;${list.title}&quot;&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;td th:text=&quot;${list.hitCnt}&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;td th:text=&quot;${list.createdDatetime}&quot;&amp;gt;&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt; 
    &amp;lt;!-- th:unless - 타임리프 문법을 사용할 경우에 쓴는 if 문 / 단, if와 반대로 조건이 일치하지 않을 경우 tr 생성 --&amp;gt;
    &amp;lt;tr th:unless=&quot;${#lists.size(boardPaging.boardList)} &amp;gt; 0&quot;&amp;gt;
        &amp;lt;td colspan=&quot;4&quot;&amp;gt;조회된 결과가 없습니다.&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/tbody&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;페이징이 구현 됐다. 데이터를 많이 넣지 않아서 페이징이 10까지 나오지 않고 4까지만 나온다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2642&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLBQ2w/btsuRG4LzM2/yqqK2jkKDVNfDylJ4dao61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLBQ2w/btsuRG4LzM2/yqqK2jkKDVNfDylJ4dao61/img.png&quot; data-alt=&quot;currentPage가 마지막인 상태에서 다음,마지막으로 버튼이 label태그로 변한 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLBQ2w/btsuRG4LzM2/yqqK2jkKDVNfDylJ4dao61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLBQ2w%2FbtsuRG4LzM2%2FyqqK2jkKDVNfDylJ4dao61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2642&quot; height=&quot;948&quot; data-origin-width=&quot;2642&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;currentPage가 마지막인 상태에서 다음,마지막으로 버튼이 label태그로 변한 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2616&quot; data-origin-height=&quot;1533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xGo6y/btsuCGyNiVm/CIPnpJgXwGoWxR0CPdKEW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xGo6y/btsuCGyNiVm/CIPnpJgXwGoWxR0CPdKEW1/img.png&quot; data-alt=&quot;다른 페이지를 클릭할 경우 처음으로와 이전버튼이 a태그로 바뀌고 활성화된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xGo6y/btsuCGyNiVm/CIPnpJgXwGoWxR0CPdKEW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxGo6y%2FbtsuCGyNiVm%2FCIPnpJgXwGoWxR0CPdKEW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2616&quot; height=&quot;1533&quot; data-origin-width=&quot;2616&quot; data-origin-height=&quot;1533&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다른 페이지를 클릭할 경우 처음으로와 이전버튼이 a태그로 바뀌고 활성화된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2619&quot; data-origin-height=&quot;237&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vWJfQ/btsuSiW4bVg/udf76VkOfDMfg3bxc0W4uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vWJfQ/btsuSiW4bVg/udf76VkOfDMfg3bxc0W4uK/img.png&quot; data-alt=&quot;7을 누르면 페이징 범위가 2에서 11로 바뀐다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vWJfQ/btsuSiW4bVg/udf76VkOfDMfg3bxc0W4uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvWJfQ%2FbtsuSiW4bVg%2Fudf76VkOfDMfg3bxc0W4uK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2619&quot; height=&quot;237&quot; data-origin-width=&quot;2619&quot; data-origin-height=&quot;237&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;7을 누르면 페이징 범위가 2에서 11로 바뀐다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/159</guid>
      <comments>https://leeri-study.tistory.com/159#entry159comment</comments>
      <pubDate>Tue, 12 Sep 2023 17:24:24 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기8 / 첨부파일 삭제 및 수정 구현하기</title>
      <link>https://leeri-study.tistory.com/158</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;먼저 이미 첨부되어 있는 파일을 글 수정 과정에서 삭제하기 위해서&lt;br /&gt;html과 자바스크립트를 이용해서 화면과 이벤트 핸들러를 구현해야 한다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 현재 상태 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;- 첨부 파일이 있다면 첨부파일 목록이 나타나게만 해둔 상태&lt;br /&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1155&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGeXS/btst6Nrlabq/a5rLykbZK3qK5GTKZwr2gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGeXS/btst6Nrlabq/a5rLykbZK3qK5GTKZwr2gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGeXS/btst6Nrlabq/a5rLykbZK3qK5GTKZwr2gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGeXS%2Fbtst6Nrlabq%2Fa5rLykbZK3qK5GTKZwr2gk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;240&quot; data-origin-width=&quot;1155&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre id=&quot;code_1694767185122&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;file_list&quot; th:if=&quot;${board.fileList != null and board.fileList.size() &amp;gt; 0}&quot;&amp;gt;
	[ 첨부파일 ]
	&amp;lt;p th:each=&quot;list : ${board.fileList}&quot; th:text=&quot;|${list.originalFileName} (${list.fileSize} kb)|&quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;게시글 수정에서 필요한 로직&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;]&lt;/span&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;1. 게시글 수정 (이미 게시글 수정 구현 과정에서 구현되어있음)&lt;br /&gt;2. 이전에 첨부되어있던 파일 삭제(할 수도 있고 안 할 수도 있다.)&amp;nbsp; &amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- 첨부파일이 존재한다면 옆에 파일 삭제 버튼도 같이 보여야 한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- 파일 삭제 버튼을 누른다고 해서 바로 삭제되는 것은 아니다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- 파일 삭제 버튼을 누르면 파일 삭제 버튼이 삭제 취소 버튼으로 바뀐다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- 삭제 취소 버튼을 누르면 다시 파일 삭제 버튼으로 바뀐다.&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;- 최종적으로 수정 완료 버튼을 눌렀을 때 파일 삭제 버튼이 눌린 파일만 완전히 삭제된다.&lt;br /&gt;3.새롭게 첨부한 파일 등록(할 수도 있고 안 할 수도 있다.)&amp;nbsp; (이미 첨부파일 업로드 구현 과정에서 구현되어있음)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 1. BoardController 수정 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694949016350&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 기존 게시글 수정 처리 */
@PutMapping(&quot;/board/{boardIdx}&quot;)
public String modifyBoard(@PathVariable(&quot;boardIdx&quot;) int boardIdx, BoardDto board) throws Exception{
    boardService.modifyBoard(board);

    return &quot;redirect:/board/&quot; + boardIdx;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;pre id=&quot;code_1694948886917&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
*수정 후 게시글 수정 처리 
* files는 새롭게 첨부할 파일을 등록할 때 필요하다.
* 이미 첨부되었는 파일을 삭제할 때 필요한 데이터는 이미 board에 들어있음 (경로)
*/
@PutMapping(&quot;/board/{boardIdx}&quot;)
public String modifyBoard(@PathVariable(&quot;boardIdx&quot;) int boardIdx, 
                          @ModelAttribute BoardDto board,
                          @RequestParam(&quot;files&quot;) List&amp;lt;MultipartFile&amp;gt; files) throws Exception{
    boardService.modifyBoard(board, files);

    return &quot;redirect:/board/&quot; + boardIdx;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 2. BoardService 수정 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694949190840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 기존 게시글 수정 */
public void modifyBoard(BoardDto board) throws Exception{
    boardMapper.modifyBoard(board);
}​&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;
&lt;pre id=&quot;code_1694949168729&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 수정 후 게시글 수정 */
public void modifyBoard(BoardDto board, List&amp;lt;MultipartFile&amp;gt; files) throws Exception{
    // 게시글 수정
    boardMapper.modifyBoard(board);

    // 새로운 첨부파일 등록
    boardFileService.insertFile(board, files);

    // 기존 첨부파일 삭제
    boardFileService.deleteFile(board);	    
}​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 3. BoardFileService 추가 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694949714023&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 첨부파일 삭제 */
public void deleteFile(BoardDto board)throws Exception{
    List&amp;lt;BoardFileDto&amp;gt; fileList = board.getFileList();

	// fileList가 null이거나 비어있을 경우 종료(이미 첨부되어 있던 파일 삭제를 안할 경우)
    if(fileList == null || fileList.isEmpty()) {
        return;
    }

    // 서버 컴퓨터에서 삭제
    for(BoardFileDto oneFile : fileList) {
        String path = oneFile.getStoredFilePath();
        File file = new File(path);

        if(!file.exists()) {
            log.info(&quot;존재하지 않는 파일&quot;);
            return;
        }

        if(file.delete()) {
            log.info(&quot;삭제 성공&quot;,oneFile.getOriginalFileName());
        }else {
            log.info(&quot;삭제 실패&quot;,oneFile.getOriginalFileName());
        }

    }

    // 첨부파일 번호 리스트로 넣어주기
    List&amp;lt;Integer&amp;gt; idxs =new ArrayList&amp;lt;Integer&amp;gt;();
    for(BoardFileDto oneFile : fileList) {	
        idxs.add(oneFile.getIdx());
    }
    // 데이터베이스에서 파일 정보 상태 삭제로 바꾸기
    boardMapper.deleteFile(idxs);
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 4. 쿼리 추가 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694949758730&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;update id=&quot;deleteFile&quot; parameterType =&quot;int&quot;&amp;gt;
    /* 파일 삭제 상태로 변경 */
    &amp;lt;![CDATA[
        UPDATE
            t_file
        SET
            deleted_yn = 'Y',
            updator_id = 'admin',
            updated_datetime = Now()
        WHERE
            idx IN (
    ]]&amp;gt;
        &amp;lt;foreach collection=&quot;list&quot; item=&quot;item&quot; separator=&quot;,&quot;&amp;gt;
            #{item}
        &amp;lt;/foreach&amp;gt;
        )
&amp;lt;/update&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 5. html 수정 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694948477349&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form id=&quot;frm&quot; method=&quot;post&quot; th:action=&quot;'/board/'+ ${boardIdx}&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
	
    .. 테이블(formdata) ..
    
    // 메서드 put으로 설정
    &amp;lt;input type=&quot;hidden&quot; id=&quot;method&quot; name=&quot;_method&quot;  value=&quot;put&quot;/&amp;gt;
    // 파일삭제 버튼은 동적으로 생성되기 때문에 첨부파일 없을 경우 존재하지 않기 때문에
    // 따로 div를 만들어서 이벤트 핸들러를 등록해줄 예정이다.
    &amp;lt;div class=&quot;eventDiv&quot;&amp;gt;
        &amp;lt;div class=&quot;file_list&quot; th:if=&quot;${board.fileList != null  and board.fileList.size() &amp;gt; 0}&quot;&amp;gt;
            [ 첨부파일 ]
            &amp;lt;div th:each=&quot;list, stat : ${board.fileList}&quot; &amp;gt;
                &amp;lt;span class=&quot;file_bg&quot; th:href=&quot;@{/board/file(idx=${list.idx}, boardIdx=${list.boardIdx})}&quot; th:text=&quot;|${list.originalFileName} (${list.fileSize} kb)|&quot;&amp;gt;&amp;lt;/span&amp;gt;
                /* 
                * 선택된 파일만 name을 동적으로 추가해서 컨트롤러로 값을 넘겨 줄 예정
                * 컨트롤러에서는 BoardDto에서 board.setFileList(List&amp;lt;BoardFileDto&amp;gt;)를 통해서 매핑할 예정
                * 즉, fileList의 name은 List 형태여야한다.
                * 서버 컴퓨터에서 파일을 삭제할 때 필요한 경로와 데이터베이스에서 파일을 삭제할 때 필요한 pk만 받아온다.
                */
                &amp;lt;input type=&quot;hidden&quot; class=&quot;file_idx&quot;  th:value=&quot;${list.idx}&quot;&amp;gt;
                &amp;lt;input type=&quot;hidden&quot; class=&quot;file_path&quot; th:value=&quot;${list.storedFilePath}&quot;&amp;gt;
                &amp;lt;button type=&quot;button&quot; class=&quot;deleteFileBtn btn&quot;&amp;gt;파일삭제&amp;lt;/button&amp;gt;					
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;	
    &amp;lt;/div&amp;gt;
    &amp;lt;input type=&quot;file&quot; id=&quot;files&quot; name=&quot;files&quot; multiple=&quot;multiple&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; id=&quot;boardIdx&quot; name=&quot;boardIdx&quot; th:value=&quot;${board.boardIdx}&quot;&amp;gt;
    &amp;lt;button type=&quot;button&quot; id=&quot;modifyBtn&quot; class=&quot;btn&quot;&amp;gt;수정완료&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;[ 6. 자바스크립트 수정 ]&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694948667003&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이전에 첨부되어있던 파일 삭제(할 수도 있고 안 할 수도 있다.)   
const eventDiv = document.querySelector('.eventDiv');

eventDiv.addEventListener('click', e =&amp;gt; {
    const files = document.querySelector('#files');
    const filesInfo = files.files;
    console.log(filesInfo);
    if(e.target.classList.contains('deleteFileBtn')){
        const fileDiv = e.target.closest('div');
        const fileSpan = fileDiv.querySelector('span');				
        fileSpan.className = 'file_delete_bg';

        e.target.classList.toggle('deleteFileBtn');
        e.target.classList.toggle('cancleFileBtn');
        e.target.textContent = '삭제취소';

    }else if(e.target.classList.contains('cancleFileBtn')){		
        const fileDiv = e.target.closest('div');
        const fileSpan = fileDiv.querySelector('span');				
        fileSpan.className = 'file_bg';

        e.target.classList.toggle('deleteFileBtn');
        e.target.classList.toggle('cancleFileBtn');
        e.target.textContent = '파일삭제';
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;수정할 때 이미 첨부되어있던 파일 삭제, 파일 새롭게 첨부 구현 끝&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2633&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bviUjH/btst7xo5bNb/vQfqyCY1YqseYpFKAGmKsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bviUjH/btst7xo5bNb/vQfqyCY1YqseYpFKAGmKsK/img.png&quot; data-alt=&quot;게시글 수정 화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bviUjH/btst7xo5bNb/vQfqyCY1YqseYpFKAGmKsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbviUjH%2Fbtst7xo5bNb%2FvQfqyCY1YqseYpFKAGmKsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2633&quot; height=&quot;1276&quot; data-origin-width=&quot;2633&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;게시글 수정 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2609&quot; data-origin-height=&quot;1268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxjXuw/btst6pSgmUk/KqUh0sUz7PGKPvk8kN6tp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxjXuw/btst6pSgmUk/KqUh0sUz7PGKPvk8kN6tp1/img.png&quot; data-alt=&quot;이미 첨부되어있던 파일 삭제 선택&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxjXuw/btst6pSgmUk/KqUh0sUz7PGKPvk8kN6tp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxjXuw%2Fbtst6pSgmUk%2FKqUh0sUz7PGKPvk8kN6tp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2609&quot; height=&quot;1268&quot; data-origin-width=&quot;2609&quot; data-origin-height=&quot;1268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이미 첨부되어있던 파일 삭제 선택&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2635&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUBgYk/btsuh6iTApu/AGhqTGqAKKSS9UDtKazsb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUBgYk/btsuh6iTApu/AGhqTGqAKKSS9UDtKazsb1/img.png&quot; data-alt=&quot;파일 새롭게 첨부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUBgYk/btsuh6iTApu/AGhqTGqAKKSS9UDtKazsb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUBgYk%2Fbtsuh6iTApu%2FAGhqTGqAKKSS9UDtKazsb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2635&quot; height=&quot;1276&quot; data-origin-width=&quot;2635&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;파일 새롭게 첨부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2645&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rpbqo/btsugUpqQV0/tmjzszDkWMtJVKkWYZz2T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rpbqo/btsugUpqQV0/tmjzszDkWMtJVKkWYZz2T1/img.png&quot; data-alt=&quot;수정 완료!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rpbqo/btsugUpqQV0/tmjzszDkWMtJVKkWYZz2T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRpbqo%2FbtsugUpqQV0%2FtmjzszDkWMtJVKkWYZz2T1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2645&quot; height=&quot;819&quot; data-origin-width=&quot;2645&quot; data-origin-height=&quot;819&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;수정 완료!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 게시글 삭제할 때 파일도 같이 삭제되게 하는 경우...]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;- 굳이 삭제할 파일을 선택하지 않고 전부 삭제하면 되기 때문에 &lt;br /&gt;&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; boardDtail.html&lt;/b&gt; &lt;/span&gt;(게시글 상세 보기 화면에서) 을 아래와 같이 추가한다.&lt;br /&gt;
&lt;pre id=&quot;code_1694950984055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;file_list&quot; th:if=&quot;${board.fileList != null  and board.fileList.size() &amp;gt; 0}&quot;&amp;gt;
    [ 첨부파일 ]
    // 게시글 수정에서는 input 태그의 name을 자바스크립트를 통해 동적으로 추가해줬다.
    // 게시글 삭제에서는 모든 파일을 삭제할 것이기 때문에 굳이 선택해서 추가할 필요가 없다.
    // th:block을 추가해서 a태그와 input태그를 반복하게 한다.
    &amp;lt;th:block th:each=&quot;list, stat : ${board.fileList}&quot;&amp;gt;
        &amp;lt;a th:href=&quot;@{/board/file(idx=${list.idx}, boardIdx=${list.boardIdx})}&quot; th:text=&quot;|${list.originalFileName} (${list.fileSize} kb)|&quot;&amp;gt;&amp;lt;/a&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; class=&quot;file_idx&quot;  th:name=&quot;'fileList[' + ${stat.index} + '].idx'&quot;  th:value=&quot;${list.idx}&quot;&amp;gt;
        &amp;lt;input type=&quot;hidden&quot; class=&quot;file_path&quot; th:name=&quot;'fileList[' + ${stat.index} + '].storedFilePath'&quot; th:value=&quot;${list.storedFilePath}&quot;&amp;gt;
    &amp;lt;/th:block&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;- BoardController&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694951114237&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 기존 게시글 삭제 처리 */
@DeleteMapping(&quot;/board/{boardIdx}&quot;)
public String deleteBoard(@PathVariable(&quot;boardIdx&quot;) int boardIdx) throws Exception{
    boardService.deleteBoard(boardIdx);

    return &quot;redirect:/board&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694951130576&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 수정 후 게시글 삭제 처리
@DeleteMapping(&quot;/board/{boardIdx}&quot;)
public String deleteBoard(@ModelAttribute BoardDto board) throws Exception{
    boardService.deleteBoard(board);

    return &quot;redirect:/board&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;- BoardService&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694951235085&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 기존 게시글 삭제 */
public void deleteBoard(int boardIdx) throws Exception{
    boardMapper.deleteBoard(boardIdx);  
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694951338640&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 수정 후 게시글 삭제 */
public void deleteBoard(BoardDto board) throws Exception{
    // 게시글 삭제
    int boardIdx = board.getBoardIdx();
    boardMapper.deleteBoard(boardIdx);  

    // 첨부파일 삭제
    boardFileService.deleteFile(board);	    
}&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/158</guid>
      <comments>https://leeri-study.tistory.com/158#entry158comment</comments>
      <pubDate>Mon, 11 Sep 2023 20:03:33 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기7 / 첨부파일 다운로드 구현하기(HttpSevletResponse / ResponsEntity / 바이너리 데이터 )</title>
      <link>https://leeri-study.tistory.com/157</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 다운로드를 구현하기 전에 알아둘 것들 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #456771;&quot;&gt;- HttpSevletRquest&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; 사용자로부터 들어오는 모든 요청 정보를 담고 있다.&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;- HttpSevletResponse&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; 사용자에게 전달할 데이터를 담고 있다.&lt;br /&gt;&amp;nbsp; 즉, 사용자에게 전달할 데이터를 원하는 상태로 설정해서 전달 가능하게 한다.&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- ResponseEntity&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; HTTP 응답의 상태 코드, 헤더, 본문 / RESTful 웹 서비스에서 응답을 보다 세밀하게 제어하기 위해 사용&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #5f6d2b; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;- HttpSevletResponse VS ResponseEntity&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; 둘 다 HTTP 응답을 처리하기 위해 사용된다.&lt;br /&gt;&amp;nbsp; HttpServletResponse가 더 세밀한 응답 조작이 가능하지만,&lt;br /&gt;&amp;nbsp; ResponseEntity를 사용하면 높은 수준의 추상화와 간결한 코드를 만들 수 있다.&lt;br /&gt;&amp;nbsp; 낮은 수준의 추상화 : 컴퓨터 메모리에서 직접 바이트를 읽고 쓰는 작업 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 메모리 구조, 주소, 바이트 조작 등에 대한 깊은 지식 필요&lt;br /&gt;&amp;nbsp; 높은 수준의 추상화 : 파일을 열고 텍스트를 읽는 작업&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 단순히 '파일 열기'와 '텍스트 읽기' 명령만 사용하면 되기 때문에 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 실제 동작 방식에 대해 알 필요 없다.&lt;br /&gt;&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt; 즉, ResponseEntity는 코드의 재사용성, 유지 관리성, 확장성을 향상 시켜 준다. &lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&amp;nbsp; 하지만, HTTP 응답에 대해 세밀한 제어가 필요할 경우 HttpServletResponse를 사용한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;- 바이너리 데이터(Binary Data) : 데이터!!!&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; 컴퓨터는 이진수(1과 0의 집합)로 데이터를 저장하고 표현하고, 1과 0이 들어있는 자리를 Binary digit이라고 한다.&lt;br /&gt;&amp;nbsp; 줄여쓰면 우리가 흔히 들어본 비트(bit)가 된다.&amp;nbsp;&lt;br /&gt;&amp;nbsp; 우리가 실제로 쓰는 숫자는 십진수라고 하고, 컴퓨터는 십진수를 이진수로 변환해서 다룬다.&lt;br /&gt;&amp;nbsp; 하지만 우리가 컴퓨터가 다루길 바라는 데이터는 숫자만 존재하는게 아니다. &lt;br /&gt;&amp;nbsp; 문자열, 이미지 등 다양한 형식의 데이터를 다루고자 할때 컴퓨너는 이런 데이터 역시 이진수로 변환한다.&lt;br /&gt;&amp;nbsp; 즉, 사람은 읽을 수 없지만 컴퓨터는 읽을 수 있는 데이터를 바이너리 '&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;데이터&lt;/span&gt;&lt;/b&gt;'라고한다.&lt;br /&gt;&amp;nbsp; 따라서 컴퓨터는 모든 데이터들을 바이너리 데이터로 저장하고 처리한다.&lt;br /&gt;&amp;nbsp; &lt;span style=&quot;color: #ee2323; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;비트(bit)는 바이너리 데이터를 처리하는 단위!!! /&amp;nbsp; 8비트 = 1 바이트!&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- 바이트(byte) : 단위!!!&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; 데이터의 기본적인 저장 및 전송 단위(1byte = 8bit)&lt;br /&gt;&amp;nbsp; 메모리의 크기나 파일의 크기를 나타내는 기본 적인 저장 및 전송 '&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;단위&lt;/b&gt;&lt;/span&gt;'&lt;br /&gt;&amp;nbsp; 최소 단위인 비트가 있는데 바이트가 최소 단위가 된 이유는 최소 단위의 숫자, 영문, 특수 문자 등을 표현하려면&lt;br /&gt;&amp;nbsp; 최소 8비트가 필요하기 때문이다. (실제로 영문 a를 표현하려면 8비트가 필요)&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- 그러면 왜 경로를 바이트 배열에 넣어주는건데?&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&amp;nbsp; 앞에서 HttpSevletResponse에 대해 이야기 했다.&lt;br /&gt;&amp;nbsp; 파일 다운로드 구현시 사용자에게 다운로드할 파일에 대한 데이터를 전달해야하는데&lt;br /&gt;&amp;nbsp; HTTP 프로토콜은 바이트 스트림을 기반이며, 자바에서 스트림 처리를 할 때 가장 기본적인 단위가 바이트 배열이기 때문이다.&lt;br /&gt;&amp;nbsp; 또, 파일을 스트림으로 읽어 바이트 배열 형태로 처리하면, 서버의 메모리 사용량을 최소하하면서 대용량 파일도 효율적으로 처리할 수 있다.&lt;br /&gt;
&lt;pre id=&quot;code_1694600600746&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// boardFile.getStoredFilePath()는 파일이 저장된 경로다.
// readFileToByteArray는 FileUtils에서 제공하는 메서드로 파일을 바이트 배열로 읽어온다.
byte[] files = BoardFileService.readFileToByteArray(new File(boardFile.getStoredFilePath()));​&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&amp;nbsp; 하지만 굳이 직접 바이트 배열로 넣어주지 않아도 UrlResourse 객체를 활용할 수 있다.&lt;br /&gt;&amp;nbsp; 실제로 구현할 때 위의 코드를 사용하지 않을 예정이다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- UrlResourse&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; 파일, HTTP, FTP 대상 등에 접근하여(특정 URL에 접근하여) 정보를 바이트 배열로 읽어 클라이언트에 전송&lt;br /&gt;&amp;nbsp; URL은 특정한 접두어를 갖는데 이 접두어가 접근하려는 리소스의 종류를 뜻한다.&lt;br /&gt;&amp;nbsp; 파일의 경로에 접하기 위해서는 접두어로 &quot;file:&quot;을 사용한다. (HTTP는 &quot;https:&quot;)&lt;br /&gt;&amp;nbsp; UrlResource 객체는 UrlResource 생성자(new UrlResource()) 를 통해 생성되지만 종종 이 접두어를 통해 묵시적으로 생성될 수 있다.&lt;br /&gt;&amp;nbsp; &quot;classpath:&quot;는 ClassPathResource가 &quot;file:&quot;은 FileSystemResource가 생성된다. 인식되지 않는 접두어를 사용할 경우 표준 URL 문자로 추측하여 UrlResource 객체가 생성된다.&lt;br /&gt;&amp;nbsp; 그러면 그냥 처음부터 FileSystemResource 객체를 생성하면 안되나 싶지만 나중에 참조하는 URL이 HTTP 등으로 변경될 수 있기 때문에 유연성을 위해 UrlResource를 사용해줄 예정이다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- URLEncoder.encode() vs UriUtils.encode()&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; /board/write에서 board와 write가 경로 세그먼트다. 하지만 이 경로 세그먼트에는 특별한 예약 문자들이 존재한다.&lt;br /&gt;&amp;nbsp; 예를 들어, URL의 경로 세그먼트에서는 &quot;+&quot;기호가 실제 &quot;+&quot;기호로 해석되어야한다.&lt;br /&gt;&amp;nbsp; - URIEncoder : 주로 쿼리 매개변수 값을 인코딩하기 위해 설계됐기 때문에 공백 문자를 &quot;+&quot; 기호로 변환하다.&lt;br /&gt;&amp;nbsp; - UriUtils : URL의 다양한 세그먼트를 올바르게 인코딩하기 위한 도구 (스프링 프레임워크 제공)&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 1. dao(쿼리) 작성 ]&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;- 클라이언트 측에서 다운로드를 실행했을 때 받아오는 정보 : idx(첨부파일번호) / boardInx(게시판번호)&lt;br /&gt;- 파일 다운로드 할 때 필요한 데이터 : originalFileName(파일이름) / storedFilePath(저장경로)&lt;br /&gt;
&lt;pre id=&quot;code_1694512613477&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;select id=&quot;selectFileInfo&quot; parameterType=&quot;BoardFileDto&quot; resultType=&quot;BoardFileDto&quot;&amp;gt;
    /* 게시글 번호에 해당하는 파일 정보 가져오기 */
    &amp;lt;![CDATA[
        SELECT
            original_file_name,
            stored_file_path,
            file_size
        FROM
            t_file
        WHERE
            idx = #{idx}
        AND
            board_idx = #{boardIdx}
        AND
            deleted_yn = 'N'
    ]]&amp;gt;
&amp;lt;/select&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;br /&gt;[ 2. Mapper 작성 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694512740278&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 하나의 게시글에서 다운 받고자하는 파일 정보 조회
BoardFileDto selectFileInfo(BoardFileDto file) throws Exception;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 3. BoardController 작성 ]&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- HttpServletResonse 사용&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694620640867&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@GetMapping(&quot;/board/file&quot;)
public void downloadFile(BoardFileDto file, HttpServletResponse response) throws Exception{
    BoardFileDto boardFile = boardFileService.selectFileInfo(file);

    //boardFile은 따로 만든 dto 클래스이기 때문에 MultipartFile처럼 isEmpty 메서드가 없으니까 ObjectUitls.isEmpty메서드 사용
    if(!ObjectUtils.isEmpty(boardFile)) {
        String fileName = boardFile.getOriginalFileName();
        int fileSize = (int)boardFile.getFileSize();

        // 파일의 경로를 바이트배열로 읽어온다.
        UrlResource resource = new UrlResource(&quot;file:&quot; + boardFile.getStoredFilePath());

        // 파일 이름 URL 주소로 사용하기 전에 인코딩하기
        String encodedFileName = UriUtils.encode(fileName, StandardCharsets.UTF_8);

        // Content-Disposition: HTTP 응답의 처리 방식, 브라우저가 반환된 내용을 화면에 표시할 것인지 파일로 다운로드할 것인지 결정한다.
        // attachment : 반환된 내용을 파일로 다운로드 해라
        String contentDisposition = &quot;attachment; filename=\&quot;&quot; + encodedFileName + &quot;\&quot;&quot;;
        response.setHeader(&quot;Content-Disposition&quot;, contentDisposition);

        // ContentType : 응답의 본문(body)의 미디어 타입 (데이터의 종류, 포맷에 대한 정보 제공)
        // application/octet-stream : 바이너리 데이터
        response.setContentType(&quot;application/octet-stream&quot;);

        // 파일의 크기 설정(바이트 단위)
        response.setContentLengthLong(fileSize);

        // 파일의 바이트 수 가져오기 위해서 인풋 스트림 생성
        InputStream inputStream = resource.getInputStream();

        // 다운로드 파일 데이터를 클라이언트에 전달하기 위해 response에 출력할 출력 스트림을 생성	        I
        OutputStream outStream = response.getOutputStream();	       

        // 파일을 바이트 배열로 가져오기
        byte[] byteFile = resource.getContentAsByteArray();

        int byteLength;
        // inputStream.read(byteFile) : 바이트 배열의 길이
        // 바이트 배열을 루프를 돌면서 하나의 바이트씩 읽는다. 더 이상 읽을 바이트가 없으면 -1 리턴
        while ((byteLength = inputStream.read(byteFile)) &amp;gt; 0) {
            // write(바이트배열, 바이트배열에서 데이터의 시작 위치, 바이트배열의 테이터 길이]
            // 바이트 배열에서 시작 위치 부터 데이터 길이까지 데이터를 출력 스트림으로 보관
            outStream.write(byteFile, 0, byteLength);
        }			
        outStream.flush();
        inputStream.close();   
        outStream.close();  
    }
}​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- ResponseEntity 사용&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694957317511&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 첨부파일 다운로드 
 * ResponseEntity : HTTP 응답의 상태 코드, 헤더, 본문 
 * 									RESTful 웹 서비스에서 응답을 보다 세밀하게 제어하기 위해 사용
 * Resource : UrlResource의 내용을 body로 보내주기 위해서 사용
 */
@GetMapping(&quot;/board/file&quot;)
public ResponseEntity&amp;lt;Resource&amp;gt; downloadFile(BoardFileDto file) throws Exception{
    BoardFileDto boardFile = boardFileService.selectFileInfo(file);

    //boardFile은 따로 만든 dto 클래스이기 때문에 MultipartFile처럼 isEmpty 메서드가 없으니까 ObjectUitls.isEmpty메서드 사용
    if(!ObjectUtils.isEmpty(boardFile)) {
        String fileName = boardFile.getOriginalFileName();
        int fileSize = (int)boardFile.getFileSize();

        // 파일의 경로를 바이트배열로 읽어온다.
        UrlResource resource = new UrlResource(&quot;file:&quot; + boardFile.getStoredFilePath());

        // 파일 이름 URL 주소로 사용하기 전에 인코딩하기
        String encodedFileName = UriUtils.encode(fileName, StandardCharsets.UTF_8);

        // Content-Disposition: HTTP 응답의 처리 방식, 브라우저가 반환된 내용을 화면에 표시할 것인지 파일로 다운로드할 것인지 결정한다.
        // attachment : 반환된 내용(인코딩된 파일 이름)을 파일로 다운로드 해라
        String contentDisposition = &quot;attachment; filename=\&quot;&quot; + encodedFileName + &quot;\&quot;&quot;;

        // ResponseEntity.ok() : ResponseEntity의 메서드 상태 코드 200(OK)를 가진 ResponseEntity 객체 생성
        // header() : HTTP 응답의 헤더
        // HttpHeaders.CONTENT_DISPOSITION : HTTP 헤더 중 Content-Disposition이라는 헤더의 이름
        // 헤더에 contentDisposition이라는 변수를 넣어준다. (다운로드할 것을 명시하는 attachment와 파일이름)
        // body(resource) : HTTP 응답의 본문 / 위에서 생성한 UrlResource 객체 - 실제 파일의 내용
        return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,contentDisposition).body(resource);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 4. BoardFileService 작성 ]&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694620717997&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 하나의 게시글에서 다운 받고자하는 파일 정보 조회
// 서비스에서는 다운로드에 필요한 파일의 정보만 가져올 뿐 다운로드에 직접적인 관련이 없다.
public BoardFileDto selectFileInfo(BoardFileDto file) throws Exception{	
        BoardFileDto boardFile = boardMapper.selectFileInfo(file);
    return boardFile;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/157</guid>
      <comments>https://leeri-study.tistory.com/157#entry157comment</comments>
      <pubDate>Mon, 11 Sep 2023 20:02:03 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / database ] RESTful한 게시판 만들기6 / 첨부파일 크기 오류</title>
      <link>https://leeri-study.tistory.com/156</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;파일 크기 제한이 있다보니 파일 크기를 넘어가면 MaxUploadSizeExceededException 오류가 발생한다.&lt;br /&gt;&lt;br /&gt;프로퍼티 파일에 다음 코드를 추가해서 체크 시점을 지연시킨 후&amp;nbsp;&lt;br /&gt;
&lt;pre id=&quot;code_1694448331376&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 원래 파일 업로드시 HTTP 요청 데이터가 스프링 MVC의 컨트롤러 메서드로 전달되기 전
# 즉, 요청 파싱 단계에서 파일 크기를 체크해서 에러를 발생시킨다고 한다.
# 그래서 해당 파일에 접근하는 시점에 파일을 체크하게 한다. (체크 시점을 지연시키는 것)
spring.servlet.multipart.resolve-lazily=true​&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;컨트롤러에 예외 처리를 해줘도 되지만,&lt;/p&gt;
&lt;pre id=&quot;code_1694448444618&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler(MaxUploadSizeExceededException.class)
public String handleMaxUploadSizeExceededException(MaxUploadSizeExceededException maxUploadSizeExceededException,
													RedirectAttributes redirectAttributes) {
    redirectAttributes.addFlashAttribute(&quot;errorMessage&quot;, &quot;파일 용량이 초과되었습니다.&quot;);

    return &quot;redirect:/board/write&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;굳이 이미 에러 상태인데 파일을 서버에 전송할 필요가 없을 것 같기도하고 작성 화면 그 상태로 새로고침 하고싶지 않아서, 자바스크립트를 통해 클라이언트 측에서 미리 체크하기로 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1694448600185&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const insertBtn = document.querySelector('#insertBtn');
const insertForm = document.querySelector('#frm');

insertBtn.addEventListener('click', e =&amp;gt; {
    const maxOneFileSize = 1 * 1024 * 1024;
    const maxAllFileSize = 5 * 1024 * 1024;
    let sumFileSize = 0;
    let isOneTrue = true;
    let isAllTrue = true;

    const files = document.querySelector('#files');
    // input의 type이 file인 태그의 경우 .files라는 내장 프로퍼티를 사용 가능하다.
    // .files라는 프로퍼티는 type이 file인 input을 요소로 선택해서 사용할 수 있고,
    // 사용자가 선택한 파일들의 정보를 담고 있다.
    const filesInfo = files.files;

    for(const oneFile of files){
        sumFileSize += oneFile.size;
        if(oneFile.size &amp;gt;maxOneFileSize){
            alert(&quot;파일 용량이 초과되었습니다.&quot;);
            isOneTrue = false;
            break;
        }				
    }

    if(sumFileSize &amp;gt; maxAllFileSize) { isAllTrue = false; }			
    if(isOneTrue &amp;amp;&amp;amp; isAllTrue) { insertForm.submit(); }
});&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pT4nJ/btstLQBaM6h/fPlhCEWzWtyyIUtcuN0vx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pT4nJ/btstLQBaM6h/fPlhCEWzWtyyIUtcuN0vx1/img.png&quot; data-alt=&quot;그럼 새로고침 없이 글 작성하던 상태 그대로 경고창만 뜬다!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pT4nJ/btstLQBaM6h/fPlhCEWzWtyyIUtcuN0vx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpT4nJ%2FbtstLQBaM6h%2FfPlhCEWzWtyyIUtcuN0vx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;95&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;363&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그럼 새로고침 없이 글 작성하던 상태 그대로 경고창만 뜬다!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/156</guid>
      <comments>https://leeri-study.tistory.com/156#entry156comment</comments>
      <pubDate>Mon, 11 Sep 2023 15:16:17 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기5 / 첨부파일 업로드 구현하기(MultipartResolver)</title>
      <link>https://leeri-study.tistory.com/155</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;원래는 게시글 등록할 때 첨부파일 업로드도 같이 구현해줬어햐 하는데...&lt;br /&gt;첨부파일 구현 경험이 없어서 일단 CRUD 부터 다 끝내고 첨부파일 구현 시작!&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ &lt;span style=&quot;background-color: #ffffff; text-align: start;&quot;&gt;MultipartResolver&lt;/span&gt;]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;클라이언트로부터 데이터를 전송 받을 때 폼 데이터만 전송 받는 경우도 있지만,&lt;br /&gt;폼 데이터와 함께 파일 같은 데이터도 같이 전송 받는 경우가 있다. 이런 다중 부분 데이터를 처리하기 위해 스프링에서 제공하는 인터페이스다.&lt;br /&gt;주로 파일 업로드와 다운로드에 사용된다.&lt;br /&gt;스프링부트에는 이미 기본 빈으로 등록되어 있기 때문에 따로 처리해줄 필요가 없다.&lt;br /&gt;&lt;br /&gt;1. CommonsMultipartResolver (Apache Commons FileUpload를 사용)&lt;br /&gt;2. StandardServletMultipartResolver (Servlet 3.0+의 멀티파트 해결 방법을 사용)&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 파일 업로드 구현하기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;b&gt;[ 1. application.properties와 html 작성 ]&lt;br /&gt;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694441762110&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 첨부파일 처리 위한 설정
# 최대 파일 크기 설정 - 기본 값 1MB
spring.servlet.multipart.max-file-size=1MB
# 여러 파일 업로드 시 전체 파일 크기 - 기본 값 10MB
spring.servlet.multipart.max-request-size=10MB 
# 파일 저장 경로 설정
# file.path는 내가 임의로 정한 이름 다른 걸로 해도되고 여러 개 설정해도 된다.
file.path=C:/Users/abc/Documents/project/restApi/board/src/main/resources/static/boardFile&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694441779785&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- enctype=&quot;multipart/form-data&quot;를 추가해주는 것이 포인트 --&amp;gt;
&amp;lt;form id=&quot;frm&quot; name=&quot;frm&quot; method=&quot;post&quot; action=&quot;/board/write&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
    &amp;lt;!-- 파일 하나만 업로드 가능하게 할 때 --&amp;gt;
    &amp;lt;input type=&quot;file&quot; id=&quot;file&quot; name=&quot;file&quot;&amp;gt;

    &amp;lt;!-- 여러 파일 업로드 가능하게 할 때 --&amp;gt;
    &amp;lt;input type=&quot;file&quot; id=&quot;files&quot; name=&quot;files&quot; multiple=&quot;multiple&quot;&amp;gt;

    &amp;lt;input type=&quot;submit&quot; id=&quot;submit&quot; value=&quot;등록&quot; class=&quot;btn&quot;&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;[ 2. BoardFileDto 생성 ]&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;- dto 패키지 내에 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;BoardFileDto&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 클래스 생성&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694420651222&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Setter
@ToString
public class BoardFileDto {
	private int idx;
	private int boardIdx;
	private String originalFileName;
	private String storedFilePath;
	private long fileSize;	
}&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 3. BoardController 수정 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694412164992&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 게시글 등록 처리
@PostMapping(&quot;/board&quot;)
public String insertBoard(BoardDto board) throws Exception{
    boardService.insertBoard(board);

    return &quot;redirect:/board&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694412335260&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 수정 후 게시글 등록 처리
// @RequestParam(&quot;files&quot;) List&amp;lt;MultipartFile&amp;gt; files 추가
@PostMapping(&quot;/board&quot;)
public String insertBoard(BoardDto board, @RequestParam(&quot;files&quot;) List&amp;lt;MultipartFile&amp;gt; files) throws Exception{
    boardService.insertBoard(board, files);

    return &quot;redirect:/board&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 4. BoardFileService 생성 ]&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- BoardFileService에서 따로 파일 처리를 위한 기능을 만들어 줄 것&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694421216134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * 프로퍼티 파일, 환경 변수, 시스템 속성 등의 외부 설정 값을 Spring 빈에 주입한다.
 * application.properties(프로퍼티 파일)에 작성해 둔 file.path를 사용하기 위해 썼다. 즉, 프로퍼티 파일에
 * 작성해둔 file.path라는 값을 필드의 filePath에 주입하겠다는 뜻
 */
@Value(&quot;${file.path}&quot;)
private String filePath;
private final BoardMapper boardMapper;

/**
 * 첨부파일 서버 컴퓨터에 저장 &amp;amp; 파일 정보 리턴
 * */
public List&amp;lt;BoardFileDto&amp;gt; uploadFile(int boardIdx, List&amp;lt;MultipartFile&amp;gt; files) throws Exception {		    	
    // 업로드한 파일이 없으면 null 반환
    // 만약 files.isEmpty() 하면 false 떠서 return이 안된다.
    // files는 데이터 타입이 List&amp;lt;MultipartFile&amp;gt; 이기 때문에 일단 List 자체에는 비어있는 MultipartFile이 존재하기 때문
    // 실제로 files.size()는 1이고 files.get(0).getSize()는 0이 나온다.
    // 참고로 size()는 List 같은 컬렉션에서 제공하는 메서드고
    // getSize()는 MultipartFile에서 제공하는 메서드라는 점에서 차이를 알 수 있다.
    // files.isEmpty()는 files가 String이거나 컬렉션 등 특정 타입에서만 사용 가능하다.
    // 다양한 타입에서 사용할 경우에는 ObjectUtils.isEmpty(files) 사용
    // 하지만, MultiparFile 자체에서 isEmpty()를 제공하기 때문에 files.isEmpty()를 사용
    if(files.get(0).isEmpty()) {
        return null;
    }

    // 파일을 저장할 경로 지정하고 폴더 생성하기
    // 날짜를 20230911 형식으로 가져오도록 미리 형식 선언
    DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern(&quot;yyyMMdd&quot;);
    // 파일 저장 당시 날짜 가져오기
    ZonedDateTime currentDate = ZonedDateTime.now();
    // 파일 저장 경로 설정해주기 (날짜를 미리 선언해둔 형식으로 바꿔줘라)
    String path = filePath + currentDate.format(dateFormat);	    
    // File 객체는 파일 또는 디렉토리의 경로 정보를 저장하고 있다. 그래서 생성할 때 path 넣어준다.
    // File 객체가 제공하는 메서드 exists,isFile,isDirectory,mkdir,mkdirs...
    File file =new File(path);
    // 만약 지정된 경로에  해당 폴더가 존재하지 않을 경우 폴더를 만든다.
    // exists() : 파일 및 디렉토리 존재여부 확인 (파일 : isFile() / 디렉토리 : isDirectory())
    if(file.isDirectory() == false) {
        // mkdir() : 경로의 가장 마지막 폴더만 생성 / mkdirs() : 없는 폴더 전부 생성
        // 예를 들어 경로가 /a/b/c 인데 a라는 폴더만 있을 경우 mkdir()는 c 폴더만 / mkdirs()는 b,c 폴더 생성
        file.mkdirs();
    }

    List&amp;lt;BoardFileDto&amp;gt; fileList = new ArrayList&amp;lt;&amp;gt;();

    for (MultipartFile oneFile : files) {
        // 원래 파일 이름 추출 MultipartFile에서 제공하는 메서드 (확장자 포함)
        String originalFileName = oneFile.getOriginalFilename();

        // 확장자 추출
        // lastIndexOf() : 문자열의 끝에서 부터 해당 문자열을 찾아 인덱스를 반환 (확장자명은 .으로 구분되어 있기 때문)
        // substring(숫자) : 해당 인덱스부터 끝까지 반환 
        // 만약 파일 이름이 img.gif 였다면 substring(3) -&amp;gt; .gif 반환
        String fileExtension = originalFileName.substring(originalFileName.lastIndexOf(&quot;.&quot;));

        // 새로운 파일 이름 (확장자 포함)
        // 새로운 파일 이름은 유일성을 보장해야한다. (중복 x)
        // UUID라는 객체는 따로 생성자 생성 없이 사용 가능
        // UUID.randomUUID()는 유일성을 보장하는 랜덤 객체 반환
        // 즉 UUID 객체를 반환하기 때문에 Strign으로 형변환
        String uniqueObject =  UUID.randomUUID().toString();

        // 문자열로 변환한 UUID 객체와 확장자 결합
        String newFileName = uniqueObject + fileExtension;

        BoardFileDto boardOneFile = new BoardFileDto();

        boardOneFile.setBoardIdx(boardIdx);
        boardOneFile.setFileSize(oneFile.getSize());
        boardOneFile.setOriginalFileName(originalFileName);
        boardOneFile.setStoredFilePath(path + &quot;/&quot; + newFileName);

        fileList.add(boardOneFile);

        // File 객체에 파일 저장 (파일 확장자 포함한 파일경로)
        file = new File(path + &quot;/&quot; + newFileName);
        // onFile을 file 경로로 전송(저장)한다.
        oneFile.transferTo(file);
    }
return fileList;
}

/** 첨부파일 등록 */
public void insertFile(BoardDto board, List&amp;lt;MultipartFile&amp;gt; files) throws Exception{		
    // 파일을 서버 컴퓨터에 저장 후 파일 정보들 DTO에 받아주기
    // useGeneratedKeys로 받아온 새로 생성된 boardIdx를 매개변수로 넣어준다.
    List&amp;lt;BoardFileDto&amp;gt; fileList = uploadFile(board.getBoardIdx(), files);

    if(fileList == null) {
        return;
    }

    // 데이터베이스에 파일 정보 저장
    boardMapper.insertFileList(fileList);
}&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 5. BoardService 수정 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694421527641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 기존 게시글 등록 로직
public void insertBoard(BoardDto board) throws Exception{
    boardMapper.insertBoard(board);
}​&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694427221818&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 수정 후 게시글 등록
public void insertBoard(BoardDto board, List&amp;lt;MultipartFile&amp;gt; files) throws Exception{
    // form 데이터 등록
    // dao에서 등록된 pk(boardIdx) 받아오도록 작성함 - useGeneratedKeys
    boardMapper.insertBoard(board);

    // 첨부파일 등록
    boardFileService.insertFile(board, files);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 6. BoardMapper 작성 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694427285215&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 데이터베이스에 업로드 파일 정보 저장
void insertFileList(List&amp;lt;BoardFileDto&amp;gt; fileList) throws Exception;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 7. DAO(SQL) 수정 및 작성 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694427385552&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;insert id=&quot;insertBoard&quot; parameterType=&quot;BoardDto&quot;&amp;gt;
    /* 기존 게시글 등록 */
    &amp;lt;![CDATA[
        INSERT INTO t_board
        (
            title,
            contents,
            created_datetime,
            creator_id
        )
        VALUES
        (	#{title},
            #{contents},
            NOW(),
            'admin'
        )
    ]]&amp;gt;
&amp;lt;/insert&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694427669588&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- useGeneratedKey : insert 문에서 insert된 auto-increment 또는 sequence로 생성된 기본 키의 값을 가져온다. --&amp;gt;
&amp;lt;!-- keyProperty : 가져온 값을 담아주는 변수 --&amp;gt;
&amp;lt;insert id=&quot;insertBoard&quot; parameterType=&quot;BoardDto&quot; useGeneratedKeys=&quot;true&quot; keyProperty=&quot;boardIdx&quot;&amp;gt;
    /* 게시글 등록 */
    &amp;lt;![CDATA[
        INSERT INTO t_board
        (
            title,
            contents,
            created_datetime,
            creator_id
        )
        VALUES
        (	#{title},
            #{contents},
            NOW(),
            'admin'
        )
    ]]&amp;gt;
&amp;lt;/insert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;br /&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694427849280&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;insert id=&quot;insertFileList&quot; parameterType=&quot;BoardFileDto&quot;&amp;gt;
    /* 데이터베이스에 업로드 파일 정보 저장 */
    &amp;lt;![CDATA[
        INSERT INTO t_file
        (
            board_idx, original_file_name,
            stored_file_path, file_size,
            creator_id, created_datetime
        )
        VALUES
    ]]&amp;gt;
    /* 받아온 데이터(collection) : List 일 때 반복해서 쿼리 생성*/
    &amp;lt;foreach collection=&quot;list&quot; item=&quot;item&quot; separator=&quot;,&quot;&amp;gt;
        (
            #{item.boardIdx},
            #{item.originalFileName},
            #{item.storedFilePath},
            #{item.fileSize},
            'admin',
            Now()
        )
    &amp;lt;/foreach&amp;gt;
&amp;lt;/insert&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 게시글 클릭했을 때 첨부파일 정보 같이 보기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 1. DAO(SQL) 작성 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694428935141&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- 바이트 크기로 저장되어 있기 때문에 킬로바이트 크기로 변환한다. --&amp;gt;
&amp;lt;select id=&quot;selectFileList&quot; parameterType=&quot;int&quot; resultType=&quot;BoardFileDto&quot;&amp;gt;
    /* 해당 첨부파일 조회 */
    &amp;lt;![CDATA[
        SELECT
            idx,
            board_idx,
            original_file_name,
            stored_file_path,
            FORMAT(ROUND(file_size / 1024), 0) AS file_size
        FROM
            t_file
        WHERE
            board_idx = #{boardIdx}
        AND
            deleted_yn = 'N'
    ]]&amp;gt;
&amp;lt;/select&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;[ 2. BoardDto 추가 ]&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- BoardDto와 BoardFileDto는 1 : N 관계다.&lt;br /&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- boardIdx 하나에 파일 정보 여러 개가 매핑 될 수 있다는 뜻&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;- 그래서 BoardDto에 BoardFileDto 타입으로 List 필드를 하나 추가해 준다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694429022642&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private List&amp;lt;BoardFileDto&amp;gt; fileList;&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 3. BoardService 수정 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694429363290&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 게시글 상세 내용 조회
public BoardDto selectDetail(int boardIdx) throws Exception{		
    // 상세 내용 조회 -&amp;gt; 조회수 증가
    boardMapper.updateHitCount(boardIdx);

    // 상세 내용 가져오기
    BoardDto boardDetail = boardMapper.selectDetail(boardIdx);

    // 파일들 정보 가져오기
    List&amp;lt;BoardFileDto&amp;gt; fileList = boardMapper.selectFileList(boardIdx);
    boardDetail.setFileList(fileList);

    return boardDetail;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 4. BoardMapper 추가 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694429459143&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;List&amp;lt;BoardFileDto&amp;gt; selectFileList(int boardIdx) throws Exception;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;901&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DwglM/btstM2nwK6q/FR2JOyRM8kc80AAjpeuSKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DwglM/btstM2nwK6q/FR2JOyRM8kc80AAjpeuSKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DwglM/btstM2nwK6q/FR2JOyRM8kc80AAjpeuSKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDwglM%2FbtstM2nwK6q%2FFR2JOyRM8kc80AAjpeuSKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2650&quot; height=&quot;901&quot; data-origin-width=&quot;2650&quot; data-origin-height=&quot;901&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/155</guid>
      <comments>https://leeri-study.tistory.com/155#entry155comment</comments>
      <pubDate>Mon, 11 Sep 2023 15:11:29 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / 백준 2675 ] 문자열 문자 단위로 반복해서 출력하기</title>
      <link>https://leeri-study.tistory.com/154</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 문제 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;1. 첫 번째 줄에는 문자열을 입력 받을 횟수를 받는다.&lt;br /&gt;2. 두 번째 줄에는 문자열을 문자 단위로 반복하는 횟수 + 공백 + 문자열을 받는다.&lt;br /&gt;3. 출력은 문자 단위로 입력 받은 만큼 반복하는 방식으로 수정된 문자열만 출력하되 각 문자열은 개행해서 출력한다.&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;[ 예시 - 입력 형태 ]&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;2&lt;br /&gt;3 ABC &lt;br /&gt;5 /HTP&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 예시 - 출력 형태 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;AAABBBCCC &lt;br /&gt;/////HHHHHTTTTTPPPPP&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #a6bc00;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기본적으로 for문과 charAt를 이용해서 코드를 작성하고 조금씩 다르게 총 4가지로 수정해봤는데 크게 차이는 없는 듯 하다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 1. BufferedWriter의 write() 이용해서 바로 담기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;속도 : 124 ms&lt;/blockquote&gt;
&lt;pre id=&quot;code_1694408810561&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BufferedReader br = new BufferedReader (new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter (new OutputStreamWriter(System.out));

// 첫 번째 줄에서 int 타입으로 문자열 몇 개 입력 받을지 입력 받기
int inputNum = Integer.valueOf(br.readLine());

// for문 안에서 입력 받은 만큼 문자열을 입력 받고 for문 안에서 문자 반복 출력하기
for(int i = 0; i &amp;lt; inputNum; i += 1) {
    // 두 번째 줄에서는 공백을 기준으로 문자를 반복할 횟수와 문자열을 받을 예정
    // StringTokenizer로 공백을 기준으로 둘을 int와 String으로 나누어준다.
    StringTokenizer st = new StringTokenizer(br.readLine());
    int repeatNum = Integer.valueOf(st.nextToken());
    String repeatStr = st.nextToken();

    // charAt()를 이용해서 문자열의 각 인덱스를 반복해준다.
    for(int j = 0; j &amp;lt; repeatStr.length(); j += 1) {
        for(int k = 0; k &amp;lt; repeatNum; k += 1) {
            bw.write(String.valueOf(repeatStr.charAt(j)));            			
        }
    }
    // 문자열 마다 개행
    bw.write(&quot;\n&quot;);    
}
bw.flush();
bw.close();
br.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 2. StringBuffer 이용해서 문자열 연결해주고 write()에 담기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;속도 : 124 ms (코드만 더 길어지고 속도 차이는 없긴 하다...)&lt;/blockquote&gt;
&lt;pre id=&quot;code_1694408943571&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BufferedReader br = new BufferedReader (new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter (new OutputStreamWriter(System.out));

// 첫 번째 줄에서 int 타입으로 문자열 몇 개 입력 받을지 입력 받기
int inputNum = Integer.valueOf(br.readLine());

// for문 안에서 입력 받은 만큼 문자열을 입력 받고 for문 안에서 문자 반복 출력하기
for(int i = 0; i &amp;lt; inputNum; i += 1) {
	// 두 번째 줄에서는 공백을 기준으로 문자를 반복할 횟수와 문자열을 받을 예정
    // StringTokenizer로 공백을 기준으로 둘을 int와 String으로 나누어준다.
    StringTokenizer st = new StringTokenizer(br.readLine());
    int repeatNum = Integer.valueOf(st.nextToken());
    String repeatStr = st.nextToken();

    StringBuffer sb = new StringBuffer();
    
    // charAt()를 이용해서 문자열의 각 인덱스를 반복해준다.
    for(int j = 0; j &amp;lt; repeatStr.length(); j += 1) {
        char c = repeatStr.charAt(j);
        for(int k = 0; k &amp;lt; repeatNum; k += 1) {
            // StringBuffer에서는 문자열을 append()를 통해 붙여준다.
            sb.append(c);
        }
    }
    bw.write(sb.toString()); 
        
    // 문자열 마다 개행
    bw.write(&quot;\n&quot;);    
}
bw.flush();
bw.close();
br.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 3. String 클래스의 repeat() 메서드 활용하기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;속도 : 120 ms (속도가 미세하게 빨라지고 for문도 세 번에서 두 번으로 덜 쓸 수 있다.)&lt;/blockquote&gt;
&lt;pre id=&quot;code_1694409230515&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BufferedReader br = new BufferedReader (new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter (new OutputStreamWriter(System.out));

// 첫 번째 줄에서 int 타입으로 문자열 몇 개 입력 받을지 입력 받기
int inputNum = Integer.valueOf(br.readLine());

// for문 안에서 입력 받은 만큼 문자열을 입력 받고 for문 안에서 문자 반복 출력하기
for(int i = 0; i &amp;lt; inputNum; i += 1) {
    // 두 번째 줄에서는 공백을 기준으로 문자를 반복할 횟수와 문자열을 받을 예정
    // StringTokenizer로 공백을 기준으로 둘을 int와 String으로 나누어준다.
    StringTokenizer st = new StringTokenizer(br.readLine());
    int repeatNum = Integer.valueOf(st.nextToken());
    String repeatStr = st.nextToken();
    
    for(int j = 0; j &amp;lt; repeatStr.length(); j += 1) {
        // repeat() 메서드는 String 클래스에서 제공하기 때문에 
        // char 타입은 String으로 변환해줘야 쓸 수 있다.
        // bw.write(String.valueOf(repeatStr.charAt(j)).repeat(repeatNum)); 처럼 바로 갖다 넣을 수 도 있지만,
        // 지저분해서 변수에 담아서 넣어줬다.
        String c = String.valueOf(repeatStr.charAt(j));
        bw.write(c.repeat(repeatNum));
    }        
    // 문자열 마다 개행
    bw.write(&quot;\n&quot;);    
}
bw.flush();
bw.close();
br.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;[ 4. String 클래스의 repeat() 메서드와 toCharArray() 메서드 활용하기 ]&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;속도 : 120 ms (코드가 살짝 짧아지긴 했지만 속도도 그렇고 별 차이는 없긴 하다...)&lt;/blockquote&gt;
&lt;pre id=&quot;code_1694409528539&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BufferedReader br = new BufferedReader (new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter (new OutputStreamWriter(System.out));

// 첫 번째 줄에서 int 타입으로 문자열 몇 개 입력 받을지 입력 받기
int inputNum = Integer.valueOf(br.readLine());

// for문 안에서 입력 받은 만큼 문자열을 입력 받고 for문 안에서 문자 반복 출력하기
for(int i = 0; i &amp;lt; inputNum; i += 1) {
    // 두 번째 줄에서는 공백을 기준으로 문자를 반복할 횟수와 문자열을 받을 예정
    // StringTokenizer로 공백을 기준으로 둘을 int와 String으로 나누어준다.
    StringTokenizer st = new StringTokenizer(br.readLine());
    int repeatNum = Integer.valueOf(st.nextToken());
    String repeatStr = st.nextToken();
    
    // toCharArray() 메서드는 문자열을 char 타입으로 하나 씩 떼어내 배열에 담아준다.
	for(char c : repeatStr.toCharArray()) {
        // repeat() 메서드는 String 클래스에서 제공하기 때문에 
        // char 타입은 String으로 변환해줘야 쓸 수 있다.
        // bw.write(String.valueOf(c).repeat(repeatNum)); 처럼 바로 갖다 넣을 수 도 있지만,
        // 지저분해서 변수에 담아서 넣어줬다.
        String sToc = String.valueOf(c);
        bw.write(sToc.repeat(repeatNum));
    }     
    // 문자열 마다 개행
    bw.write(&quot;\n&quot;);    
}
bw.flush();
bw.close();
br.close();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고로 개행할 때 bw.write(&quot;\n&quot;)를 사용하는 것 보다 bw.newLine()을 사용해주면 플랫폼 독립성에 좋다고 한다.&lt;br /&gt;유닉스와 리눅스 환경에서는 개행할 때 \n 을 사용하지만 윈도우 환경에서는 \r\n를 사용하기 때문이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;또, repeat()을 사용하면 for문 중첩은 줄겠지만 쓸 데 없이 형변환 과정을 거쳐야 한다는 의견도 있다. 코드의 가독성, 유지보수, 속도 등을 생각해 적당한 방법을 선택해서 사용하면 될 듯 하다.&lt;/blockquote&gt;</description>
      <category>Back - end/JAVA</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/154</guid>
      <comments>https://leeri-study.tistory.com/154#entry154comment</comments>
      <pubDate>Mon, 11 Sep 2023 14:23:09 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기4 / HTTP Method 중 put과 delete는 html에서 지원하지 않는다.</title>
      <link>https://leeri-study.tistory.com/153</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;RESTful한 게시판을 만들기 위해서 중요한 것은 CRUD에 맞게 HTTP Method를 작성해주는 것이다.&lt;br /&gt;즉, CRUD 중 UPDATE 와 DELETE는 @PutMapping과 @DeleteMapping을 사용해줘야 한다.&lt;br /&gt;하지만 html에서는 get과 post만 지원해준다.&lt;br /&gt;form에 아무리 method를 put / delete로 명시해줘도 사용하지 못한다.&lt;br /&gt;&lt;br /&gt;이런 문제를 해결하기 위해 스프링에서 HiddenHttpMethodFilter 라는 클래스를 제공한다.&lt;br /&gt;보통 기본적으로 활성화되어 있다는데 나는 아무리 해도 get이나 post로만 이동해서 따로 application.properties 파일에서 활성화 해줬다.&lt;br /&gt;
&lt;pre id=&quot;code_1694347001519&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# put / delete 메서도 사용하기 위해서 HiddenHttpMethodFilter 활성화
spring.mvc.hiddenmethod.filter.enabled=true​&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694347261617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form id=&quot;frm&quot; method=&quot;post&quot; th:action=&quot;'/board/'+ ${boardIdx}&quot;&amp;gt;
	// ... 전송하고자 하는 데이터 작성 ...
    
    // HiddenHttpMethodFilter는 name이 _method인 파라미터가 존재할 경우 
    // post를 value의 값으로 해석해서 method를 인식한다.
	&amp;lt;input type=&quot;hidden&quot; id=&quot;method&quot; name=&quot;_method&quot; value=&quot;put&quot;/&amp;gt;
	&amp;lt;button type=&quot;submit&quot; id=&quot;edit&quot; class=&quot;btn&quot;&amp;gt;수정완료&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694347268691&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 만약 타임리프를 사용하는 경우에는 이렇게 직접 작성 가능하다.
&amp;lt;form id=&quot;frm&quot; th:method=&quot;put&quot; th:action=&quot;'/board/'+ ${boardIdx}&quot;&amp;gt;
	// ... 전송하고자 하는 데이터 작성 ...

	&amp;lt;button type=&quot;submit&quot; id=&quot;edit&quot; class=&quot;btn&quot;&amp;gt;수정완료&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/153</guid>
      <comments>https://leeri-study.tistory.com/153#entry153comment</comments>
      <pubDate>Sun, 10 Sep 2023 21:02:11 +0900</pubDate>
    </item>
    <item>
      <title>[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기3 / 전체 목록 조회, 게시글 작성, 게시글 조회</title>
      <link>https://leeri-study.tistory.com/152</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. MVC 패턴을 사용할 것이기 때문에 controller / service / mapper 패키지를 생성하고 클래스를 생성해준다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&amp;nbsp; &amp;nbsp; 단, BoardMapper는 클래스가 아닌 인터페이스로 생성해준다. &lt;br /&gt;&amp;nbsp; &amp;nbsp; 쿼리는 xml 파일에서 구현해줄 것이기 때문이다.&lt;br /&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IoBCm/btstxjRylOA/zDAyI3nmBiA1rsLfbVCtok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IoBCm/btstxjRylOA/zDAyI3nmBiA1rsLfbVCtok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IoBCm/btstxjRylOA/zDAyI3nmBiA1rsLfbVCtok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIoBCm%2FbtstxjRylOA%2FzDAyI3nmBiA1rsLfbVCtok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;445&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;445&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2. Controller 작성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694339969368&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 해당 클래스가 Controller의 역할을 한다고 명시해주는 어노테이션
// 주의할 점은 컨트롤러든 서비스든 매퍼든 같은 이름의 Bean 등록이 불가능하다는 점이다.
// 만약 a 패키지와 b 패키지에 각각 board 클래스에 컨트롤러를 등록할 경우 오류 발생
@Controller
// 필드 중 final이 붙은 필드만을 포함하여 생성자를 생성해주는 어노테이션 (의존성 주입)
@RequiredArgsConstructor
public class BoardController {
	// 의존성 주입을 위한 필드 선언(컨트롤러에서 BordService라는 서비스를 사용할 것이기 때문)
    // 주의할 점(DTO는 의존성 주입해서 사용하지 않는다.)
	private final BoardService boardService;
	
	// 게시판 목록 조회 화면 (Read / 조회 HTTP Method - GET)
	@GetMapping(&quot;/board&quot;)
	public String openBoardList(Model model) throws Exception{
		List&amp;lt;BoardDto&amp;gt; list = boardService.selectBoardList();
		
		model.addAttribute(&quot;list&quot;, list);
		
		return &quot;/board/restBoardList&quot;;		
	}
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;3. Service 작성&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694340093042&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 해당 클래스가 서비스 역할을 한다고 명시하는 어노테이션
@Service
// 의존성 주입
@RequiredArgsConstructor
public class BoardService {
	private final BoardMapper boardMapper;
	
	// 게시글 내역 조회
	public List&amp;lt;BoardDto&amp;gt; selectBoardList() throws Exception{
		List&amp;lt;BoardDto&amp;gt; boardList = boardMapper.selectBoardList();
		
		return boardList;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;4. Mapper 인터페이스 작성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694340237017&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Mapper
public interface BoardMapper {
	// 게시글 내역 조회
    // 인터페이스는 구현부가 존재하지 않는데 상속 받는 클래스에서 구현해야한다.
	List&amp;lt;BoardDto&amp;gt; selectBoardList() throws Exception;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;5. dao 작성 (mapper.xml)&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694340319491&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE mapper
    PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;
    &quot;https://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;

&amp;lt;!-- namespace : implememts와 같은 기능 --&amp;gt;
&amp;lt;!-- class 상속받은 클래스 implements 인터페이스와 같은 기능이라고 생각하면 된다. --&amp;gt;
&amp;lt;!-- 위에서 작성한 BoardMapper.java 인터페이스의 상속을 받아 구현하겠다는 뜻 --&amp;gt;
&amp;lt;!-- 주의할 점은 인터페이스를 상속 받은 경우 인터페이스의 모든 메서드를 구현해야 한다는 것--&amp;gt;
&amp;lt;!-- CDATA : 특수 문자(&amp;lt;, &amp;gt;, &amp;amp; 등)를 태그로 인식하지 않고 쿼리 코드라는 것을 명시 --&amp;gt;
&amp;lt;!-- 특수 문자가 없는 경우에도 넣어준 이유는 코드의 일관성과 나중에 쿼리가 수정될 수 있기 때문 --&amp;gt;
&amp;lt;mapper namespace=&quot;board.rest.api.mapper.BoardMapper&quot;&amp;gt;
    &amp;lt;!-- 게시글을 '조회'하는 쿼리이기 때문에 select 사용 --&amp;gt;
    &amp;lt;!-- id는 인터페이스의 메서드와 일치 --&amp;gt;
    &amp;lt;!-- resultType은 리턴할 데이터의 타입 --&amp;gt;
    &amp;lt;!-- 반대로 parameterType은 매개변수라고 보면되는데 매개변수가 없기 때문에 사용하지 않음--&amp;gt;
    &amp;lt;!-- 처음에 mybatis를 설정할때 자바변수(카멜)와 db컬럼(스테이크)를 자동으로 매핑해주도록 했기 때문에 --&amp;gt;
    &amp;lt;!-- 별칭(AS)을 따로 써주지 않아도 자동으로 DTO의 필드와 연결해준다. --&amp;gt;
	&amp;lt;select id=&quot;selectBoardList&quot; resultType=&quot;BoardDto&quot;&amp;gt;
		/* 게시글 내역 조회 */
		&amp;lt;![CDATA[
			SELECT
				board_idx,
				title,
				hit_cnt,
				created_datetime
			FROM
				t_board
			WHERE
				deleted_yn = 'N'
			ORDER BY board_idx DESC		
		]]&amp;gt;		
	&amp;lt;/select&amp;gt;
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;6. html 작성 (타임리프)&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1694341280604&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;ko&quot; xmlns:th=&quot;http://www.thymeleaf.org&quot;&amp;gt;
&amp;lt;head&amp;gt;
	&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
	&amp;lt;title&amp;gt;board&amp;lt;/title&amp;gt;
	&amp;lt;link rel=&quot;stylesheet&quot; th:href=&quot;@{/css/style.css}&quot;/&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;container&quot;&amp;gt;
    	&amp;lt;h2&amp;gt;게시글 목록&amp;lt;/h2&amp;gt;
    	&amp;lt;table class=&quot;board_list&quot;&amp;gt;
	        &amp;lt;colgroup&amp;gt;
	            &amp;lt;col width=&quot;15%&quot;/&amp;gt;
	            &amp;lt;col width=&quot;*&quot;/&amp;gt;
	            &amp;lt;col width=&quot;15%&quot;/&amp;gt;
	            &amp;lt;col width=&quot;20%&quot;/&amp;gt;
	        &amp;lt;/colgroup&amp;gt;
	        &amp;lt;thead&amp;gt;
	            &amp;lt;tr&amp;gt;
	                &amp;lt;th scope=&quot;col&quot;&amp;gt;글번호&amp;lt;/th&amp;gt;
	                &amp;lt;th scope=&quot;col&quot;&amp;gt;제목&amp;lt;/th&amp;gt;
	                &amp;lt;th scope=&quot;col&quot;&amp;gt;조회수&amp;lt;/th&amp;gt;
	                &amp;lt;th scope=&quot;col&quot;&amp;gt;작성일&amp;lt;/th&amp;gt;
	            &amp;lt;/tr&amp;gt;
	        &amp;lt;/thead&amp;gt;
	        &amp;lt;tbody&amp;gt;
			&amp;lt;!-- th:if - 타임리프 문법을 사용 할 경우에 쓰는 if 문 --&amp;gt;
			&amp;lt;!-- if문 내의 조건이 일치해야만 해당 tr이 생성된다. --&amp;gt;
			&amp;lt;!-- th:each - 타임리프 문법을 사용할 경우에 쓰는 반복문 ( ${list}에 들어있는 값을 모두 순회하면서 tr 태그를 생성한다. )--&amp;gt;
	        	&amp;lt;tr th:if=&quot;${#lists.size(list)} &amp;gt; 0&quot; th:each=&quot;list : ${list}&quot;&amp;gt;
	        		&amp;lt;td th:text=&quot;${list.boardIdx}&quot;&amp;gt;&amp;lt;/td&amp;gt;
	        		&amp;lt;!-- th:href -  타임리프 문법을 사용할 경우에 쓰는 href 속성 --&amp;gt;
	        		&amp;lt;!-- href=&quot;/board/&quot; th:attrappend=&quot;href=${list.boardIdx}&quot; 로도 작성 가능하다. --&amp;gt;
	        		&amp;lt;td class=&quot;title&quot;&amp;gt;&amp;lt;a th:href=&quot;'/board/' + ${list.boardIdx}&quot;  th:text=&quot;${list.title}&quot;&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;
	        		&amp;lt;td th:text=&quot;${list.hitCnt}&quot;&amp;gt;&amp;lt;/td&amp;gt;
	        		&amp;lt;td th:text=&quot;${list.createdDatetime}&quot;&amp;gt;&amp;lt;/td&amp;gt;
	        	&amp;lt;/tr&amp;gt; 
	        	&amp;lt;!-- th:unless - 타임리프 문법을 사용할 경우에 쓴는 if 문 / 단, if와 반대로 조건이 일치하지 않을 경우 tr 생성 --&amp;gt;
	        	&amp;lt;tr th:unless=&quot;${#lists.size(list)} &amp;gt; 0&quot;&amp;gt;
	        		&amp;lt;td colspan=&quot;4&quot;&amp;gt;조회된 결과가 없습니다.&amp;lt;/td&amp;gt;
	        	&amp;lt;/tr&amp;gt;
	        &amp;lt;/tbody&amp;gt;
	    &amp;lt;/table&amp;gt;
	    &amp;lt;a href=&quot;/board/write&quot; class=&quot;btn&quot;&amp;gt;글 쓰기&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2690&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5xFm6/btstyxu5MwW/layYK8U3hh5ncGIn597aIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5xFm6/btstyxu5MwW/layYK8U3hh5ncGIn597aIK/img.png&quot; data-alt=&quot;완성! 현재 작성된 글이 없기 때문에 th:unless 구문이 작동했다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5xFm6/btstyxu5MwW/layYK8U3hh5ncGIn597aIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5xFm6%2Fbtstyxu5MwW%2FlayYK8U3hh5ncGIn597aIK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2690&quot; height=&quot;567&quot; data-origin-width=&quot;2690&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;완성! 현재 작성된 글이 없기 때문에 th:unless 구문이 작동했다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;게시글 작성과 게시글 조회도 같은 방법으로 &lt;span style=&quot;color: #a6bc00;&quot;&gt;HTTP URL(자원/Resource)&lt;/span&gt;와 &lt;span style=&quot;color: #a6bc00;&quot;&gt;HTTP Method&lt;/span&gt;를 지켜서 작성하면 된다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Controller&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694342100377&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 게시글 등록 화면
// 등록화면은 DB로부터 받을 데이터가 없기 때문에 로직은 따로 없다.
// 그냥 HTML 경로만 리턴해주면 된다.
@GetMapping(&quot;/board/view&quot;)
public String openBoardWrite(Model model) throws Exception{
    return &quot;/board/restBoardWrite&quot;;
}

// 게시글 등록 처리 (Create / 등록 HTTP Method - POST)
@PostMapping(&quot;/board&quot;)
public String insertBoard(BoardDto board) throws Exception{
	// BoardDto에 작성한 데이터를 담아 DB에 전달해준다. (parameterType 존재)
    boardService.insertBoard(board);

	// 전체 목록 조회 경로로 새로고침
    return &quot;redirect:/board&quot;;
}

// 게시글 상세 조회 화면
@GetMapping(&quot;/board/view/{boardIdx}&quot;)
public String openBoardDetail(@PathVariable(&quot;boardIdx&quot;) int boardIdx, Model model) throws Exception{
    // @PathVariable : 요청(Request)을 받아와서 리소스에 사용 가능
    // (&quot;매핑에 사용할 변수 이름&quot;) 변수와 묶어 줄 매개변수
	
    // BoardDto에 boardInd(PK)를 담아 DB에 전달해준다. (parameterType 존재)
    // BoardDto에 전달 받은 데이터를 담아서 view에 전달해준다.
    BoardDto board = boardService.selectBoardDetail(boardIdx);
    
    model.addAttribute(&quot;board&quot;, board);

    return &quot;/board/restBoardDetail&quot;;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #456771;&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Service&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694342167688&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 게시글 등록
public void insertBoard(BoardDto board) throws Exception{
    boardMapper.insertBoard(board);
}

// 게시글 상세 내용 조회
public BoardDto selectBoardDetail(int boardIdx) throws Exception{
    // 상세 내용 조회 -&amp;gt; 조회수 증가
    boardMapper.updateHitCount(boardIdx);

    // 상세 내용 가져오기
    BoardDto boardDetail = boardMapper.selectBoardDetail(boardIdx);

    return boardDetail;
}​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;dao&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;pre id=&quot;code_1694342256677&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;insert id=&quot;insertBoard&quot; parameterType=&quot;BoardDto&quot;&amp;gt;
/* 게시글 등록 */
&amp;lt;![CDATA[
    INSERT INTO t_board
    (
        title,
        contents,
        created_datetime,
        creator_id
    )
    VALUES
    (	#{title},
        #{contents},
        NOW(),
        'admin'
    )
]]&amp;gt;
&amp;lt;/insert&amp;gt;

&amp;lt;update id=&quot;updateHitCount&quot; parameterType=&quot;int&quot;&amp;gt;
/* 조회수 증가 */
&amp;lt;![CDATA[
    UPDATE
        t_board
    SET
        hit_cnt = hit_cnt + 1
    WHERE
        board_idx = #{boardIdx}
]]&amp;gt;
&amp;lt;/update&amp;gt;

&amp;lt;select id=&quot;selectBoardDetail&quot; parameterType=&quot;int&quot; resultType=&quot;BoardDto&quot;&amp;gt;
/* 해당 게시글 조회 */
&amp;lt;![CDATA[
    SELECT
        board_idx,
        title,
        contents,
        hit_cnt,
        creator_id,
        created_datetime
    FROM
        t_board
    WHERE
        board_idx = #{boardIdx}
]]&amp;gt;
&amp;lt;/select&amp;gt;​&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #456771; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;​게시글&amp;nbsp;조회&amp;nbsp;html에서&amp;nbsp;목록으로&amp;nbsp;버튼&amp;nbsp;누르면&amp;nbsp;첫&amp;nbsp;화면(전체&amp;nbsp;목록&amp;nbsp;조회)로&amp;nbsp;돌아가기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694342626382&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;a href=&quot;/board&quot; id=&quot;list&quot; class=&quot;btn&quot;&amp;gt;목록으로&amp;lt;/a&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #456771;&quot;&gt;&lt;b&gt;게시글 삭제 할 때 다시 한 번 묻게 하기&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694415674664&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form id=&quot;frm&quot; th:method=&quot;delete&quot; th:action=&quot;'/board/delete/' + ${boardIdx}&quot;&amp;gt;
	..... 입력 내용들 ....
&amp;lt;/form&amp;gt;

&amp;lt;button type=&quot;button&quot; id=&quot;deleteBtn&quot; class=&quot;btn&quot;&amp;gt;삭제하기&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1694416246350&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const $deleteBtn = document.querySelector('#deleteBtn');
const $deleteForm = document.querySelector('#frm');

$deleteBtn.addEventListener('click', e =&amp;gt; {
    const result = confirm(&quot;글을 삭제하시겠습니까?&quot;);
    if(result === true){
        $deleteForm.submit();
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2630&quot; data-origin-height=&quot;831&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8KAPa/btstvSgck71/wrhffb7uIOs59AFhfpk6t1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8KAPa/btstvSgck71/wrhffb7uIOs59AFhfpk6t1/img.png&quot; data-alt=&quot;게시글 등록 화면으로 이동 성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8KAPa/btstvSgck71/wrhffb7uIOs59AFhfpk6t1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8KAPa%2FbtstvSgck71%2Fwrhffb7uIOs59AFhfpk6t1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2630&quot; height=&quot;831&quot; data-origin-width=&quot;2630&quot; data-origin-height=&quot;831&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;게시글 등록 화면으로 이동 성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2637&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2J97t/btstw3BxRkD/pA2woMvhQqHMdpW4GzkUn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2J97t/btstw3BxRkD/pA2woMvhQqHMdpW4GzkUn1/img.png&quot; data-alt=&quot;게시글 등록 처리와 조회수 증가 성공 (글 번호는 내가 몇 개 썼다 지웠더니 9가 됐다..)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2J97t/btstw3BxRkD/pA2woMvhQqHMdpW4GzkUn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2J97t%2Fbtstw3BxRkD%2FpA2woMvhQqHMdpW4GzkUn1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2637&quot; height=&quot;543&quot; data-origin-width=&quot;2637&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;게시글 등록 처리와 조회수 증가 성공 (글 번호는 내가 몇 개 썼다 지웠더니 9가 됐다..)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/e-sori/Rest-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;자세한 코드 보기 (깃허브)&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1694412007474&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - e-sori/Rest-api: Restful 게시판 / Rest API&quot; data-og-description=&quot;Restful 게시판 / Rest API . Contribute to e-sori/Rest-api development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/e-sori/Rest-api&quot; data-og-url=&quot;https://github.com/e-sori/Rest-api&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bvpp1h/hyTVQ13cHB/dBD0tzHHdUXwzcKZcn4aQk/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_151_1034_226&quot;&gt;&lt;a href=&quot;https://github.com/e-sori/Rest-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/e-sori/Rest-api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bvpp1h/hyTVQ13cHB/dBD0tzHHdUXwzcKZcn4aQk/img.png?width=1200&amp;amp;height=600&amp;amp;face=965_151_1034_226');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - e-sori/Rest-api: Restful 게시판 / Rest API&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Restful 게시판 / Rest API . Contribute to e-sori/Rest-api development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>REST API</category>
      <author>낫쏘링</author>
      <guid isPermaLink="true">https://leeri-study.tistory.com/152</guid>
      <comments>https://leeri-study.tistory.com/152#entry152comment</comments>
      <pubDate>Sun, 10 Sep 2023 19:22:38 +0900</pubDate>
    </item>
  </channel>
</rss>