<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발일기</title>
    <link>https://chb2005.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 00:18:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>chb2005</managingEditor>
    <item>
      <title>[JAVA] 최장 증가 부분 수열 (LIS, Longest Increasing Subsequence)</title>
      <link>https://chb2005.tistory.com/204</link>
      <description>&lt;h1&gt;LIS 란?&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Longest Increasing Subsequence&lt;/li&gt;
&lt;li&gt;ex) 수열 A = {10, 20, 10, 30, 20, 50}가 주어지고 가장 긴 증가하는 부분 수열을 구하는 문제
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;증가하는 부분 수열을 모두 구해보면 아래와 같음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;{10}, {20}, {30}, {50}&lt;/li&gt;
&lt;li&gt;{10, 20}, {10, 30}, {20, 30}, {20, 50}, {30, 50}&lt;/li&gt;
&lt;li&gt;{10, 20, 30}, {10, 20, 50}, {10, 30, 50}, {20, 30, 50}&lt;/li&gt;
&lt;li&gt;{10, 20, 30, 50}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 중 가장 길이가 긴 부분 수열은 {10, 20, 30, 50}&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;해결 방법 1 - O(n^2)&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 쉬운 해결 방법은 일반적인 dp를 사용하는 방법&lt;/li&gt;
&lt;li&gt;dp[i] = dp[i - 1]과 arr[i]를 포함시켜 만들 수 있는 가장 긴 증가하는 부분수열을 비교하여 더 큰 값을 넣어주면 됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;arr[i]를 포함시켜 만들 수 있는 가장 긴 증가하는 부분수열을 구하는 방법은 arr[0] ~ arr[i - 1] 중 arr[i]보다 값이 작으면서 dp의 해당 index의 값이 가장 큰 값 + 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ex) arr[] = {10, 20, 10, 30, 20, 50}이 주어졌을 때
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp[0] = 1 (10을 넣는 방법)&lt;/li&gt;
&lt;li&gt;dp[1] = 2 (10, 20을 넣는 방법), 1 (dp[0]) 중 큰 값 = 2&lt;/li&gt;
&lt;li&gt;dp[2] = 1 (10을 넣는 방법), 2 (dp[1]) 중 큰 값 = 2&lt;/li&gt;
&lt;li&gt;dp[3] = 3 (10, 20, 30을 넣는 방법), 2 (dp[2]) 중 큰 값 = 3&lt;/li&gt;
&lt;li&gt;dp[4] = 2 (10, 20을 넣는 방법), 3 (dp[3]) 중 큰 값 = 3&lt;/li&gt;
&lt;li&gt;dp[5] = 4 (10, 20, 30, 50)을 넣는 방법, 3 (dp[4]) 중 큰 값 = 4&lt;/li&gt;
&lt;li&gt;정답 = 4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;해결 방법 2 - O(nlogn)&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결 방법 1은 이해하기도 쉽고 구현하기도 쉽지만 arr[i]를 포함시켜 만들 수 있는 가장 긴 증가하는 부분수열을 구하기 위해선 0 ~ i-1의 반복문이 필요함
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간 복잡도가 n^2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;더 빠르게 찾기 위해서 arr[i]를 포함시켜 만들 수 있는 가장 긴 증가하는 부분수열을 구하는 시간을 줄여보자
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;dp + 이분 탐색을 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;arr[] = {10, 20, 9, 30, 20, 50}이 주어졌다고 가정&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;List 타입의 dp에 0 삽입 (arr의 각각의 값이 1 이상이기 때문)&lt;/li&gt;
&lt;li&gt;i = 0일 때, List 타입의 dp에 arr[0] 삽입&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Zb20h/btsrBQEfuFb/sUvEvn3Tjni7IcvQw36Wb0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;i = 1일 때, dp의 마지막 숫자(10)가 arr[i](20)보다 작다면 삽입&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2EOrX/btsrypfSZmh/7rq7aGIGvpDYEr3zWOxSR0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;i = 2일 때, dp의 마지막 숫자(20)이 arr[i](9)보다 크거나 같음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이런 경우에는 이분 탐색을 통해 dp에서 9보다 작은 값 중에서 최대값을 찾음(0)&lt;/li&gt;
&lt;li&gt;0 뒤에 9 삽입&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vhhqV/btsrH9I2Svo/EWKMn2Ls0GxmfnlIVDubW0/img.png&quot; alt=&quot;&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 dp에는 9, 20이 들어있지만 이는 {9, 20}이 가장 긴 증가하는 부분수열이라는 뜻이 아닌 길이가 2가 최대라는 의미&lt;/li&gt;
&lt;li&gt;이후에 10이 나온다면 9 뒤에 10을 넣음으로써 {9, 10}으로 만들 수 있기 때문에 위와 같은 작업을 한 것임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;i = 3일 때, dp의 마지막 숫자(20)이 arr[i](30)보다 작기 때문에 삽입&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NBtod/btsrDGUWUxO/L0OnqtKWEQkGSmWzCr23C0/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;i = 4일 때, dp의 마지막 숫자(30)이 arr[i](20)보다 크거나 같기 때문에 9 뒤에 20 삽입&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oy2zM/btsryswRT7h/HvgNDWnnl1jzRh3ht2aRe1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;i = 5일 때, dp의 마지막 숫자(30)이 arr[i](50)보다 작기 때문에 삽입&lt;br /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kHuQi/btsrysjhAcO/fvbcalgdYDhdxpdJMP6DN1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;dp의 길이 - 1(첫번째 0 제외) = 4가 정답, 이 때 {9, 20, 30, 50}이 가장 긴 증가하는 부분수열은 아님&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;구현&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/12015&quot;&gt;백준 12015번 : 가장 긴 증가하는 부분 수열 2&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;코드&lt;/h1&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class p12015 {

    static List&amp;lt;Integer&amp;gt; dp;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr = new int[n];
        for (int i = 0 ; i &amp;lt; n ; i ++) {
            arr[i] = sc.nextInt();
        }

        dp = new ArrayList&amp;lt;&amp;gt;();
        dp.add(0);
        dp.add(arr[0]);

        for (int i = 1 ; i &amp;lt; n ; i ++) {
            if (arr[i] &amp;gt; dp.get(dp.size() - 1)) {
                dp.add(arr[i]);
            } else {
                // dp에서 arr[i]보다 작은값 중 가장 큰 값 바로 뒤의 값을 arr[i]로 수정
                // arr[i]보다 작은값 중 가장 큰 값을 찾기 위해 이분 탐색 사용
                binary_search(arr[i]);
            }
        }

        System.out.println(dp.size() - 1);
    }

    private static void binary_search(int x) {
        int left = 0;
        int right = dp.size() - 1;
        int mid = 0;

        while (left &amp;lt;= right) {
            mid = (left + right) / 2;
            if (dp.get(mid) &amp;gt;= x) {
                right = mid - 1;
            } else {
                if (dp.get(mid) &amp;lt; x &amp;amp;&amp;amp; dp.get(mid + 1) &amp;gt;= x) {
                    break;
                } else {
                    left = mid + 1;
                }
            }
        }

        dp.set(mid + 1, x);
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>JAVA/알고리즘 개념 정리</category>
      <category>JAVA LIS.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/204</guid>
      <comments>https://chb2005.tistory.com/204#entry204comment</comments>
      <pubDate>Sun, 20 Aug 2023 16:25:12 +0900</pubDate>
    </item>
    <item>
      <title>[RDS MySQL] Too many connections 해결 방법</title>
      <link>https://chb2005.tistory.com/203</link>
      <description>&lt;h1&gt;문제 상황&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS RDS (MySQL) 사용 중 DB에 접속이 안 됨&lt;/li&gt;
&lt;li&gt;Spring Boot 프로젝트 실행 시 ERROR 1040 (08004): Too many connections 에러 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;원인&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 프로젝트에서도 해당 DB를 사용하는데, Connection이 중지되지 않고 계속 유지되서 사용할 수 있는 Connection이 없는 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;해결 방법&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDS DB의 최대 Connections 수 (max_connections)를 늘려주고, 일정 시간이 지나면 Connection을 중지 시키도록 connect_timeout을 설정하여 해결함&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS RDS console 접속&lt;/li&gt;
&lt;li&gt;좌측 탭의 파라미터 그룹 클릭&lt;/li&gt;
&lt;li&gt;만약 파라미터 그룹이 하나 있으면 파라미터 그룹 생성 (default는 수정할 수 없기 때문)&lt;/li&gt;
&lt;li&gt;아래와 같이 'my-default'라는 그룹을 만들었음&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EpNR5/btsdG59SUeZ/94bfL8XDDQC4XXFu4h7gs0/img.png&quot; alt=&quot;&quot; width=&quot;772&quot; height=&quot;380&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;5&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;'my-default'를 클릭하면 파라미터를 수정할 수 있음&lt;/li&gt;
&lt;li&gt;아래와 같이 connect_timeout(180), max_connections(100) 으로 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TSyEQ/btsdOBsDufv/4soKP6Rp5kqsQKHAU6ADeK/img.png&quot; alt=&quot;&quot; width=&quot;677&quot; height=&quot;284&quot; /&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RcXrV/btsdJqFJncC/m7EJDFLfPRsDtKVj3pWX40/img.png&quot; alt=&quot;&quot; width=&quot;678&quot; height=&quot;212&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;7&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;변경사항 저장&lt;/li&gt;
&lt;li&gt;이제 에러가 난 DB에서 수정 클릭&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOkKvh/btsdIp08e6g/UwMLX9ZeuIr2pUKjKmo4ZK/img.png&quot; alt=&quot;&quot; width=&quot;723&quot; height=&quot;205&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;9&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB 수정페이지에서 'DB 파라미터 그룹'을 방금 생성한 그룹으로 설정 후 계속&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bto21a/btsdPdrhGd2/5cVaHQanafh1uY8wJGKtY1/img.png&quot; alt=&quot;&quot; width=&quot;632&quot; height=&quot;322&quot; /&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;10&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;'수정 예약'을 즉시 적용 선택 후 DB 인스턴스 수정&lt;/li&gt;
&lt;li&gt;마지막으로 DB를 재부팅하면 파라미터가 적용되어 에러 해결&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Trouble Shooting</category>
      <category>RDS MySQL Too many connections 해결 방법.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/203</guid>
      <comments>https://chb2005.tistory.com/203#entry203comment</comments>
      <pubDate>Wed, 3 May 2023 14:11:34 +0900</pubDate>
    </item>
    <item>
      <title>[Linux] Linux(EC2) 용량 부족 문제 해결 방법</title>
      <link>https://chb2005.tistory.com/202</link>
      <description>&lt;h1&gt;문제 상황&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트를 AWS EC2(Linux 환경)에 배포하는 과정에 용량이 부족하다는 메세지가 출력됨
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;No space left on device&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장치에 남은 공간이 없어 작업을 진행할 수 없는 상황&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;원인&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장치에 남은 공간이 없어서 발생하는 에러&lt;/li&gt;
&lt;li&gt;프로젝트의 용량이 크지도 않고, 불필요한 컨테이너와 이미지들도 모두 삭제하고, 로컬에 파일을 저장하는 등의 로직도 없는데도 공간이 부족해짐&lt;/li&gt;
&lt;li&gt;원인은 Docker를 사용하면서 컨테이너들을 다루게 되는데, 이 때 사용하지 않는 불필요한 리소스들이 생성되었기 때문이라 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;해결 방법&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 명령어를 통해 불필요한 리소스들을 제거할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker system prune -a -f&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 방법으로는 EC2 인스턴스의 용량을 늘리면 되긴 함&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;추가 - 남은 용량 확인 방법&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 명령어를 통해 현재 남은 용량을 확인할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;df -h&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 리소스 제거 전 (59% 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biRUOV/btsbmcV7TCe/Kntuvhhp3HQYPCHyj5lLd1/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불필요한 리소스 제거 후 (46% 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NsdBJ/btsbk5XyXCH/m09M8hhAPGB9X8ssTJ9kBk/img.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;추가 - crontab을 활용하여 주기적으로 불필요한 리소스 제거&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://chb2005.tistory.com/190&quot;&gt;[CI/CD] GitLab을 활용한 CI/CD 파이프라인 구축 (+ Linux Crontab)&lt;/a&gt; 해당 글 아래쪽에 Linux Crontab에 대해 정리해 놓았으니 참고&lt;/li&gt;
&lt;li&gt;매일 오전 5시에 불필요한 리소스들을 제거하는 명령어는 아래와 같음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;0 5 * * * docker system prune -a -f&lt;/code&gt;&lt;/pre&gt;</description>
      <category>기타</category>
      <category>Linux(EC2) 용량 부족 문제 해결 방법.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/202</guid>
      <comments>https://chb2005.tistory.com/202#entry202comment</comments>
      <pubDate>Wed, 19 Apr 2023 15:54:48 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 프로젝트 TimeZone 설정</title>
      <link>https://chb2005.tistory.com/201</link>
      <description>&lt;h1&gt;문제 상황&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot 프로젝트를 로컬에서 실행했을 때는 DB에 데이터를 삽입할 때, createdAt이 제대로 삽입됨&lt;/li&gt;
&lt;li&gt;하지만 EC2로 배포 후 데이터를 삽입하면 한국 시간으로 들어가지 않고, 시간이 UTC로 들어가는 문제가 발생함 (한국 시간 -9)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;원인&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EC2 인스턴스 자체의 TimeZone을 바꿔봐도 해당 문제는 여전히 발생&lt;/li&gt;
&lt;li&gt;프로젝트 자체의 TimeZone이 문제라고 생각함&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;해결 방법&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래의 코드를 통해 프로젝트의 TimeZone을 서울로 맞춰주면 됨&lt;/li&gt;
&lt;li&gt;이 코드는 프로젝트의 main 클래스나 @Component로 등록한 클래스 내부에 적어줘야 실행됨&lt;/li&gt;
&lt;li&gt;@PostConstruct는 프로젝트가 처음 실행될 때, 한 번만 실행 시켜주는 어노테이션&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@PostConstruct
public void setTimeZone(){
    TimeZone.setDefault(TimeZone.getTimeZone(&quot;Asia/Seoul&quot;));
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Trouble Shooting</category>
      <category>Spring Boot TimeZone 설정.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/201</guid>
      <comments>https://chb2005.tistory.com/201#entry201comment</comments>
      <pubDate>Wed, 19 Apr 2023 15:32:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] AWS S3를 이용한 파일 업로드</title>
      <link>https://chb2005.tistory.com/200</link>
      <description>&lt;h1&gt;AWS S3 란?&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWS Simple Storage Service의 줄임말로 파일 서버의 역할을 하는 서비스&lt;/li&gt;
&lt;li&gt;프로젝트 개발 중 파일을 저장하고 불러오는 작업이 필요한 경우에 프로젝트 내부 폴더에 저장할 수 있지만, AWS S3를 사용하여 파일을 관리할 수도 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AWS S3의 장점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;무제한 용량 (하나의 파일에 대한 용량 제한은 있지만, 전체 용량은 무제한)&lt;/li&gt;
&lt;li&gt;파일 저장에 최적화 (개발자가 따로 용량을 추가하거나 성능을 높이는 작업을 하지 않아도 됨)&lt;/li&gt;
&lt;li&gt;99.999%라는 높은 내구도 (파일이 유실될 가능성이 낮음)&lt;/li&gt;
&lt;li&gt;이 외에도 저렴한 비용, 높은 객체 가용성, 뛰어난 보안성 등의 장점이 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;AWS S3 생성&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체(Object) : 파일과 파일정보로 구성된 저장단위로 파일이라 생각하면 됨&lt;/li&gt;
&lt;li&gt;버킷(Bucket) : 저장된 객체에 대한 컨테이너&lt;/li&gt;
&lt;li&gt;버킷은 최대 100개 생성 가능하며, 버킷에 저장할 수 있는 객체수는 제한이 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bucket 생성&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS Console 접속 후 S3 서비스 선택&lt;/li&gt;
&lt;li&gt;버킷 만들기 클릭&lt;/li&gt;
&lt;li&gt;원하는 버킷 이름 입력&lt;/li&gt;
&lt;li&gt;AWS 리전 선택 (아시아 태평양(서울) ap-northeast-2 )&lt;/li&gt;
&lt;li&gt;객체 소유권 선택 (ACL 비활성화)&lt;/li&gt;
&lt;li&gt;'모든 퍼블릭 액세스 차단' 해제&lt;/li&gt;
&lt;li&gt;나머지는 Default&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용자 생성&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS Console 접속 후 IAM 서비스 선택&lt;/li&gt;
&lt;li&gt;액세스 관리 -&amp;gt; 사용자 -&amp;gt; 사용자 추가&lt;/li&gt;
&lt;li&gt;원하는 이름 입력 후 다음&lt;/li&gt;
&lt;li&gt;직접 정책 연결 -&amp;gt; AmazonS3FullAccess 선택 후 다음&lt;/li&gt;
&lt;li&gt;사용자 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;액세스 키 생성&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;방금 생성한 사용자 선택 후 보안 자격 증명 탭 클릭&lt;/li&gt;
&lt;li&gt;액세스 키 만들기 클릭&lt;/li&gt;
&lt;li&gt;기타 선택 후 다음&lt;/li&gt;
&lt;li&gt;원하는 설명 태그 입력 후 액세스 키 만들기&lt;/li&gt;
&lt;li&gt;액세스 키를 만들면 아래와 같이 액세스 키와 비밀 액세스 키를 확인할 수 있는데, 이 값들을 저장후 완료&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNT6MY/btsaV3fDA0k/5TBGBioAJaGOFfKubaUjM1/img.png&quot; alt=&quot;&quot; width=&quot;691&quot; height=&quot;340&quot; /&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;버킷 정책 변경&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AWS Console에서 생성한 버킷으로 이동&lt;/li&gt;
&lt;li&gt;권한 -&amp;gt; 버킷 정책 -&amp;gt; 편집&lt;/li&gt;
&lt;li&gt;정책이 비어있으면 '+ 새 문 추가' 클릭&lt;/li&gt;
&lt;li&gt;정책 내용을 아래와 같이 변경&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Sid&quot;: &quot;Statement1&quot;,
            &quot;Principal&quot;: &quot;*&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &quot;s3:*&quot;,
            &quot;Resource&quot;: &quot;arn:aws:s3:::&amp;lt;버킷 이름&amp;gt;/*&quot;
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Spring 프로젝트와 연동&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;라이브러리 추가 (build.gradle)&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;application.yml에 아래 내용 추가&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;cloud:
  aws:
    s3:
      bucket: &amp;lt;S3 버킷 이름&amp;gt;
    credentials:
      access-key: &amp;lt;저장해놓은 액세스 키&amp;gt;
      secret-key: &amp;lt;저장해놓은 비밀 액세스 키&amp;gt;
    region:
      static: ap-northeast-2
      auto: false
    stack:
      auto: false&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;S3Config.java 생성&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class S3Config {

    @Value(&quot;${cloud.aws.credentials.access-key}&quot;)
    private String accessKey;

    @Value(&quot;${cloud.aws.credentials.secret-key}&quot;)
    private String secretKey;

    @Value(&quot;${cloud.aws.region.static}&quot;)
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return (AmazonS3Client) AmazonS3ClientBuilder
                .standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 업로드 구현&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class S3UploadService {

    private final AmazonS3 amazonS3;

    @Value(&quot;${cloud.aws.s3.bucket}&quot;)
    private String bucket;

    public String saveFile(MultipartFile multipartFile) throws IOException {
        String originalFilename = multipartFile.getOriginalFilename();

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(multipartFile.getSize());
        metadata.setContentType(multipartFile.getContentType());

        amazonS3.putObject(bucket, originalFilename, multipartFile.getInputStream(), metadata);
        return amazonS3.getUrl(bucket, originalFilename).toString();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;putObject() 메소드가 파일을 저장해주는 메소드&lt;/li&gt;
&lt;li&gt;getURl()을 통해 파일이 저장된 URL을 return 해주고, 이 URL로 이동 시 해당 파일이 오픈됨 (버킷 정책 변경을 하지 않았으면 파일은 업로드 되지만 해당 URL로 이동 시 accessDenied 됨)&lt;/li&gt;
&lt;li&gt;만약 MultipartFile에 대해 잘 모르거나 웹 페이지에서 form으로 파일을 입력받고 싶다면 &lt;a href=&quot;https://chb2005.tistory.com/102&quot;&gt;[Spring Boot] 파일 업로드&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 다운로드 구현&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public ResponseEntity&amp;lt;UrlResource&amp;gt; downloadImage(String originalFilename) {
    UrlResource urlResource = new UrlResource(amazonS3.getUrl(bucket, originalFilename));

    String contentDisposition = &quot;attachment; filename=\&quot;&quot; +  originalFilename + &quot;\&quot;&quot;;

    // header에 CONTENT_DISPOSITION 설정을 통해 클릭 시 다운로드 진행
    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
            .body(urlResource);

}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 파일 다운로드 할 때에는 UrlResource() 메소드에 &quot;file:&quot; + 로컬 파일 경로를 넣어주면 로컬 파일이 다운로드 되었음&lt;/li&gt;
&lt;li&gt;S3에 올라간 파일은 위와 같이 amazonS3.getUrl(버킷이름, 파일이름)을 통해 파일 다운로드를 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTML 파일에서 이미지 미리보기 구현&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;String url = amazonS3.getUrl(bucket, filename).toString();&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 구한 amazonS3 URL을 Model에 담아 HTML 파일로 전송&lt;/li&gt;
&lt;li&gt;아래와 같이 Thymeleaf를 이용해 URL을 받고 src에 넣어주면 화면에서 이미지 미리보기 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;img th:src=&quot;${s3ImageUrl}&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;파일 삭제 구현&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void deleteImage(String originalFilename)  {
    amazonS3.deleteObject(bucket, originalFilename);
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Spring Boot/문법 정리</category>
      <category>Spring Boot AWS S3 파일 업로드.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/200</guid>
      <comments>https://chb2005.tistory.com/200#entry200comment</comments>
      <pubDate>Tue, 18 Apr 2023 17:59:45 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 게시판 만들기 6 - 화면 제작</title>
      <link>https://chb2005.tistory.com/199</link>
      <description>&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML, Thymeleaf, BootStrap, JavaScript, JQuery를 사용하여 화면 제작&lt;/li&gt;
&lt;li&gt;디자인이나 화면에 대해 자세히는 정리하지는 않고 핵심적인 부분만 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Changbum97/SpringBoot-Basic-Board/tree/main/src/main/resources/templates&quot;&gt;화면 관련 코드&lt;/a&gt;에 전체 코드가 있으니 참고&lt;/li&gt;
&lt;li&gt;Thyemleaf와 관련된 것은 &lt;a href=&quot;https://chb2005.tistory.com/77&quot;&gt;[Spring Boot] Thymeleaf 기능 정리&lt;/a&gt;, &lt;a href=&quot;https://chb2005.tistory.com/82&quot;&gt;[Spring Boot] Thymeleaf - form 관련 기능 정리&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;printMessage.html&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 페이지는 내용을 출력하기 위한 페이지가 아닌 javascript를 통해 메세지를 출력해주고, 바로 다음 페이지로 이동시켜주는 페이지&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&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;게시판&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;script th:inline=&quot;javascript&quot;&amp;gt;
    window.onload = function () {
      if([[${message}]] != null) {
        alert([[${message}]])
      }
      window.location.href = [[${nextUrl}]]
    }
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;header.html&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;화면 상단의 nav-bar와 Bootstrap, css 등을 import 해주는 페이지&lt;/li&gt;
&lt;li&gt;모든 페이지에 같은 nav-bar가 들어가기 때문에 한번 만들어 놓고 thymeleaf의 fragment를 통해 다른 페이지에서는 &amp;lt;div th:replace=&quot;fragments/header.html :: header ('admin')&quot;/&amp;gt; 이런 식으로 불러서 사용하기만 하면 됨&lt;/li&gt;
&lt;li&gt;thymeleaf-extras-springsecurity5 라이브러리를 설치하고, html 태그에 xmlns:sec=&quot;http://www.thymeleaf.org/extras/spring-security&quot;를 추가해주면 sec:authorize=&quot;isAuthenticated()&quot;, sec:authorize=&quot;hasAuthority('ADMIN')&quot; 와 같이 인증, 인가를 쉽게 할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&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; xmlns:sec=&quot;http://www.thymeleaf.org/extras/spring-security&quot;&amp;gt;
&amp;lt;head th:fragment=&quot;head&quot;&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;게시판&amp;lt;/title&amp;gt;
    &amp;lt;!-- Bootstrap 5.2.3 Version --&amp;gt;
    &amp;lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65&quot; crossorigin=&quot;anonymous&quot;&amp;gt;

    &amp;lt;!-- My CSS --&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;/css/custom.css&quot;&amp;gt;

    &amp;lt;!-- JQuery --&amp;gt;
    &amp;lt;script src=&quot;https://code.jquery.com/jquery-3.4.1.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src=&quot;https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;/head&amp;gt;

&amp;lt;body&amp;gt;

&amp;lt;div th:fragment=&quot;header (pageName)&quot;&amp;gt;
    &amp;lt;!-- Bootstrap 5.2.3 Version --&amp;gt;
    &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js&quot; integrity=&quot;sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4&quot; crossorigin=&quot;anonymous&quot;&amp;gt;&amp;lt;/script&amp;gt;

    &amp;lt;nav class=&quot;navbar navbar-expand-lg&quot; style=&quot;background-color: limegreen; margin-bottom: 60px;&quot;&amp;gt;
        &amp;lt;div class=&quot;container-fluid&quot;&amp;gt;
            &amp;lt;a class=&quot;navbar-brand&quot; href=&quot;/&quot;&amp;gt;Basic Board&amp;lt;/a&amp;gt;
            &amp;lt;div class=&quot;collapse navbar-collapse&quot; id=&quot;navbarNavDropdown&quot;&amp;gt;
                &amp;lt;!--&amp;lt;ul class=&quot;navbar-nav&quot;&amp;gt;--&amp;gt;
                &amp;lt;ul class=&quot;navbar-nav&quot;&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a th:if=&quot;${pageName == 'home'}&quot; class=&quot;nav-link active&quot; aria-current=&quot;page&quot; href=&quot;/&quot;&amp;gt;Home&amp;lt;/a&amp;gt;
                        &amp;lt;a th:unless=&quot;${pageName == 'home'}&quot; class=&quot;nav-link&quot; aria-current=&quot;page&quot; href=&quot;/&quot;&amp;gt;Home&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a th:if=&quot;${pageName == 'greeting'}&quot; class=&quot;nav-link active&quot; aria-current=&quot;page&quot; href=&quot;/boards/greeting&quot;&amp;gt;가입인사&amp;lt;/a&amp;gt;
                        &amp;lt;a th:unless=&quot;${pageName == 'greeting'}&quot; class=&quot;nav-link&quot; aria-current=&quot;page&quot; href=&quot;/boards/greeting&quot;&amp;gt;가입인사&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a th:if=&quot;${pageName == 'free'}&quot; class=&quot;nav-link active&quot; aria-current=&quot;page&quot; href=&quot;/boards/free&quot;&amp;gt;자유게시판&amp;lt;/a&amp;gt;
                        &amp;lt;a th:unless=&quot;${pageName == 'free'}&quot; class=&quot;nav-link&quot; aria-current=&quot;page&quot; href=&quot;/boards/free&quot;&amp;gt;자유게시판&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;a th:if=&quot;${pageName == 'gold'}&quot; class=&quot;nav-link active&quot; aria-current=&quot;page&quot; href=&quot;/boards/gold&quot;&amp;gt;골드게시판&amp;lt;/a&amp;gt;
                        &amp;lt;a th:unless=&quot;${pageName == 'gold'}&quot; class=&quot;nav-link&quot; aria-current=&quot;page&quot; href=&quot;/boards/gold&quot;&amp;gt;골드게시판&amp;lt;/a&amp;gt;
                    &amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div class=&quot;justify-content-between&quot; id=&quot;navbarNav&quot; style=&quot;padding: 5px 30px&quot;&amp;gt;

                &amp;lt;!-- 로그인 안 했을때 --&amp;gt;
                &amp;lt;ul class=&quot;navbar-nav&quot; sec:authorize=&quot;isAnonymous()&quot;&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;button class=&quot;btn nav-btn&quot; onclick=&quot;location.href = '/users/login'&quot;&amp;gt;로그인&amp;lt;/button&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;button class=&quot;btn nav-btn&quot; onclick=&quot;location.href = '/users/join'&quot;&amp;gt;회원가입&amp;lt;/button&amp;gt;
                    &amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;

                &amp;lt;!-- 로그인 했을때 --&amp;gt;
                &amp;lt;ul class=&quot;navbar-nav&quot; sec:authorize=&quot;isAuthenticated()&quot;&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot; sec:authorize=&quot;hasAuthority('ADMIN')&quot;&amp;gt;
                        &amp;lt;button class=&quot;btn nav-btn&quot; onclick=&quot;location.href = '/users/admin'&quot; style=&quot;width: fit-content&quot;&amp;gt;관리자 페이지&amp;lt;/button&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;button class=&quot;btn nav-btn&quot; onclick=&quot;location.href = '/users/myPage/board'&quot;&amp;gt;마이페이지&amp;lt;/button&amp;gt;
                    &amp;lt;/li&amp;gt;
                    &amp;lt;li class=&quot;nav-item&quot;&amp;gt;
                        &amp;lt;button class=&quot;btn nav-btn&quot; onclick=&quot;location.href = '/users/logout'&quot;&amp;gt;로그아웃&amp;lt;/button&amp;gt;
                    &amp;lt;/li&amp;gt;
                &amp;lt;/ul&amp;gt;

            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/nav&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;확인 메세지 출력&lt;/h1&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;function clickDelete() {
    if (confirm(&quot;해당 글을 삭제 하시겠습니까?&quot;) == true) {
        location.href = &quot;/boards/&quot; + [[${category}]] + &quot;/&quot; + [[${boardDto.id}]] + &quot;/delete&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;boards/detail.html에서 위와 같은 코드가 있음&lt;/li&gt;
&lt;li&gt;이 코드는 버튼을 눌렀을 때, 바로 GET 요청을 보내지 않고 확인 메세지를 출력해 줌&lt;/li&gt;
&lt;li&gt;확인 메세지에서 확인 버튼을 눌러야 if 문 안의 내용이 실행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;페이지 기능 구현&lt;/h1&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;window.onload = function () {
    let nowPage = [[${boards.getNumber()}]] + 1;    // 현재 페이지
    let totalPage = [[${boards.getTotalPages()}]];  // 전체 페이지 수

    let firstPage;  // 화면에 출력될 첫 페이지
    for (let i = nowPage ; i &amp;gt;= 1 ; i --) {
        if(i % 5 == 1) {
            firstPage = i;
            break;
        }
    }

    let lastPage;   // 화면에 출력될 마지막 페이지
    let nextButton; // 다음 버튼 출력 여부
    if (firstPage + 4 &amp;gt;= totalPage) {
        lastPage = totalPage;
        nextButton = false;
    } else {
        lastPage = firstPage + 4;
        nextButton = true;
    }

    // HTML 생성
    let pageHtml = &quot;&quot;;
    pageHtml += &quot;&amp;lt;li&amp;gt;&amp;lt;a class='page-link' href='&quot; + makeUrl(1) +  &quot;'&amp;gt;&amp;amp;laquo;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&quot;;
    if (firstPage != 1) {
        pageHtml += &quot;&amp;lt;li&amp;gt;&amp;lt;a class='page-link' href='&quot; + makeUrl(firstPage - 1) +  &quot;'&amp;gt;&amp;amp;lsaquo;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&quot;;
    }

    for (let i = firstPage; i &amp;lt;= lastPage; i++) {
        if (i == nowPage) {
            pageHtml += &quot;&amp;lt;li class='page-item active'&amp;gt;&amp;lt;a class= 'page-link'&amp;gt;&quot; + i + &quot;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&quot;;
        } else {
            pageHtml += &quot;&amp;lt;li class='page-item'&amp;gt;&amp;lt;a class= 'page-link' href='&quot; + makeUrl(i) + &quot;'&amp;gt;&quot; + i + &quot;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&quot;;
        }
    }

    if (nextButton) {
        pageHtml += &quot;&amp;lt;li&amp;gt;&amp;lt;a class='page-link' href='&quot; + makeUrl(lastPage + 1) +  &quot;'&amp;gt;&amp;amp;rsaquo;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&quot;;
    }
    pageHtml += &quot;&amp;lt;li&amp;gt;&amp;lt;a class='page-link' href='&quot; + makeUrl(totalPage) +  &quot;'&amp;gt;&amp;amp;raquo;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&quot;;

    $(&quot;#paging-ul&quot;).html(pageHtml);
}

function makeUrl(page) {
    let category = [[${category}]];
    let url = &quot;/boards/&quot; + category + &quot;?page=&quot; + page;

    // 검색 했으면 다음 URL에도 추가
    let sortType = [[${boardSearchRequest.sortType}]];
    let searchType = [[${boardSearchRequest.searchType}]];
    let keyword = [[${boardSearchRequest.keyword}]];

    if (sortType != null) {
        url += &quot;&amp;amp;sortType=&quot; + sortType;
    }
    if (searchType != null) {
        url += &quot;&amp;amp;searchType=&quot; + searchType + &quot;&amp;amp;keyword=&quot; + keyword;
    }

    return url;
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;boards/list.html에서 위와 같은 코드가 있음&lt;/li&gt;
&lt;li&gt;이 코드는 해당 html 파일이 open 될 때 페이지 버튼을 만드는 코드&lt;/li&gt;
&lt;li&gt;이 사이트에서는 1 ~ 5, ^ ~ 10과 같이 5개의 페이지 단위로 버튼을 생성함&lt;/li&gt;
&lt;li&gt;Model로 넘어온 값들을 Thymeleaf 문법을 사용해 받고, 현재 페이지와 전체 페이지를 통해 첫번째 페이지, 마지막 페이지를 계산하고, 버튼을 생성함&lt;/li&gt;
&lt;li&gt;또한, 정렬, 검색을 했다면 sortType, searchType, keyword가 넘어오게 되는데, 이 값들을 url에 추가해줌으로써, 검색, 정렬 후 페이지 이동을 했을 때, 검색, 정렬 결과가 유지될 수 있게 해줌&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;파일 입력&lt;/h1&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;form class=&quot;offset-3 col-6 form-group&quot; th:object=&quot;${boardCreateRequest}&quot; th:method=&quot;post&quot;
        th:action=&quot;|@{/boards/{category} (category = ${category})}|&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label th:for=&quot;title&quot;&amp;gt;제목 : &amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;text&quot; th:field=&quot;*{title}&quot; style=&quot;width: 50%&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;br/&amp;gt;

    &amp;lt;div&amp;gt;
        &amp;lt;label th:for=&quot;body&quot;&amp;gt;내용 : &amp;lt;/label&amp;gt;
        &amp;lt;textarea rows=&quot;10&quot; style=&quot;width: 100%;&quot; th:field=&quot;*{body}&quot;/&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;br/&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;label th:for=&quot;uploadImage&quot;&amp;gt;이미지 첨부 : &amp;lt;/label&amp;gt;
        &amp;lt;input type=&quot;file&quot; th:field=&quot;*{uploadImage}&quot; style=&quot;width: 50%&quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;br/&amp;gt;
    &amp;lt;div align=&quot;center&quot;&amp;gt;
        &amp;lt;button class=&quot;btn post-btn&quot; type=&quot;submit&quot;&amp;gt;등록&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드는 제목, 내용, 이미지를 입력받는 코드&lt;/li&gt;
&lt;li&gt;form을 통해 text를 입력 받는것 외에도 파일을 입력 받아야 하기 때문에 form 태그에 enctype=&quot;multipart/form-data&quot;를 추가해주고, input type file로 파일을 입력받을 수 있음&lt;/li&gt;
&lt;li&gt;이전에 작성한 글 (&lt;a href=&quot;https://chb2005.tistory.com/197&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Boot] 게시판 만들기 4 - 게시판 기능&lt;/a&gt;의 BoardController와 &lt;a href=&quot;https://chb2005.tistory.com/198&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Boot] 게시판 만들기 5 - 댓글, 좋아요, 파일 업로드 기능&lt;/a&gt;의 UploadService에서 파일 입력을 받으니 참고)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Boot/프로젝트</category>
      <category>Spring Boot 게시판 - 화면 제작.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/199</guid>
      <comments>https://chb2005.tistory.com/199#entry199comment</comments>
      <pubDate>Mon, 17 Apr 2023 16:20:42 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 게시판 만들기 5 - 댓글, 좋아요, 파일 업로드 관련 기능</title>
      <link>https://chb2005.tistory.com/198</link>
      <description>&lt;h1&gt;댓글 관련 코드&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommentRepository&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface CommentRepository extends JpaRepository&amp;lt;Comment, Long&amp;gt; {
    List&amp;lt;Comment&amp;gt; findAllByBoardId(Long boardId);
    List&amp;lt;Comment&amp;gt; findAllByUserLoginId(String loginId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommentService&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;댓글에 관련된 CRUD&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class CommentService {

    private final CommentRepository commentRepository;
    private final BoardRepository boardRepository;
    private final UserRepository userRepository;

    public void writeComment(Long boardId, CommentCreateRequest req, String loginId) {
        Board board = boardRepository.findById(boardId).get();
        User user = userRepository.findByLoginId(loginId).get();
        board.commentChange(board.getCommentCnt() + 1);
        commentRepository.save(req.toEntity(board, user));
    }

    public List&amp;lt;Comment&amp;gt; findAll(Long boardId) {
        return commentRepository.findAllByBoardId(boardId);
    }

    @Transactional
    public Long editComment(Long commentId, String newBody, String loginId) {
        Optional&amp;lt;Comment&amp;gt; optComment = commentRepository.findById(commentId);
        Optional&amp;lt;User&amp;gt; optUser = userRepository.findByLoginId(loginId);
        if (optComment.isEmpty() || optUser.isEmpty() || !optComment.get().getUser().equals(optUser.get())) {
            return null;
        }

        Comment comment = optComment.get();
        comment.update(newBody);

        return comment.getBoard().getId();
    }

    public Long deleteComment(Long commentId, String loginId) {
        Optional&amp;lt;Comment&amp;gt; optComment = commentRepository.findById(commentId);
        Optional&amp;lt;User&amp;gt; optUser = userRepository.findByLoginId(loginId);
        if (optComment.isEmpty() || optUser.isEmpty() ||
                (!optComment.get().getUser().equals(optUser.get()) &amp;amp;&amp;amp; !optUser.get().getUserRole().equals(UserRole.ADMIN))) {
            return null;
        }

        Board board = optComment.get().getBoard();
        board.commentChange(board.getCommentCnt() + 1);

        commentRepository.delete(optComment.get());
        return board.getId();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CommentController&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;댓글 추가, 수정, 삭제 시 완료 메세지 출력 후 해당 댓글이 포함된 게시글 페이지로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/comments&quot;)
@RequiredArgsConstructor
public class CommentController {

    private final CommentService commentService;
    private final BoardService boardService;

    @PostMapping(&quot;/{boardId}&quot;)
    public String addComments(@PathVariable Long boardId, @ModelAttribute CommentCreateRequest req,
                                     Authentication auth, Model model) {
        commentService.writeComment(boardId, req, auth.getName());

        model.addAttribute(&quot;message&quot;, &quot;댓글이 추가되었습니다.&quot;);
        model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + boardService.getCategory(boardId) + &quot;/&quot; + boardId);
        return &quot;printMessage&quot;;
    }

    @PostMapping(&quot;/{commentId}/edit&quot;)
    public String editComment(@PathVariable Long commentId, @ModelAttribute CommentCreateRequest req,
                              Authentication auth, Model model) {
        Long boardId = commentService.editComment(commentId, req.getBody(), auth.getName());
        model.addAttribute(&quot;message&quot;, boardId == null? &quot;잘못된 요청입니다.&quot; : &quot;댓글이 수정 되었습니다.&quot;);
        model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + boardService.getCategory(boardId) + &quot;/&quot; + boardId);
        return &quot;printMessage&quot;;
    }

    @GetMapping(&quot;/{commentId}/delete&quot;)
    public String deleteComment(@PathVariable Long commentId, Authentication auth, Model model) {
        Long boardId = commentService.deleteComment(commentId, auth.getName());
        model.addAttribute(&quot;message&quot;, boardId == null? &quot;작성자만 삭제 가능합니다.&quot; : &quot;댓글이 삭제 되었습니다.&quot;);
        model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + boardService.getCategory(boardId) + &quot;/&quot; + boardId);
        return &quot;printMessage&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;CommentCreateRequest&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;댓글을 입력받아 DB에 저장할 때 사용하는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
public class CommentCreateRequest {

    private String body;

    public Comment toEntity(Board board, User user) {
        return Comment.builder()
                .user(user)
                .board(board)
                .body(body)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;좋아요 관련 코드&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LikeRepository&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface LikeRepository extends JpaRepository&amp;lt;Like, Long&amp;gt; {
    void deleteByUserLoginIdAndBoardId(String loginId, Long boardId);
    Boolean existsByUserLoginIdAndBoardId(String loginId, Long boardId);
    List&amp;lt;Like&amp;gt; findAllByUserLoginId(String loginId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LikeService&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좋아요 추가, 삭제 및 로그인 한 유저가 특정 게시글에 좋아요를 눌렀는지 여부 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class LikeService {

    private final LikeRepository likeRepository;
    private final UserRepository userRepository;
    private final BoardRepository boardRepository;

    @Transactional
    public void addLike(String loginId, Long boardId) {
        Board board = boardRepository.findById(boardId).get();
        User loginUser = userRepository.findByLoginId(loginId).get();
        User boardUser = board.getUser();

        // 자신이 누른 좋아요가 아니라면
        if (!boardUser.equals(loginUser)) {
            boardUser.likeChange(boardUser.getReceivedLikeCnt() + 1);
        }
        board.likeChange(board.getLikeCnt() + 1);

        likeRepository.save(Like.builder()
                        .user(loginUser)
                        .board(board)
                        .build());
    }

    @Transactional
    public void deleteLike(String loginId, Long boardId) {
        Board board = boardRepository.findById(boardId).get();
        User loginUser = userRepository.findByLoginId(loginId).get();
        User boardUser = board.getUser();

        // 자신이 누른 좋아요가 아니라면
        if (!boardUser.equals(loginUser)) {
            boardUser.likeChange(boardUser.getReceivedLikeCnt() - 1);
        }
        board.likeChange(board.getLikeCnt() - 1);

        likeRepository.deleteByUserLoginIdAndBoardId(loginId, boardId);
    }

    public Boolean checkLike(String loginId, Long boardId) {
        return likeRepository.existsByUserLoginIdAndBoardId(loginId, boardId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LikeController&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;댓글과는 달리 메세지를 출력하지 않기 때문에 redirect를 통해 바로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/likes&quot;)
@RequiredArgsConstructor
public class LikeController {

    private final LikeService likeService;
    private final BoardService boardService;

    @GetMapping(&quot;/add/{boardId}&quot;)
    public String addLike(@PathVariable Long boardId, Authentication auth) {
        likeService.addLike(auth.getName(), boardId);
        return &quot;redirect:/boards/&quot; + boardService.getCategory(boardId) + &quot;/&quot; + boardId;
    }

    @GetMapping(&quot;/delete/{boardId}&quot;)
    public String deleteLike(@PathVariable Long boardId, Authentication auth) {
        likeService.deleteLike(auth.getName(), boardId);
        return &quot;redirect:/boards/&quot; + boardService.getCategory(boardId) + &quot;/&quot; + boardId;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;파일 업로드 관련 코드&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UploadImageRepository&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface UploadImageRepository extends JpaRepository&amp;lt;UploadImage, Long&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UploadImageService&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;saveImage() 메소드에서 입력된 파일의 이름을 &quot;UUID + 원본 파일의 확장자&quot;로 바꿔서 저장&lt;/li&gt;
&lt;li&gt;downloadImage() 메소드에서는 다시 원본 파일명으로 수정 후 파일 return&lt;/li&gt;
&lt;li&gt;&lt;span&gt;이&lt;/span&gt; &lt;span&gt;프로젝트에서&lt;/span&gt; &lt;span&gt;파일&lt;/span&gt; &lt;span&gt;업로드&lt;/span&gt; &lt;span&gt;기능은&lt;/span&gt; &lt;span&gt;로컬&lt;/span&gt; &lt;span&gt;프로젝트에&lt;/span&gt; &quot;/src/main/resources/static/upload-images&quot; &lt;span&gt;폴더를&lt;/span&gt; &lt;span&gt;추가하고&lt;/span&gt; &lt;span&gt;해당&lt;/span&gt; &lt;span&gt;폴더에&lt;/span&gt; &lt;span&gt;이미지를&lt;/span&gt; &lt;span&gt;업로드하는&lt;/span&gt; &lt;span&gt;방식&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;따라서&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;해당&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;경로에&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;폴더가&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;없으면&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;에러가&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;발생할&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;수&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;있기&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;때문에&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;직접&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;폴더를&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;추가해줘야 함&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;만약&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;프로젝트를&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; EC2 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;서버로&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;배포하고&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;싶다면&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; EC2 &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;인스턴스&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;프로젝트&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;내부에서&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;파일을&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;관리해도&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;되지만&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;, AWS S3&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;사용하는&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;것도&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;좋은&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;방법이라&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;생각함&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chb2005.tistory.com/200&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[Spring Boot] AWS S3&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;를&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이용한&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;파일&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;업로드&lt;/span&gt;&lt;/a&gt;&amp;nbsp;참고&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class UploadImageService {

    private final UploadImageRepository uploadImageRepository;
    private final BoardRepository boardRepository;
    private final String rootPath = System.getProperty(&quot;user.dir&quot;);
    private final String fileDir = rootPath + &quot;/src/main/resources/static/upload-images/&quot;;

    public String getFullPath(String filename) {
        return fileDir + filename;
    }

    public UploadImage saveImage(MultipartFile multipartFile, Board board) throws IOException {
        if (multipartFile.isEmpty()) {
            return null;
        }

        String originalFilename = multipartFile.getOriginalFilename();
        // 원본 파일명 -&amp;gt; 서버에 저장된 파일명 (중복 X)
        // 파일명이 중복되지 않도록 UUID로 설정 + 확장자 유지
        String savedFilename = UUID.randomUUID() + &quot;.&quot; + extractExt(originalFilename);

        // 파일 저장
        multipartFile.transferTo(new File(getFullPath(savedFilename)));

        return uploadImageRepository.save(UploadImage.builder()
                .originalFilename(originalFilename)
                .savedFilename(savedFilename)
                .board(board)
                .build());
    }

    @Transactional
    public void deleteImage(UploadImage uploadImage) throws IOException {
        uploadImageRepository.delete(uploadImage);
        Files.deleteIfExists(Paths.get(getFullPath(uploadImage.getSavedFilename())));
    }

    // 확장자 추출
    private String extractExt(String originalFilename) {
        int pos = originalFilename.lastIndexOf(&quot;.&quot;);
        return originalFilename.substring(pos + 1);
    }

    public ResponseEntity&amp;lt;UrlResource&amp;gt; downloadImage(Long boardId) throws MalformedURLException {
        // boardId에 해당하는 게시글이 없으면 null return
        Board board = boardRepository.findById(boardId).get();
        if (board == null || board.getUploadImage() == null) {
            return null;
        }

        UrlResource urlResource = new UrlResource(&quot;file:&quot; + getFullPath(board.getUploadImage().getSavedFilename()));

        // 업로드 한 파일명이 한글인 경우 아래 작업을 안해주면 한글이 깨질 수 있음
        String encodedUploadFileName = UriUtils.encode(board.getUploadImage().getOriginalFilename(), StandardCharsets.UTF_8);
        String contentDisposition = &quot;attachment; filename=\&quot;&quot; + encodedUploadFileName + &quot;\&quot;&quot;;

        // header에 CONTENT_DISPOSITION 설정을 통해 클릭 시 다운로드 진행
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
                .body(urlResource);

    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Spring Boot/프로젝트</category>
      <category>Spring Boot 게시판 - 댓글 좋아요 파일업로드 관련 기능.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/198</guid>
      <comments>https://chb2005.tistory.com/198#entry198comment</comments>
      <pubDate>Mon, 17 Apr 2023 16:10:07 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 게시판 만들기 4 - 게시판 기능</title>
      <link>https://chb2005.tistory.com/197</link>
      <description>&lt;h1&gt;게시판 기능&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;글 작성, 조회, 수정, 삭제, 리스트 조회, 댓글, 좋아요 기능&lt;/li&gt;
&lt;li&gt;자세한 기능 설계는 &lt;a href=&quot;https://chb2005.tistory.com/176&quot;&gt;[Spring Boot] 게시판 만들기 1 - 설계 &amp;amp; 결과&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;BoardRepository&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findAllByCategoryAndUserUserRoleNot() : 해당 카테고리에 있는 게시글을 페이지에 맞게 조회, 이 때 ADMIN이 작성한 글(공지)는 포함 X&lt;/li&gt;
&lt;li&gt;findAllByCategoryAndUserUserRole() : 해당 카테고리에 있는 공지 글 조회&lt;/li&gt;
&lt;li&gt;findAllByCategoryAndTitleContainsAndUserUserRoleNot(), findAllByCategoryAndUserNicknameContainsAndUserUserRoleNot() : 검색 기능에 사용&lt;/li&gt;
&lt;li&gt;findAllByUserLoginId() : 유저의 마이 페이지에서 내가 작성한 글 조회 시 사용&lt;/li&gt;
&lt;li&gt;countAllByUserUserRole() : 전체 공지글이 몇개 있는지 조회 시 사용&lt;/li&gt;
&lt;li&gt;countAllByCategoryAndUserUserRoleNot() : 해당 카테고리에 공지글을 제외한 글이 몇개 있는지 조회 시 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface BoardRepository extends JpaRepository&amp;lt;Board, Long&amp;gt; {

    Page&amp;lt;Board&amp;gt; findAllByCategoryAndUserUserRoleNot(BoardCategory category, UserRole userRole, PageRequest pageRequest);
    Page&amp;lt;Board&amp;gt; findAllByCategoryAndTitleContainsAndUserUserRoleNot(BoardCategory category, String title, UserRole userRole, PageRequest pageRequest);
    Page&amp;lt;Board&amp;gt; findAllByCategoryAndUserNicknameContainsAndUserUserRoleNot(BoardCategory category, String nickname, UserRole userRole, PageRequest pageRequest);
    List&amp;lt;Board&amp;gt; findAllByUserLoginId(String loginId);
    List&amp;lt;Board&amp;gt; findAllByCategoryAndUserUserRole(BoardCategory category, UserRole userRole);
    Long countAllByUserUserRole(UserRole userRole);
    Long countAllByCategoryAndUserUserRoleNot(BoardCategory category, UserRole userRole);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;BoardService&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;getBoardList() : 해당 카테고리에 있는 글 list를 return
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 때, 검색을 했다면 제목, 유저 닉네임에 키워드가 포함되는 글을 return&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;writeBoard() : 글 작성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Board 저장 -&amp;gt; 이미지가 있다면 이미지 저장 후 저장된 Board에 해당 이미지 정보 추가 삽입&lt;/li&gt;
&lt;li&gt;만약 가입 인사 게시판에 글이 저장된 경우라면 로그인 한 유저의 등급을 SILVER로 조정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;findMyBoard() : 마이 페이지에서 내가 작성한 글, 내가 좋아요 누른 글, 내가 댓글을 추가한 글을 category로 분류해 List를 return 해주는 메소드
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 좋아요 누른 글은 로그인 한 유저의 loginId를 가지는 Like를 모두 불러온 후 Like에 저장된 Board들을 List로 변환 후 return&lt;/li&gt;
&lt;li&gt;내가 댓글을 추가한 글도 좋아요와 비슷하긴 하지만 하나의 글에 여러개의 댓글을 단 경우, 한번만 return 되게 하기 위해 HashSet을 사용하여 중복을 제거해서 return&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class BoardService {

    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final LikeRepository likeRepository;
    private final CommentRepository commentRepository;
    private final UploadImageService uploadImageService;

    public Page&amp;lt;Board&amp;gt; getBoardList(BoardCategory category, PageRequest pageRequest, String searchType, String keyword) {
        if (searchType != null &amp;amp;&amp;amp; keyword != null) {
            if (searchType.equals(&quot;title&quot;)) {
                return boardRepository.findAllByCategoryAndTitleContainsAndUserUserRoleNot(category, keyword, UserRole.ADMIN, pageRequest);
            } else {
                return boardRepository.findAllByCategoryAndUserNicknameContainsAndUserUserRoleNot(category, keyword, UserRole.ADMIN, pageRequest);
            }
        }
        return boardRepository.findAllByCategoryAndUserUserRoleNot(category, UserRole.ADMIN, pageRequest);
    }

    public List&amp;lt;Board&amp;gt; getNotice(BoardCategory category) {
        return boardRepository.findAllByCategoryAndUserUserRole(category, UserRole.ADMIN);
    }

    public BoardDto getBoard(Long boardId, String category) {
        Optional&amp;lt;Board&amp;gt; optBoard = boardRepository.findById(boardId);

        // id에 해당하는 게시글이 없거나 카테고리가 일치하지 않으면 null return
        if (optBoard.isEmpty() || !optBoard.get().getCategory().toString().equalsIgnoreCase(category)) {
            return null;
        }

        return BoardDto.of(optBoard.get());
    }

    @Transactional
    public Long writeBoard(BoardCreateRequest req, BoardCategory category, String loginId, Authentication auth) throws IOException {
        User loginUser = userRepository.findByLoginId(loginId).get();

        Board savedBoard = boardRepository.save(req.toEntity(category, loginUser));

        UploadImage uploadImage = uploadImageService.saveImage(req.getUploadImage(), savedBoard);
        if (uploadImage != null) {
            savedBoard.setUploadImage(uploadImage);
        }

        if (category.equals(BoardCategory.GREETING)) {
            loginUser.rankUp(UserRole.SILVER, auth);
        }
        return savedBoard.getId();
    }

    @Transactional
    public Long editBoard(Long boardId, String category, BoardDto dto) throws IOException {
        Optional&amp;lt;Board&amp;gt; optBoard = boardRepository.findById(boardId);

        // id에 해당하는 게시글이 없거나 카테고리가 일치하지 않으면 null return
        if (optBoard.isEmpty() || !optBoard.get().getCategory().toString().equalsIgnoreCase(category)) {
            return null;
        }

        Board board = optBoard.get();
        // 게시글에 이미지가 있었으면 삭제
        if (board.getUploadImage() != null) {
            uploadImageService.deleteImage(board.getUploadImage());
            board.setUploadImage(null);
        }

        UploadImage uploadImage = uploadImageService.saveImage(dto.getNewImage(), board);
        if (uploadImage != null) {
            board.setUploadImage(uploadImage);
        }
        board.update(dto);

        return board.getId();
    }

    public Long deleteBoard(Long boardId, String category) throws IOException {
        Optional&amp;lt;Board&amp;gt; optBoard = boardRepository.findById(boardId);

        // id에 해당하는 게시글이 없거나 카테고리가 일치하지 않으면 null return
        if (optBoard.isEmpty() || !optBoard.get().getCategory().toString().equalsIgnoreCase(category)) {
            return null;
        }

        User boardUser = optBoard.get().getUser();
        boardUser.likeChange(boardUser.getReceivedLikeCnt() - optBoard.get().getLikeCnt());
        if (optBoard.get().getUser() != null) {
            uploadImageService.deleteImage(optBoard.get().getUploadImage());
        }
        boardRepository.deleteById(boardId);
        return boardId;
    }

    public String getCategory(Long boardId) {
        Board board = boardRepository.findById(boardId).get();
        return board.getCategory().toString().toLowerCase();
    }

    public List&amp;lt;Board&amp;gt; findMyBoard(String category, String loginId) {
        if (category.equals(&quot;board&quot;)) {
            return boardRepository.findAllByUserLoginId(loginId);
        } else if (category.equals(&quot;like&quot;)) {
            List&amp;lt;Like&amp;gt; likes = likeRepository.findAllByUserLoginId(loginId);
            List&amp;lt;Board&amp;gt; boards = new ArrayList&amp;lt;&amp;gt;();
            for (Like like : likes) {
                boards.add(like.getBoard());
            }
            return boards;
        } else if (category.equals(&quot;comment&quot;)) {
            List&amp;lt;Comment&amp;gt; comments = commentRepository.findAllByUserLoginId(loginId);
            List&amp;lt;Board&amp;gt; boards = new ArrayList&amp;lt;&amp;gt;();
            HashSet&amp;lt;Long&amp;gt; commentIds = new HashSet&amp;lt;&amp;gt;();

            for (Comment comment : comments) {
                if (!commentIds.contains(comment.getBoard().getId())) {
                    boards.add(comment.getBoard());
                    commentIds.add(comment.getBoard().getId());
                }
            }
            return boards;
        }
        return null;
    }

    public BoardCntDto getBoardCnt(){
        return BoardCntDto.builder()
                .totalBoardCnt(boardRepository.count())
                .totalNoticeCnt(boardRepository.countAllByUserUserRole(UserRole.ADMIN))
                .totalGreetingCnt(boardRepository.countAllByCategoryAndUserUserRoleNot(BoardCategory.GREETING, UserRole.ADMIN))
                .totalFreeCnt(boardRepository.countAllByCategoryAndUserUserRoleNot(BoardCategory.FREE, UserRole.ADMIN))
                .totalGoldCnt(boardRepository.countAllByCategoryAndUserUserRoleNot(BoardCategory.GOLD, UserRole.ADMIN))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;BoardController&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/boards/{category} : 카테고리에 해당하는 글 리스트를 return 하는 URL
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sortType으로 정렬 방식을 입력받아 PageRequest 설정을 통해 정렬 구현&lt;/li&gt;
&lt;li&gt;searchType, keyword로 글 제목, 작성자 닉네임에 keyword가 포함되는 글을 return 함으로써 검색 기능 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/boards/{category}/{boardId} : 게시글 상세 조회 페이지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 로그인 한 유저라면 loginUserLoginId를 전송함으로써, 화면에서 본인이 작성한 글인지 확인 할 수 있게함&lt;/li&gt;
&lt;li&gt;로그인 한 유저가 이 글에 좋아요를 눌렀는지 여부를 likeCheck에 담아 전송함으로써, 화면에서 하트를 누르면 좋아요를 추가 시킬지, 제거 시킬지 알 수 있게함&lt;/li&gt;
&lt;li&gt;또한, 글 상세 페이지에서는 해당 글에 추가된 댓글 리스트도 같이 전송 시킴&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;/boards/images/{filename} : Resource 타입으로 파일을 전송함으로써, 해당 게시글에 추가된 이미지를 화면에서 출력시킴&lt;/li&gt;
&lt;li&gt;/boards/images/download/{filename} : ResponseEntity&amp;lt;UrlResource&amp;gt; 타입으로 파일을 전송함으로써, 해당 URL에 접속 시 저장된 이미지를 다운로드 하게 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequestMapping(&quot;/boards&quot;)
@RequiredArgsConstructor
public class BoardController {

    private final BoardService boardService;
    private final LikeService likeService;
    private final CommentService commentService;
    private final UploadImageService uploadImageService;

    @GetMapping(&quot;/{category}&quot;)
    public String boardListPage(@PathVariable String category, Model model,
                                @RequestParam(required = false, defaultValue = &quot;1&quot;) int page,
                                @RequestParam(required = false) String sortType,
                                @RequestParam(required = false) String searchType,
                                @RequestParam(required = false) String keyword) {
        BoardCategory boardCategory = BoardCategory.of(category);
        if (boardCategory == null) {
            model.addAttribute(&quot;message&quot;, &quot;카테고리가 존재하지 않습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/&quot;);
            return &quot;printMessage&quot;;
        }

        model.addAttribute(&quot;notices&quot;, boardService.getNotice(boardCategory));

        PageRequest pageRequest = PageRequest.of(page - 1, 10, Sort.by(&quot;id&quot;).descending());
        if (sortType != null) {
            if (sortType.equals(&quot;date&quot;)) {
                pageRequest = PageRequest.of(page - 1, 10, Sort.by(&quot;createdAt&quot;).descending());
            } else if (sortType.equals(&quot;like&quot;)) {
                pageRequest = PageRequest.of(page - 1, 10, Sort.by(&quot;likeCnt&quot;).descending());
            } else if (sortType.equals(&quot;comment&quot;)) {
                pageRequest = PageRequest.of(page - 1, 10, Sort.by(&quot;commentCnt&quot;).descending());
            }
        }

        model.addAttribute(&quot;category&quot;, category);
        model.addAttribute(&quot;boards&quot;, boardService.getBoardList(boardCategory, pageRequest, searchType, keyword));
        model.addAttribute(&quot;boardSearchRequest&quot;, new BoardSearchRequest(sortType, searchType, keyword));
        return &quot;boards/list&quot;;
    }

    @GetMapping(&quot;/{category}/write&quot;)
    public String boardWritePage(@PathVariable String category, Model model) {
        BoardCategory boardCategory = BoardCategory.of(category);
        if (boardCategory == null) {
            model.addAttribute(&quot;message&quot;, &quot;카테고리가 존재하지 않습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/&quot;);
            return &quot;printMessage&quot;;
        }

        model.addAttribute(&quot;category&quot;, category);
        model.addAttribute(&quot;boardCreateRequest&quot;, new BoardCreateRequest());
        return &quot;boards/write&quot;;
    }

    @PostMapping(&quot;/{category}&quot;)
    public String boardWrite(@PathVariable String category, @ModelAttribute BoardCreateRequest req,
                             Authentication auth, Model model) throws IOException {
        BoardCategory boardCategory = BoardCategory.of(category);
        if (boardCategory == null) {
            model.addAttribute(&quot;message&quot;, &quot;카테고리가 존재하지 않습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/&quot;);
            return &quot;printMessage&quot;;
        }

        Long savedBoardId = boardService.writeBoard(req, boardCategory, auth.getName(), auth);
        if (boardCategory.equals(BoardCategory.GREETING)) {
            model.addAttribute(&quot;message&quot;, &quot;가입인사를 작성하여 SILVER 등급으로 승급했습니다!\n이제 자유게시판에 글을 작성할 수 있습니다!&quot;);
        } else {
            model.addAttribute(&quot;message&quot;, savedBoardId + &quot;번 글이 등록되었습니다.&quot;);
        }
        model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + category + &quot;/&quot; + savedBoardId);
        return &quot;printMessage&quot;;
    }

    @GetMapping(&quot;/{category}/{boardId}&quot;)
    public String boardDetailPage(@PathVariable String category, @PathVariable Long boardId, Model model,
                                  Authentication auth) {
        if (auth != null) {
            model.addAttribute(&quot;loginUserLoginId&quot;, auth.getName());
            model.addAttribute(&quot;likeCheck&quot;, likeService.checkLike(auth.getName(), boardId));
        }

        BoardDto boardDto = boardService.getBoard(boardId, category);
        // id에 해당하는 게시글이 없거나 카테고리가 일치하지 않는 경우
        if (boardDto == null) {
            model.addAttribute(&quot;message&quot;, &quot;해당 게시글이 존재하지 않습니다&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + category);
            return &quot;printMessage&quot;;
        }

        model.addAttribute(&quot;boardDto&quot;, boardDto);
        model.addAttribute(&quot;category&quot;, category);

        model.addAttribute(&quot;commentCreateRequest&quot;, new CommentCreateRequest());
        model.addAttribute(&quot;commentList&quot;, commentService.findAll(boardId));
        return &quot;boards/detail&quot;;
    }

    @PostMapping(&quot;/{category}/{boardId}/edit&quot;)
    public String boardEdit(@PathVariable String category, @PathVariable Long boardId,
                            @ModelAttribute BoardDto dto, Model model) throws IOException {
        Long editedBoardId = boardService.editBoard(boardId, category, dto);

        if (editedBoardId == null) {
            model.addAttribute(&quot;message&quot;, &quot;해당 게시글이 존재하지 않습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + category);
        } else {
            model.addAttribute(&quot;message&quot;, editedBoardId + &quot;번 글이 수정되었습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + category + &quot;/&quot; + boardId);
        }
        return &quot;printMessage&quot;;
    }

    @GetMapping(&quot;/{category}/{boardId}/delete&quot;)
    public String boardDelete(@PathVariable String category, @PathVariable Long boardId, Model model) throws IOException {
        if (category.equals(&quot;greeting&quot;)) {
            model.addAttribute(&quot;message&quot;, &quot;가입인사는 삭제할 수 없습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/greeting&quot;);
            return &quot;printMessage&quot;;
        }

        Long deletedBoardId = boardService.deleteBoard(boardId, category);

        // id에 해당하는 게시글이 없거나 카테고리가 일치하지 않으면 에러 메세지 출력
        // 게시글이 존재해 삭제했으면 삭제 완료 메세지 출력
        model.addAttribute(&quot;message&quot;, deletedBoardId == null ? &quot;해당 게시글이 존재하지 않습니다&quot; : deletedBoardId + &quot;번 글이 삭제되었습니다.&quot;);
        model.addAttribute(&quot;nextUrl&quot;, &quot;/boards/&quot; + category);
        return &quot;printMessage&quot;;
    }

    @ResponseBody
    @GetMapping(&quot;/images/{filename}&quot;)
    public Resource showImage(@PathVariable String filename) throws MalformedURLException {
        return new UrlResource(&quot;file:&quot; + uploadImageService.getFullPath(filename));
    }

    @GetMapping(&quot;/images/download/{boardId}&quot;)
    public ResponseEntity&amp;lt;UrlResource&amp;gt; downloadImage(@PathVariable Long boardId) throws MalformedURLException {
        return uploadImageService.downloadImage(boardId);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Board 관련 DTO&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BoardCreateRequest&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Board를 입력받아 DB에 저장할 때 사용하는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
public class BoardCreateRequest {

    private String title;
    private String body;
    private MultipartFile uploadImage;

    public Board toEntity(BoardCategory category, User user) {
        return Board.builder()
                .user(user)
                .category(category)
                .title(title)
                .body(body)
                .likeCnt(0)
                .commentCnt(0)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BoardSearchRequest&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글 리스트에서 검색할 때 사용하는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@AllArgsConstructor
public class BoardSearchRequest {

    private String sortType;
    private String searchType;
    private String keyword;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BoardDto&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시글 조회, 리스트 조회, 수정 등에 사용되는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@Builder
public class BoardDto {

    private Long id;
    private String userLoginId;
    private String userNickname;
    private String title;
    private String body;
    private Integer likeCnt;
    private LocalDateTime createdAt;
    private LocalDateTime lastModifiedAt;
    private MultipartFile newImage;
    private UploadImage uploadImage;

    public static BoardDto of(Board board) {
        return BoardDto.builder()
                .id(board.getId())
                .userLoginId(board.getUser().getLoginId())
                .userNickname(board.getUser().getNickname())
                .title(board.getTitle())
                .body(board.getBody())
                .createdAt(board.getCreatedAt())
                .lastModifiedAt(board.getLastModifiedAt())
                .likeCnt(board.getLikes().size())
                .uploadImage(board.getUploadImage())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BoardCntDto&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈 화면에서 각각의 카테고리에 해당하는 Board 수를 출력하기 위해 사용되는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@Builder
public class BoardCntDto {

    private Long totalNoticeCnt;
    private Long totalBoardCnt;
    private Long totalGreetingCnt;
    private Long totalFreeCnt;
    private Long totalGoldCnt;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Spring Boot/프로젝트</category>
      <category>Spring Boot 게시판 - 게시판 기능.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/197</guid>
      <comments>https://chb2005.tistory.com/197#entry197comment</comments>
      <pubDate>Mon, 17 Apr 2023 16:06:21 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 게시판 만들기 3 - 유저 기능</title>
      <link>https://chb2005.tistory.com/196</link>
      <description>&lt;h1&gt;유저 기능&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입, 로그인, 정보 수정, 회원 탈퇴, 마이 페이지&lt;/li&gt;
&lt;li&gt;자세한 기능 설계는 &lt;a href=&quot;https://chb2005.tistory.com/176&quot;&gt;[Spring Boot] 게시판 만들기 1 - 설계 &amp;amp; 결과&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;UserRepository&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;findAllByNicknameContains() : 닉네임에 String이 포함되어 있는지 =&amp;gt; ADMIN이 User 검색 시 사용&lt;/li&gt;
&lt;li&gt;existsByLoginId(), existsByNickname() : 로그인 아이디, 닉네임을 가진 유저가 존재하는지 =&amp;gt; 회원 가입 시 중복 체크용으로 사용&lt;/li&gt;
&lt;li&gt;countAllByUserRole() : 해당 등급을 가진 유저가 몇명 있는지 =&amp;gt; 홈 화면에서 출력하기 위해 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Repository
public interface UserRepository extends JpaRepository&amp;lt;User, Long&amp;gt; {
    Optional&amp;lt;User&amp;gt; findByLoginId(String loginId);
    Page&amp;lt;User&amp;gt; findAllByNicknameContains(String nickname, PageRequest pageRequest);
    Boolean existsByLoginId(String loginId);
    Boolean existsByNickname(String nickname);
    Long countAllByUserRole(UserRole userRole);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;UserService&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;joinValid(), editValid()는 회원 가입, 정보 수정 시 비어있는지, 글자수가 초과하는지, 중복되는지 등을 상황과 변수에 맞게 판단 후 하나라도 조건을 만족하지 못하면 BindingResult를 return 함으로써 회원가입, 정보 수정을 못하게 함&lt;/li&gt;
&lt;li&gt;delete()는 유저를 삭제하는 메소드인데, DB에서 삭제하기 전 해당 유저가 추가한 댓글, 좋아요를 조회하여 Board의 likeCnt, commentCnt를 직접 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final LikeRepository likeRepository;
    private final CommentRepository commentRepository;
    private final BCryptPasswordEncoder encoder;

    public BindingResult joinValid(UserJoinRequest req, BindingResult bindingResult)
    {
        if (req.getLoginId().isEmpty()) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;loginId&quot;, &quot;아이디가 비어있습니다.&quot;));
        } else if (req.getLoginId().length() &amp;gt; 10) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;loginId&quot;, &quot;아이디가 10자가 넘습니다.&quot;));
        } else if (userRepository.existsByLoginId(req.getLoginId())) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;loginId&quot;, &quot;아이디가 중복됩니다.&quot;));
        }

        if (req.getPassword().isEmpty()) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;password&quot;, &quot;비밀번호가 비어있습니다.&quot;));
        }

        if (!req.getPassword().equals(req.getPasswordCheck())) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;passwordCheck&quot;, &quot;비밀번호가 일치하지 않습니다.&quot;));
        }

        if (req.getNickname().isEmpty()) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;nickname&quot;, &quot;닉네임이 비어있습니다.&quot;));
        } else if (req.getNickname().length() &amp;gt; 10) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;nickname&quot;, &quot;닉네임이 10자가 넘습니다.&quot;));
        } else if (userRepository.existsByNickname(req.getNickname())) {
            bindingResult.addError(new FieldError(&quot;req&quot;, &quot;nickname&quot;, &quot;닉네임이 중복됩니다.&quot;));
        }

        return bindingResult;
    }

    public void join(UserJoinRequest req) {
        userRepository.save(req.toEntity( encoder.encode(req.getPassword()) ));
    }

    public User myInfo(String loginId) {
        return userRepository.findByLoginId(loginId).get();
    }

    public BindingResult editValid(UserDto dto, BindingResult bindingResult, String loginId)
    {
        User loginUser = userRepository.findByLoginId(loginId).get();

        if (dto.getNowPassword().isEmpty()) {
            bindingResult.addError(new FieldError(&quot;dto&quot;, &quot;nowPassword&quot;, &quot;현재 비밀번호가 비어있습니다.&quot;));
        } else if (!encoder.matches(dto.getNowPassword(), loginUser.getPassword())) {
            bindingResult.addError(new FieldError(&quot;dto&quot;, &quot;nowPassword&quot;, &quot;현재 비밀번호가 틀렸습니다.&quot;));
        }

        if (!dto.getNewPassword().equals(dto.getNewPasswordCheck())) {
            bindingResult.addError(new FieldError(&quot;dto&quot;, &quot;newPasswordCheck&quot;, &quot;비밀번호가 일치하지 않습니다.&quot;));
        }

        if (dto.getNickname().isEmpty()) {
            bindingResult.addError(new FieldError(&quot;dto&quot;, &quot;nickname&quot;, &quot;닉네임이 비어있습니다.&quot;));
        } else if (dto.getNickname().length() &amp;gt; 10) {
            bindingResult.addError(new FieldError(&quot;dto&quot;, &quot;nickname&quot;, &quot;닉네임이 10자가 넘습니다.&quot;));
        } else if (!dto.getNickname().equals(loginUser.getNickname()) &amp;amp;&amp;amp; userRepository.existsByNickname(dto.getNickname())) {
            bindingResult.addError(new FieldError(&quot;dto&quot;, &quot;nickname&quot;, &quot;닉네임이 중복됩니다.&quot;));
        }

        return bindingResult;
    }

    @Transactional
    public void edit(UserDto dto, String loginId) {
        User loginUser = userRepository.findByLoginId(loginId).get();

        if (dto.getNewPassword().equals(&quot;&quot;)) {
            loginUser.edit(loginUser.getPassword(), dto.getNickname());
        } else {
            loginUser.edit(encoder.encode(dto.getNewPassword()), dto.getNickname());
        }
    }

    @Transactional
    public Boolean delete(String loginId, String nowPassword) {
        User loginUser = userRepository.findByLoginId(loginId).get();

        if (encoder.matches(nowPassword, loginUser.getPassword())) {
            List&amp;lt;Like&amp;gt; likes = likeRepository.findAllByUserLoginId(loginId);
            for (Like like : likes) {
                like.getBoard().likeChange( like.getBoard().getLikeCnt() - 1 );
            }

            List&amp;lt;Comment&amp;gt; comments = commentRepository.findAllByUserLoginId(loginId);
            for (Comment comment : comments) {
                comment.getBoard().commentChange( comment.getBoard().getCommentCnt() - 1 );
            }

            userRepository.delete(loginUser);
            return true;
        } else {
            return false;
        }
    }

    public Page&amp;lt;User&amp;gt; findAllByNickname(String keyword, PageRequest pageRequest) {
        return userRepository.findAllByNicknameContains(keyword, pageRequest);
    }

    @Transactional
    public void changeRole(Long userId) {
        User user = userRepository.findById(userId).get();
        user.changeRole();
    }

    public UserCntDto getUserCnt() {
        return UserCntDto.builder()
                .totalUserCnt(userRepository.count())
                .totalAdminCnt(userRepository.countAllByUserRole(UserRole.ADMIN))
                .totalBronzeCnt(userRepository.countAllByUserRole(UserRole.BRONZE))
                .totalSilverCnt(userRepository.countAllByUserRole(UserRole.SILVER))
                .totalGoldCnt(userRepository.countAllByUserRole(UserRole.GOLD))
                .totalBlacklistCnt(userRepository.countAllByUserRole(UserRole.BLACKLIST))
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;UserController&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 페이지 접속 시 이전 페이지를 세션에 저장함으로써, 로그인에 성공하면 이전 페이지로 이동시켜 줌&lt;/li&gt;
&lt;li&gt;/users/myPage/{category}는 마이 페이지에서 내가 작성한 글, 내가 댓글을 단 글, 내가 좋아요 누른 글을 볼 수 있는데, 이 때 {category}로 분류하여 리스트를 return&lt;/li&gt;
&lt;li&gt;/users/admin은 관리자 페이지 =&amp;gt; 모든 유저의 정보를 확인 할 수 있음&lt;/li&gt;
&lt;li&gt;/users/admin/{userId}는 관리자가 userId에 해당하는 유저의 등급을 변경할 때 사용&lt;/li&gt;
&lt;li&gt;&quot;printMessage&quot;는 message에 해당 하는 값을 출력하고, nextUrl에 해당 하는 URL로 이동시켜 주는 HTML 파일&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequiredArgsConstructor
@RequestMapping(&quot;/users&quot;)
public class UserController {

    private final UserService userService;
    private final BoardService boardService;

    @GetMapping(&quot;/join&quot;)
    public String joinPage(Model model) {
        model.addAttribute(&quot;userJoinRequest&quot;, new UserJoinRequest());
        return &quot;users/join&quot;;
    }

    @PostMapping(&quot;/join&quot;)
    public String join(@Valid @ModelAttribute UserJoinRequest req, BindingResult bindingResult, Model model) {

        // Validation
        if (userService.joinValid(req, bindingResult).hasErrors()) {
            return &quot;users/join&quot;;
        }

        userService.join(req);
        model.addAttribute(&quot;message&quot;, &quot;회원가입에 성공했습니다!\n로그인 후 사용 가능합니다!&quot;);
        model.addAttribute(&quot;nextUrl&quot;, &quot;/users/login&quot;);
        return &quot;printMessage&quot;;
    }

    @GetMapping(&quot;/login&quot;)
    public String loginPage(Model model, HttpServletRequest request) {

        // 로그인 성공 시 이전 페이지로 redirect 되게 하기 위해 세션에 저장
        String uri = request.getHeader(&quot;Referer&quot;);
        if (uri != null &amp;amp;&amp;amp; !uri.contains(&quot;/login&quot;) &amp;amp;&amp;amp; !uri.contains(&quot;/join&quot;)) {
            request.getSession().setAttribute(&quot;prevPage&quot;, uri);
        }

        model.addAttribute(&quot;userLoginRequest&quot;, new UserLoginRequest());
        return &quot;users/login&quot;;
    }

    @GetMapping(&quot;/myPage/{category}&quot;)
    public String myPage(@PathVariable String category, Authentication auth, Model model) {
        model.addAttribute(&quot;boards&quot;, boardService.findMyBoard(category, auth.getName()));
        model.addAttribute(&quot;category&quot;, category);
        model.addAttribute(&quot;user&quot;, userService.myInfo(auth.getName()));
        return &quot;users/myPage&quot;;
    }

    @GetMapping(&quot;/edit&quot;)
    public String userEditPage(Authentication auth, Model model) {
        User user = userService.myInfo(auth.getName());
        model.addAttribute(&quot;userDto&quot;, UserDto.of(user));
        return &quot;users/edit&quot;;
    }

    @PostMapping(&quot;/edit&quot;)
    public String userEdit(@Valid @ModelAttribute UserDto dto, BindingResult bindingResult,
                           Authentication auth, Model model) {

        // Validation
        if (userService.editValid(dto, bindingResult, auth.getName()).hasErrors()) {
            return &quot;users/edit&quot;;
        }

        userService.edit(dto, auth.getName());

        model.addAttribute(&quot;message&quot;, &quot;정보가 수정되었습니다.&quot;);
        model.addAttribute(&quot;nextUrl&quot;, &quot;/users/myPage/board&quot;);
        return &quot;printMessage&quot;;
    }

    @GetMapping(&quot;/delete&quot;)
    public String userDeletePage(Authentication auth, Model model) {
        User user = userService.myInfo(auth.getName());
        model.addAttribute(&quot;userDto&quot;, UserDto.of(user));
        return &quot;users/delete&quot;;
    }

    @PostMapping(&quot;/delete&quot;)
    public String userDelete(@ModelAttribute UserDto dto, Authentication auth, Model model) {
        Boolean deleteSuccess = userService.delete(auth.getName(), dto.getNowPassword());
        if (deleteSuccess) {
            model.addAttribute(&quot;message&quot;, &quot;탈퇴 되었습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/users/logout&quot;);
            return &quot;printMessage&quot;;
        } else {
            model.addAttribute(&quot;message&quot;, &quot;현재 비밀번호가 틀려 탈퇴에 실패하였습니다.&quot;);
            model.addAttribute(&quot;nextUrl&quot;, &quot;/users/delete&quot;);
            return &quot;printMessage&quot;;
        }
    }

    @GetMapping(&quot;/admin&quot;)
    public String adminPage(@RequestParam(required = false, defaultValue = &quot;1&quot;) int page,
                            @RequestParam(required = false, defaultValue = &quot;&quot;) String keyword,
                            Model model) {

        PageRequest pageRequest = PageRequest.of(page - 1, 10, Sort.by(&quot;id&quot;).descending());
        Page&amp;lt;User&amp;gt; users = userService.findAllByNickname(keyword, pageRequest);

        model.addAttribute(&quot;users&quot;, users);
        model.addAttribute(&quot;keyword&quot;, keyword);
        return &quot;users/admin&quot;;
    }

    @GetMapping(&quot;/admin/{userId}&quot;)
    public String adminChangeRole(@PathVariable Long userId,
                                  @RequestParam(required = false, defaultValue = &quot;1&quot;) int page,
                                  @RequestParam(required = false, defaultValue = &quot;&quot;) String keyword) throws UnsupportedEncodingException {
        userService.changeRole(userId);
        return &quot;redirect:/users/admin?page=&quot; + page + &quot;&amp;amp;keyword=&quot; + URLEncoder.encode(keyword, &quot;UTF-8&quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;User 관련 DTO&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserJoinRequest&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 시 사용되는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
public class UserJoinRequest {

    private String loginId;
    private String password;
    private String passwordCheck;
    private String nickname;

    public User toEntity(String encodedPassword) {
        return User.builder()
                .loginId(loginId)
                .password(encodedPassword)
                .nickname(nickname)
                .userRole(UserRole.BRONZE)
                .createdAt(LocalDateTime.now())
                .receivedLikeCnt(0)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserLoginRequest&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 시 사용되는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
public class UserLoginRequest {

    private String loginId;
    private String password;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserDto&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정보 수정, 탈퇴 등에 사용되는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@Builder
public class UserDto {

    private String loginId;
    private String nickname;
    private String nowPassword;
    private String newPassword;
    private String newPasswordCheck;

    public static UserDto of(User user) {
        return UserDto.builder()
                .loginId(user.getLoginId())
                .nickname(user.getNickname())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserCntDto&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈 화면에서 각각의 등급에 해당하는 User 수를 출력하기 위해 사용되는 DTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Data
@Builder
public class UserDto {

    private String loginId;
    private String nickname;
    private String nowPassword;
    private String newPassword;
    private String newPasswordCheck;

    public static UserDto of(User user) {
        return UserDto.builder()
                .loginId(user.getLoginId())
                .nickname(user.getNickname())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Spring Security&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인, 로그아웃, 인증, 인가 등은 Spring Security 사용 =&amp;gt; UserDetail, UserDetailService는 &lt;a href=&quot;https://chb2005.tistory.com/176&quot;&gt;[Spring Boot] Spring Security를 사용한 로그인 구현 (Form Login)&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BCryptPasswordEncoder&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비밀번호 암호화 및 암호화 된 비밀번호와 일치하는지 확인하는데 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class EncrypterConfig {

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Security Config&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;antMatchers().anonymous() =&amp;gt; 로그인하지 않은 유저들만 접근 가능&lt;/li&gt;
&lt;li&gt;antMatchers().authenticated() =&amp;gt; 로그인 한 유저들만 접근 가능&lt;/li&gt;
&lt;li&gt;antMatchers().hasAnyAuthority() =&amp;gt; 설정한 등급의 유저들만 접근 가능&lt;/li&gt;
&lt;li&gt;인증에 실패한 경우 MyAuthenticationEntryPoint, 인가에 실패한 경우 MyAccessDeniedHandler, 로그인에 성공한 경우 MyLoginSuccessHandler, 로그아웃에 성공한 경우 MyLogoutSuccessHandler 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserRepository userRepository;

    // 로그인하지 않은 유저들만 접근 가능한 URL
    private static final String[] anonymousUserUrl = {&quot;/users/login&quot;, &quot;/users/join&quot;};

    // 로그인한 유저들만 접근 가능한 URL
    private static final String[] authenticatedUserUrl = {&quot;/boards/**/**/edit&quot;, &quot;/boards/**/**/delete&quot;, &quot;/likes/**&quot;, &quot;/users/myPage/**&quot;, &quot;/users/edit&quot;, &quot;/users/delete&quot;};

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers(anonymousUserUrl).anonymous()
                .antMatchers(authenticatedUserUrl).authenticated()
                .antMatchers(&quot;/boards/greeting/write&quot;).hasAnyAuthority(&quot;BRONZE&quot;, &quot;ADMIN&quot;)
                .antMatchers(HttpMethod.POST, &quot;/boards/greeting&quot;).hasAnyAuthority(&quot;BRONZE&quot;, &quot;ADMIN&quot;)
                .antMatchers(&quot;/boards/free/write&quot;).hasAnyAuthority(&quot;SILVER&quot;, &quot;GOLD&quot;, &quot;ADMIN&quot;)
                .antMatchers(HttpMethod.POST, &quot;/boards/free&quot;).hasAnyAuthority(&quot;SILVER&quot;, &quot;GOLD&quot;, &quot;ADMIN&quot;)
                .antMatchers(&quot;/boards/gold/**&quot;).hasAnyAuthority(&quot;GOLD&quot;, &quot;ADMIN&quot;)
                .antMatchers(&quot;/users/admin/**&quot;).hasAuthority(&quot;ADMIN&quot;)
                .antMatchers(&quot;/comments/**&quot;).hasAnyAuthority(&quot;BRONZE&quot;, &quot;SILVER&quot;, &quot;GOLD&quot;, &quot;ADMIN&quot;)
                .anyRequest().permitAll()
                .and()
                .exceptionHandling()
                .accessDeniedHandler(new MyAccessDeniedHandler(userRepository))           // 인가 실패
                .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 인증 실패
                .and()
                // 폼 로그인
                .formLogin()
                .loginPage(&quot;/users/login&quot;)      // 로그인 페이지
                .usernameParameter(&quot;loginId&quot;)   // 로그인에 사용될 id
                .passwordParameter(&quot;password&quot;)  // 로그인에 사용될 password
                .failureUrl(&quot;/users/login?fail&quot;)         // 로그인 실패 시 redirect 될 URL =&amp;gt; 실패 메세지 출력
                .successHandler(new MyLoginSuccessHandler(userRepository))    // 로그인 성공 시 실행 될 Handler
                .and()
                // 로그아웃
                .logout()
                .logoutUrl(&quot;/users/logout&quot;)     // 로그아웃 URL
                .invalidateHttpSession(true).deleteCookies(&quot;JSESSIONID&quot;)
                .logoutSuccessHandler(new MyLogoutSuccessHandler())
                .and()
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증 실패&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증에 실패한 경우 (로그인하지 않은 유저가 로그인이 필요한 URL에 접근한 경우) MyAuthenticationEntryPoint 호출&lt;/li&gt;
&lt;li&gt;HttpServletResponse, PrintWriter을 사용하여 메세지 출력 후 지정한 URL로 이동 시켜줌&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        // 메세지 출력 후 홈으로 redirect
        response.setContentType(&quot;text/html&quot;);
        PrintWriter pw = response.getWriter();
        pw.println(&quot;&amp;lt;script&amp;gt;alert('로그인한 유저만 가능합니다!'); location.href='/users/login';&amp;lt;/script&amp;gt;&quot;);
        pw.flush();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인가 실패&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인가에 실패한 경우 (일반 유저가 관리자 권한이 필요한 URL에 접근한 경우 등) MyAccessDeniedHandler 호출&lt;/li&gt;
&lt;li&gt;인가에 실패한 URL을 통해 어떤 상황인지 파악 후 알맞은 메세지와 다음 페이지를 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@AllArgsConstructor
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    private final UserRepository userRepository;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        User loginUser = null;
        if (auth != null) {
            loginUser = userRepository.findByLoginId(auth.getName()).get();
        }
        String requestURI = request.getRequestURI();

        // 로그인한 유저가 login, join을 시도한 경우
        if (requestURI.contains(&quot;/users/login&quot;) || requestURI.contains(&quot;/users/join&quot;)) {
            // 메세지 출력 후 홈으로 redirect
            response.setContentType(&quot;text/html&quot;);
            PrintWriter pw = response.getWriter();
            pw.println(&quot;&amp;lt;script&amp;gt;alert('이미 로그인 되어있습니다!'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
            pw.flush();
        }
        // 골드게시판은 GOLD, ADMIN만 접근 가능
        else if (requestURI.contains(&quot;gold&quot;)) {
            // 메세지 출력 후 홈으로 redirect
            response.setContentType(&quot;text/html&quot;);
            PrintWriter pw = response.getWriter();
            pw.println(&quot;&amp;lt;script&amp;gt;alert('골드 등급 이상의 유저만 접근 가능합니다!'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
            pw.flush();
        } else  if (loginUser != null &amp;amp;&amp;amp; loginUser.getUserRole().equals(UserRole.BLACKLIST)){
            // 메세지 출력 후 홈으로 redirect
            response.setContentType(&quot;text/html&quot;);
            PrintWriter pw = response.getWriter();
            pw.println(&quot;&amp;lt;script&amp;gt;alert('블랙리스트는 글, 댓글 작성이 불가능합니다.'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
            pw.flush();
        }
        // BRONZE 등급이 자유게시판에 글을 작성하려는 경우
        else if (requestURI.contains(&quot;free/write&quot;)) {
            // 메세지 출력 후 홈으로 redirect
            response.setContentType(&quot;text/html&quot;);
            PrintWriter pw = response.getWriter();
            pw.println(&quot;&amp;lt;script&amp;gt;alert('가입인사 작성 후 작성 가능합니다!'); location.href='/boards/greeting';&amp;lt;/script&amp;gt;&quot;);
            pw.flush();
        }
        // SILVER 등급 이상이 가입인사를 작성하려는 경우
        else if (requestURI.contains(&quot;greeting&quot;)) {
            // 메세지 출력 후 홈으로 redirect
            response.setContentType(&quot;text/html&quot;);
            PrintWriter pw = response.getWriter();
            pw.println(&quot;&amp;lt;script&amp;gt;alert('가입인사는 한 번만 작성 가능합니다!'); location.href='/boards/greeting';&amp;lt;/script&amp;gt;&quot;);
            pw.flush();
        }
        // ADMIN이 아닌데 관리자 페이지에 접속한 경우
        else if (requestURI.contains(&quot;admin&quot;)) {
            // 메세지 출력 후 홈으로 redirect
            response.setContentType(&quot;text/html&quot;);
            PrintWriter pw = response.getWriter();
            pw.println(&quot;&amp;lt;script&amp;gt;alert('관리자만 접속 가능합니다!'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
            pw.flush();

        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그인 성공&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인에 성공한 경우 MyLoginSuccessHandler 호출&lt;/li&gt;
&lt;li&gt;유저에 맞는 메세지 출력 후 세션의 &quot;prevPage&quot;에 담긴 URL로 이동 시켜줌
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;prevPage에는 로그인 하기 전 URL이 저장되어 있음&lt;/li&gt;
&lt;li&gt;로그인 페이지 접속 시 해당 값 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@AllArgsConstructor
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {

    private final UserRepository userRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 세션 유지 시간 = 3600초
        HttpSession session = request.getSession();
        session.setMaxInactiveInterval(3600);

        User loginUser = userRepository.findByLoginId(authentication.getName()).get();

        // 성공 시 메세지 출력 후 홈 화면으로 redirect
        response.setContentType(&quot;text/html&quot;);
        PrintWriter pw = response.getWriter();
        if (loginUser.getUserRole().equals(UserRole.BLACKLIST)) {
            pw.println(&quot;&amp;lt;script&amp;gt;alert('&quot; + loginUser.getNickname() + &quot;님은 블랙리스트 입니다. 글, 댓글 작성이 불가능합니다.'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
        } else {
            String prevPage = (String) request.getSession().getAttribute(&quot;prevPage&quot;);
            if (prevPage != null) {
                pw.println(&quot;&amp;lt;script&amp;gt;alert('&quot; + loginUser.getNickname() + &quot;님 반갑습니다!'); location.href='&quot; + prevPage + &quot;';&amp;lt;/script&amp;gt;&quot;);
            } else {
                pw.println(&quot;&amp;lt;script&amp;gt;alert('&quot; + loginUser.getNickname() + &quot;님 반갑습니다!'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
            }
        }
        pw.flush();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;로그아웃 성공&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그아웃에 성공한 경우 MyLogoutSuccessHandler 호출&lt;/li&gt;
&lt;li&gt;메세지 출력후 홈으로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType(&quot;text/html&quot;);
        PrintWriter out = response.getWriter();
        out.println(&quot;&amp;lt;script&amp;gt;alert('로그아웃 되었습니다.'); location.href='/';&amp;lt;/script&amp;gt;&quot;);
        out.flush();
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Spring Boot/프로젝트</category>
      <category>Spring Boot 게시판 - 유저 기능.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/196</guid>
      <comments>https://chb2005.tistory.com/196#entry196comment</comments>
      <pubDate>Mon, 17 Apr 2023 16:01:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] 게시판 만들기 2 - 라이브러리 설치, ERD, Entity 생성</title>
      <link>https://chb2005.tistory.com/195</link>
      <description>&lt;h1&gt;프로젝트에 사용한 라이브러리 설치 (build.gradle)&lt;/h1&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

// DB
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Thymeleaf, Validation
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Spring Security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.7.5'&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;ERD&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sdprm/btsak9mqRCW/k08dSAJUbPjRk0mqPzYaLK/img.png&quot; alt=&quot;&quot; width=&quot;550&quot; height=&quot;621&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Entity 설명&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;User&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id(primary key), login_id(로그인에 사용되는 아이디), password, nickname, user_role(등급), received_like_cnt(이 유저가 받은 좋아요 수), created_at(가입일)&lt;/li&gt;
&lt;li&gt;User은 여러개의 Board를 작성할 수 있음 =&amp;gt; User : Board = 1 : N&lt;/li&gt;
&lt;li&gt;User은 여러개의 Comment를 작성할 수 있음 =&amp;gt; User : Comment = 1 : N&lt;/li&gt;
&lt;li&gt;User은 여러개의 좋아요를 추가할 수 있음 =&amp;gt; User : Like = 1 : N&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Board&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id(primary key), title(제목), body(내용), category, like_cnt(이 글이 받은 좋아요 수), comment_cnt(이 글이 받은 댓글 수), created_at(작성일), last_modified_at(최근 수정일)&lt;/li&gt;
&lt;li&gt;Board와 Upload Image는 1:1 관계 =&amp;gt; Board에 Upload Image의 Id(foreign key)를 넣음 =&amp;gt; Board가 연관관계 주인&lt;/li&gt;
&lt;li&gt;Board에는 여러개의 Comment가 달릴 수 있음 =&amp;gt; Board : Comment = 1 : N&lt;/li&gt;
&lt;li&gt;Board에는 여러개의 Like가 추가될 수 있음 =&amp;gt; Board : Like = 1 : N&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Comment&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id(primary key), body(내용), created_at(작성일), last_modified_at(최근 수정일)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Like&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Like는 user_id, board_id를 foreign key로 갖는데, user_id는 좋아요를 누른 유저의 아이디이고, board_id는 좋아요가 달린 게시글의 id&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Upload Image&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;id(primary key), original_filename(유저가 업로드한 이미지의 원본 파일명), saved_filename(서버에 저장된 파일명)&lt;/li&gt;
&lt;li&gt;원본 파일명으로 파일을 저장하면 파일명이 중복되어 에러가 발생할 수 있음 =&amp;gt; 이를 방지하기 위해 서버에는 중복되지 않는 UUID로 파일명을 수정 후 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Entity 구현 코드&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;User&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;User가 삭제(탈퇴)되면 유저의 Board, Comment, Like 모두 삭제 =&amp;gt; orphanrRemoval=true 설정&lt;/li&gt;
&lt;li&gt;UserRole에는 @Enumerated(EnumType.STRING)을 설정 =&amp;gt; DB에 저장될 때, &quot;BRONZE&quot;, &quot;SILVER&quot;와 같이 String 타입으로 저장됨&lt;/li&gt;
&lt;li&gt;User Entity에는 rankUp(), likeChange(), edit(), changeRole() 4개의 메소드가 존재
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;rankUp() : BRONZE -&amp;gt; SILVER, SILVER -&amp;gt; GOLD로 승급할 때 사용&lt;/li&gt;
&lt;li&gt;likeChange() : 이 유저의 글에 좋아요가 추가되거나, 취소된 경우 유저의 receivedLikeCnt를 계산하여 수정할 때 사용&lt;/li&gt;
&lt;li&gt;edit() : 유저가 수정할 수 있는 nickname, password를 수정할 때 사용&lt;/li&gt;
&lt;li&gt;changeRole() : rankUp()과는 달리 관리자가 이 유저의 등급을 수정할 때 사용, BRONZE -&amp;gt; SILVER -&amp;gt; GOLD -&amp;gt; BLACKLIST -&amp;gt; BRONZE 순으로 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class User {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String loginId;     // 로그인할 때 사용하는 아이디
    private String password;    // 비밀번호
    private String nickname;    // 닉네임
    private LocalDateTime createdAt;    // 가입 시간
    private Integer receivedLikeCnt; // 유저가 받은 좋아요 개수 (본인 제외)

    @Enumerated(EnumType.STRING)
    private UserRole userRole;      // 권한

    @OneToMany(mappedBy = &quot;user&quot;, orphanRemoval = true)
    private List&amp;lt;Board&amp;gt; boards;     // 작성글

    @OneToMany(mappedBy = &quot;user&quot;, orphanRemoval = true)
    private List&amp;lt;Like&amp;gt; likes;       // 유저가 누른 좋아요

    @OneToMany(mappedBy = &quot;user&quot;, orphanRemoval = true)
    private List&amp;lt;Comment&amp;gt; comments; // 댓글

    public void rankUp(UserRole userRole, Authentication auth) {
        this.userRole = userRole;

        // 현재 저장되어 있는 Authentication 수정 =&amp;gt; 재로그인 하지 않아도 권한 수정 되기 위함
        List&amp;lt;GrantedAuthority&amp;gt; updatedAuthorities = new ArrayList&amp;lt;&amp;gt;();
        updatedAuthorities.add(new SimpleGrantedAuthority(userRole.name()));
        Authentication newAuth = new UsernamePasswordAuthenticationToken(auth.getPrincipal(), auth.getCredentials(), updatedAuthorities);
        SecurityContextHolder.getContext().setAuthentication(newAuth);
    }

    public void likeChange(Integer receivedLikeCnt) {
        this.receivedLikeCnt = receivedLikeCnt;
        if (this.receivedLikeCnt &amp;gt;= 10 &amp;amp;&amp;amp; this.userRole.equals(UserRole.SILVER)) {
            this.userRole = UserRole.GOLD;
        }
    }

    public void edit(String newPassword, String newNickname) {
        this.password = newPassword;
        this.nickname = newNickname;
    }

    public void changeRole() {
        if (userRole.equals(UserRole.BRONZE)) userRole = UserRole.SILVER;
        else if (userRole.equals(UserRole.SILVER)) userRole = UserRole.GOLD;
        else if (userRole.equals(UserRole.GOLD)) userRole = UserRole.BLACKLIST;
        else if (userRole.equals(UserRole.BLACKLIST)) userRole = UserRole.BRONZE;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UserRole&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum UserRole {
    BRONZE, SILVER, GOLD, BLACKLIST, ADMIN;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Board&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;좋아요 수, 댓글 수를 저장한 이유는 리스트에서 정렬할 때 더 빠른 조회를 위해 따로 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Board extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;   // 제목
    private String body;    // 본문

    @Enumerated(EnumType.STRING)
    private BoardCategory category; // 카테고리

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;      // 작성자

    @OneToMany(mappedBy = &quot;board&quot;, orphanRemoval = true)
    private List&amp;lt;Like&amp;gt; likes;       // 좋아요
    private Integer likeCnt;        // 좋아요 수

    @OneToMany(mappedBy = &quot;board&quot;, orphanRemoval = true)
    private List&amp;lt;Comment&amp;gt; comments; // 댓글
    private Integer commentCnt;     // 댓글 수

    @OneToOne(fetch = FetchType.LAZY)
    private UploadImage uploadImage;

    public void update(BoardDto dto) {
        this.title = dto.getTitle();
        this.body = dto.getBody();
    }

    public void likeChange(Integer likeCnt) {
        this.likeCnt = likeCnt;
    }

    public void commentChange(Integer commentCnt) {
        this.commentCnt = commentCnt;
    }

    public void setUploadImage(UploadImage uploadImage) {
        this.uploadImage = uploadImage;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BoardCategory&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;of 메소드를 통해 String 타입의 category를 BoardCategory 타입으로 변환하는 기능 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public enum BoardCategory {
    FREE, GREETING, GOLD;

    public static BoardCategory of(String category) {
        if (category.equalsIgnoreCase(&quot;free&quot;)) return BoardCategory.FREE;
        else if (category.equalsIgnoreCase(&quot;greeting&quot;)) return BoardCategory.GREETING;
        else if (category.equalsIgnoreCase(&quot;gold&quot;)) return BoardCategory.GOLD;
        else return null;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Comment&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;댓글은 수정이 가능하기 때문에 update() 메소드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class Comment extends BaseEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String body;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;      // 작성자

    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;    // 댓글이 달린 게시판

    public void update(String newBody) {
        this.body = newBody;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Like&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL에서 Table 명을 like로 사용할 수 없는데, @Table(name = &quot;&quot;like&quot;&quot;)를 통해 like로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Table(name = &quot;\&quot;like\&quot;&quot;)
public class Like {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;      // 좋아요를 누른 유저

    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;    // 좋아요가 추가된 게시글

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UploadImage&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class UploadImage {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String originalFilename;    // 원본 파일명
    private String savedFilename;        // 서버에 저장된 파일명

    @OneToOne(mappedBy = &quot;uploadImage&quot;, fetch = FetchType.LAZY)
    private Board board;

}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BaseEntity&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 객체가 DB에 저장될 때, 자동으로 createdAt, lastModifiedAt을 저장하기 위해 사용되는 Entity&lt;/li&gt;
&lt;li&gt;아래의 JpaAuditingConfig를 등록하고, Board와 같이 상속시켜 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime lastModifiedAt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;JpaAuditingConfig&lt;/h2&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Spring Boot/프로젝트</category>
      <category>Spring Boot 게시판 - 라이브러리 설치 &amp;amp; ERD &amp;amp; Entity 생성.</category>
      <author>chb2005</author>
      <guid isPermaLink="true">https://chb2005.tistory.com/195</guid>
      <comments>https://chb2005.tistory.com/195#entry195comment</comments>
      <pubDate>Mon, 17 Apr 2023 05:43:42 +0900</pubDate>
    </item>
  </channel>
</rss>