<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>seren-dev.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 08 Sep 2022 01:37:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. seren-dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/serendipity-dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[블로그 이전]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84</guid>
            <pubDate>Thu, 08 Sep 2022 01:37:11 GMT</pubDate>
            <description><![CDATA[<p>티스토리로 블로그 이전
-&gt; <a href="https://serendev.tistory.com/">https://serendev.tistory.com/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 12933 오리[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-12933-%EC%98%A4%EB%A6%ACJava</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-12933-%EC%98%A4%EB%A6%ACJava</guid>
            <pubDate>Wed, 07 Sep 2022 15:20:09 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/12933">https://www.acmicpc.net/problem/12933</a></p>
<h3 id="풀이">풀이</h3>
<p>올바른 오리의 울음 소리는 울음 소리를 한 번 또는 그 이상 연속해서 내는 것이다. 예를 들어, &quot;quack&quot;, &quot;quackquackquackquack&quot;, &quot;quackquack&quot;는 올바른 오리의 울음 소리이다.</p>
<p>오리의 수를 세기 위해 <code>HashMap</code>을 사용하여 <code>map(오리 번호, 해당 오리의 현재 문자열 사이즈)</code> 을 저장한다.
그 다음 문자를 입력받을 때마다, map 엔트리를 탐색해 알맞은 오리를 찾아 그 오리의 value값에 1을 더한다.
예를 들면, <strong>&#39;a&#39;(idx=2) 를 만나면 값이 2(&#39;qu&#39;)인 map 엔트리의 값을 3으로 바꾼다.</strong></p>
<p><strong>주의</strong></p>
<ul>
<li>문자열의 문자를 체크하다가 오리의 울음소리가 될 수 없는 소리(문자)를 만날 경우를 처리하기 위해 <code>boolean</code>형 변수 <code>flag</code>, <code>fail</code>을 추가한다.
ex) 예제 입력 6 <code>quackqauckquack</code></li>
<li>map의 엔트리를 탐색하다가 값이 5가 아닌 오리가 있을 경우, 올바른 오리의 울음소리가 아니므로, 이 경우도 -1을 출력해야 한다.</li>
</ul>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;


public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String str = br.readLine();
        String duck = &quot;quack&quot;;
        HashMap&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
        int num = 0; // 오리 번호

        boolean fail = false; // 오리의 울음소리가 될 수 없는 소리(문자)가 있을 때 true

        int cnt = 0;

        for (int i = 0; i &lt; str.length(); i++) {

            if (str.charAt(i) == &#39;q&#39;) { // 문자가 &#39;q&#39;인 경우 따로 처리

                boolean flag = false;
                for (int key: map.keySet()) {
                    if (map.get(key) == 5) {
                        map.put(key, 1);
                        flag = true;
                        break;
                    }
                }
                if (!flag) // 새로운 오리 추가
                    map.put(num++, 1);
            }

            else {

                int idx = duck.indexOf(String.valueOf(str.charAt(i))); // duck 문자열의 인덱스 위치

                boolean flag = false;  // 오리의 울음소리가 될 수 없는 소리(문자)가 있을 때 true

                for (int key: map.keySet()) {

                    if (map.get(key) == idx) {
                        map.put(key, map.get(key)+1);
                        flag = true;
                        break;
                    }
                }

                if (!flag) {
                    // 올바른 오리의 울음소리가 아니므로 반복문 종료
                    fail = true;
                    break;
                }

            }
        }

        if (fail) {
            // 올바른 오리의 울음소리가 아니므로 -1 출력
            System.out.println(-1);
        }
        else {
            for (int key : map.keySet()) {

                if (map.get(key) == 5) {
                    // 값이 5라면 올바른 오리가 있다
                    cnt++;
                }
                else {
                    // 올바른 오리의 울음소리가 아닌 경우
                    cnt = 0;
                    break;
                }
            }

            System.out.println(cnt == 0 ? -1 : cnt);
        }

    }

}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1913 달팽이[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-1913-%EB%8B%AC%ED%8C%BD%EC%9D%B4Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-1913-%EB%8B%AC%ED%8C%BD%EC%9D%B4Java</guid>
            <pubDate>Tue, 06 Sep 2022 03:12:34 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/1913">https://www.acmicpc.net/problem/1913</a></p>
<h3 id="풀이">풀이</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/d6b1a0b8-7601-45b5-a4b0-e4a60d8c7f51/image.png" alt=""></p>
<p>규칙을 찾기 위해 위 그림과 같이 N=3, N=5인 경우 한 방향으로 이동할 때 이동하는 칸 수를 셌다.
ex)
1 -&gt; 2 : 1
2 -&gt; 3 : 1
3 -&gt; 5 : 2</p>
<pre><code>N = 3 : 1 1 2 2 2

N = 5 : 1 1 2 2 3 3 4 4 4</code></pre><p>1 2번, 2 2번,..., (N-1) 3번이다.
위 규칙을 활용하여 문제를 풀었다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;


public class Main {

    static int[] dx = {-1, 0, 1, 0};
    static int[] dy = {0, 1, 0, -1};

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int k = Integer.parseInt(br.readLine());
        int[][] arr = new int[n][n];

        int i = n/2, j = n/2; //배열 인덱스
        int dir = 0; //방향 인덱스
        int num = 1; // 배열에 저장할 숫자

        int cnt = 0; // 현재 line 으로 총 2번 이동했는지 카운트

        int line = 1; // 이동해야 하는 칸 수
        int move = 0; // 현재 이동한 칸 수

        int xIdx = 0, yIdx = 0; //정답 인덱스

        while(num &lt;= n*n) {
            move = 0;

            while (move &lt; line) {
                arr[i][j] = num;
                if (num == k) {
                    xIdx = i;
                    yIdx = j;
                }
                num++;
                i += dx[dir];
                j += dy[dir];
                move++;
            }
            cnt++;

            dir = (dir+1) % 4; // 방향 변환

            // 이동하는 칸 수가 N-1일 경우 총 3번을 이동해야 하기 때문에 따로 처리
            if (line == n-1) {
                if (cnt == 3) {
                    break;
                }
            }

            // 현재 line으로 2번 이동했으면 이동하는 칸 수 증가
            else if (cnt &gt;= 2) {
                line++;
                cnt = 0;
            }

        }
        arr[0][0] = num;
        xIdx++;
        yIdx++;

        StringBuilder sb = new StringBuilder();
        for (i = 0; i &lt; n; i++) {
            for (j = 0; j &lt; n; j++)
                sb.append(arr[i][j] + &quot; &quot;);
            sb.append(&quot;\n&quot;);
        }
        sb.append(xIdx + &quot; &quot; + yIdx);

        System.out.println(sb);

    }
}</code></pre><ul>
<li>방향 배열을 정의하여 행 인덱스 <code>i</code>에는 <code>dx[dir]</code>을 더하고 열 인덱스 <code>j</code>에는 <code>dy[dir]</code>을 더한다.</li>
</ul>
<p><strong>주의</strong></p>
<ul>
<li>로직 순서: <strong>배열에 숫자를 저장 -&gt; 이동</strong>하기 때문에 마지막 숫자(n*n)를 배열에 따로 저장하는 코드가 있어야 한다.
<code>arr[0][0] = num;</code></li>
<li><code>xIdx</code>, <code>yIdx</code>는 0부터 시작하므로 각각 1을 더해야 한다.
<code>xIdx++;</code>
<code>yIdx++;</code></li>
<li>위치를 찾고자 하는 자연수가 n<em>n일 수 있으므로 <code>xIdx</code>, <code>yIdx</code> 모두 *</em>0으로 초기화**한다.</li>
</ul>
<hr>
<h3 id="다른-풀이-2">다른 풀이 2</h3>
<p>위 방법과 같이 안쪽에서 바깥쪽으로 배열을 채워나가며, 이동하는 칸의 횟수가 1 &gt; 1 &gt; 2 &gt; 2 &gt; 3 &gt; 3&gt; 4 &gt; ... &gt; n 씩 이동하는 것을 활용하여 구현한다.</p>
<pre><code>import java.io.*;
import java.util.*;


public class Main {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int m = Integer.parseInt(br.readLine());
        int[][] arr = new int[n][n];

        int i = n/2, j = n/2; //배열 인덱스
        int num = 1; // 배열에 저장할 숫자

        int line = 1; // 이동해야 하는 칸 수

        int xIdx = 0, yIdx = 0; //정답 인덱스

        while(true) {

            for (int k = 0; k &lt; line; k++) {
                if (num == m) {
                    xIdx = i;
                    yIdx = j;
                }
                arr[i--][j] = num++;
            }

            // 배열을 다 채웠으면 이 때 num = n*n + 1
            if (num &gt; n*n)
                break;

            for (int k = 0; k &lt; line; k++) {
                if (num == m) {
                    xIdx = i;
                    yIdx = j;
                }
                arr[i][j++] = num++;
            }
            line++;

            for (int k = 0; k &lt; line; k++) {
                if (num == m) {
                    xIdx = i;
                    yIdx = j;
                }
                arr[i++][j] = num++;
            }
            for (int k = 0; k &lt; line; k++) {
                if (num == m) {
                    xIdx = i;
                    yIdx = j;
                }
                arr[i][j--] = num++;
            }
            line++;

        }

        xIdx++;
        yIdx++;

        StringBuilder sb = new StringBuilder();
        for (i = 0; i &lt; n; i++) {
            for (j = 0; j &lt; n; j++)
                sb.append(arr[i][j] + &quot; &quot;);
            sb.append(&quot;\n&quot;);
        }
        sb.append(xIdx + &quot; &quot; + yIdx);

        System.out.println(sb);

    }
}</code></pre><ul>
<li>이동 방향은 모든 경우에 대해 항상 같으므로 방향 배열이 필요 없다.</li>
<li>두 번의 반복문을 수행하면 이동하는 칸의 횟수가 늘어나므로 두번째, 네번째 반복문 다음에 <code>line++</code></li>
<li><code>arr[0][0]</code>까지 배열을 다 채웠으면 이 때 <code>num = n*n + 1</code>이 되므로 반복문을 종료해야 한다.<pre><code>마지막 반복문 (N = 5)
num = 21 -&gt; 22 -&gt; 23 -&gt; 24 -&gt; 25 -&gt; </code></pre>참고: <a href="https://loosie.tistory.com/538">https://loosie.tistory.com/538</a></li>
</ul>
<hr>
<h3 id="다른-풀이-3">다른 풀이 3</h3>
<p>위 방법들과 다르게 바깥쪽에서 안쪽으로 배열을 채워나간다.</p>
<pre><code>import java.io.*;
import java.util.*;


public class Main {

    static int[] dx = {1, 0, -1, 0};
    static int[] dy = {0, 1, 0, -1};

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int m = Integer.parseInt(br.readLine());
        int[][] arr = new int[n][n];

        int i = 0, j = 0; // 배열 인덱스
        int num = n * n; // 배열에 저장할 숫자
        int dir = 0; // 방향 인덱스

        int xIdx = 0, yIdx = 0; //정답 인덱스

        while(num &gt; 0) {

            if (num == m) {
                xIdx = i+1;
                yIdx = j+1;
            }
            arr[i][j] = num--;

            i += dx[dir];
            j += dy[dir];

            if (i &lt; 0 || j &lt; 0 || i &gt;= n || j &gt;= n || arr[i][j] != 0 ) {
                // 이전 위치로 복구
                i -= dx[dir];
                j -= dy[dir];

                dir = (dir + 1) % 4; // 뱡향 변환

                // 바뀐 방향으로 이동
                i += dx[dir];
                j += dy[dir];
            }
        }

        StringBuilder sb = new StringBuilder();
        for (i = 0; i &lt; n; i++) {
            for (j = 0; j &lt; n; j++)
                sb.append(arr[i][j] + &quot; &quot;);
            sb.append(&quot;\n&quot;);
        }
        sb.append(xIdx + &quot; &quot; + yIdx);

        System.out.println(sb);

    }
}</code></pre><ul>
<li><code>i &lt; 0 || j &lt; 0 || i &gt;= n || j &gt;= n || arr[i][j] != 0</code>
이동한 위치가 배열의 인덱스 밖이거나, 이미 배열에 값이 채워져있다면 이전 위치로 복구한 다음 새로운 방향으로 이동한다.</li>
</ul>
<p>참고: <a href="https://bbangson.tistory.com/110">https://bbangson.tistory.com/110</a></p>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/a2b034f2-579b-484f-a54d-664ff58a7630/image.png" alt=""> </p>
<ul>
<li>바깥쪽에서 안쪽으로 배열을 채워나가는 3번째 방법이 제일 코드가 깔끔하고 가독성이 높다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 20546 기적의 매매법[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-20546-%EA%B8%B0%EC%A0%81%EC%9D%98-%EB%A7%A4%EB%A7%A4%EB%B2%95Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-20546-%EA%B8%B0%EC%A0%81%EC%9D%98-%EB%A7%A4%EB%A7%A4%EB%B2%95Java</guid>
            <pubDate>Mon, 05 Sep 2022 10:26:08 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/20546">https://www.acmicpc.net/problem/20546</a></p>
<h3 id="풀이">풀이</h3>
<p>준현이와 성민이가 주식을 매매하는 방법은 다르다. 이를 함수로 구현하고, 배열에 주식 가격을 저장한다. 단, <strong>마지막 날의 주식 가격은 따로 저장</strong>한다. 즉, arr 배열에는 13일까지의 주식 가격만 저장한다.
주식 가격의 상승/하락 여부는 <code>boolean</code>형 변수로 나타낸다. </p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static int funcOne(int n, int[] arr, int last) {
        int cnt = 0;
        for (int price : arr) {
            if (n &gt;= price) {
                cnt += n / price;
                n %= price;
            }
        }
        return n + cnt * last;
    }

    public static int funcTwo(int n, int[] arr, int last) {
        boolean upOne = false, upTwo = false, downOne = false, downTwo = false;
        int cnt = 0;

        for (int i = 1; i &lt; arr.length; i++) {
            if (arr[i] &gt; arr[i-1]) {
                downOne = downTwo = false;
                if (!upOne)
                    upOne = true;
                else if (!upTwo)
                    upTwo = true;
                else {
                    n += arr[i] * cnt;
                    cnt = 0;
                }
            }
            else if (arr[i] &lt; arr[i-1]) {
                upOne = upTwo = false;
                if (!downOne)
                    downOne = true;
                else if (!downTwo)
                    downTwo = true;
                else {
                    cnt += n / arr[i];
                    n %= arr[i];
                }
            }
        }

        return n + cnt * last;
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[13];

        StringTokenizer st = new StringTokenizer(br.readLine());

        for (int i = 0; i &lt; 13; i++)
            arr[i] = Integer.parseInt(st.nextToken());
        int last = Integer.parseInt(st.nextToken());

        int first = funcOne(n, arr, last);
        int second = funcTwo(n, arr, last);

        if (first == second)
            System.out.println(&quot;SAMESAME&quot;);
        else
            System.out.println(first &gt; second ? &quot;BNP&quot; : &quot;TIMING&quot;);

    }
}</code></pre><h3 id="다른-풀이">다른 풀이</h3>
<p><code>boolean</code>형 변수를 사용하지 않고, 주식이 상승/하락할 때마다 <code>upCnt</code>/<code>downCnt</code>에 1을 더한다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static int funcOne(int n, int[] arr, int last) {
        int cnt = 0;
        for (int price : arr) {
            if (n &gt;= price) {
                cnt += n / price;
                n %= price;
            }
        }
        return n + cnt * last;
    }

    public static int funcTwo(int n, int[] arr, int last) {
        int cnt = 0, upCnt = 0, downCnt = 0;

        for (int i = 1; i &lt; arr.length; i++) {
            if (arr[i] &gt; arr[i-1]) {
                downCnt = 0;
                if (upCnt &lt; 2)
                    upCnt++;
                else {
                    n += arr[i] * cnt;
                    cnt = 0;
                }
            }
            else if (arr[i] &lt; arr[i-1]) {
                upCnt = 0;
                if (downCnt &lt; 2)
                    downCnt++;
                else {
                    cnt += n / arr[i];
                    n %= arr[i];
                }
            }
        }

        return n + cnt * last;
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[13];

        StringTokenizer st = new StringTokenizer(br.readLine());

        for (int i = 0; i &lt; 13; i++)
            arr[i] = Integer.parseInt(st.nextToken());
        int last = Integer.parseInt(st.nextToken());

        int first = funcOne(n, arr, last);
        int second = funcTwo(n, arr, last);

        if (first == second)
            System.out.println(&quot;SAMESAME&quot;);
        else
            System.out.println(first &gt; second ? &quot;BNP&quot; : &quot;TIMING&quot;);

    }
}</code></pre><p><img src="https://velog.velcdn.com/images/serendipity-dev/post/52c55c50-5df1-4b37-ad5c-e6ac98b977c9/image.png" alt="">
첫번째 방법과 두번째 방법의 메모리와 실행시간의 차이가 크지 않다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2981 검문[Java]⭐]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2981-%EA%B2%80%EB%AC%B8Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2981-%EA%B2%80%EB%AC%B8Java</guid>
            <pubDate>Mon, 05 Sep 2022 04:42:32 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/2981">https://www.acmicpc.net/problem/2981</a></p>
<h3 id="풀이">풀이</h3>
<p>처음에는 문제 그대로 N개의 숫자를 M으로 나누었을 때, 나머지가 모두 같게 되는 M을 모두 찾으려 했다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];

        StringBuilder sb = new StringBuilder();
        ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();
        int min, i;

        for (i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }

        Arrays.sort(arr);
        min = arr[0];

        for (i = 2; i &lt; arr[1]; i++) {
            int div = arr[0] % i;
            boolean flag = true;
            for (int idx = 1; idx &lt; n; idx++) {
                if (arr[idx] % i != div) {
                    flag = false;
                    break;
                }
            }
            if (flag) list.add(i);
        }


        for (int num : list)
            sb.append(num + &quot; &quot;);
        System.out.println(sb);
    }
}</code></pre><ul>
<li><code>Arrays.sort(arr);</code> : <code>arr</code> 정렬
<code>arr[0]</code>이 arr의 가장 작은 값이 된다.
<code>i</code>가 <code>arr[0]</code>보다 커지면 <strong>나머지는 <code>arr[0]</code></strong>이 되기 때문에 <code>i</code>를 <code>arr[1]</code> 전까지만 for문을 돌린다.
=&gt; <code>for (i = 2; i &lt; arr[1]; i++)</code></li>
</ul>
<p>하지만 숫자가 10억까지 입력할 수 있기 때문에 <strong>시간 초과</strong>가 났다. 그래서 <strong>두 수의 차이</strong>를 이용하여 실행시간을 줄인다.</p>
<hr>
<pre><code>a = b * i + d
e = u * i + d

e - a = (u-b) * i</code></pre><p>두 수의 차이를 <code>gap</code> 배열에 저장하고 그 차이가 <code>i</code>로 나누어 떨어지면 된다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];
        int[] gap = new int[n-1];

        StringBuilder sb = new StringBuilder();
        ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();
        int i;

        for (i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }

        Arrays.sort(arr);
        for (i = 1; i &lt; n; i++) {
            // gap에 두 수의 차이를 저장
            gap[i-1] = arr[i] - arr[i-1];
        }

        for (i = 2; i &lt; arr[1]; i++) {

            boolean flag = true;
            for (int idx = 0; idx &lt; n-1; idx++) {
                if (gap[idx] % i != 0) {
                    flag = false;
                    break;
                }
            }

            if (flag) list.add(i);
        }

        for (int num : list)
            sb.append(num + &quot; &quot;);
        System.out.println(sb);
    }
}</code></pre><p>하지만 76%에서 <strong>시간 초과</strong>가 뜬다.</p>
<p>생각해보니 <code>gap[idx] % i != 0</code>이면 <code>i</code>는 답이 될 수 없다.
=&gt; 즉 <code>gap[idx]</code> 보다 <code>i</code>가 크면 그 이후의 숫자부터는 답이 될 수 없으므로 <code>gap</code>에서 가장 작은 값까지만 for문을 도리면 된다. 그래서 <code>gap</code>을 정렬한 다음 <code>for (i = 2; i &lt;= gap[0]; i++)</code> 로 for문 을 돌렸다. 그 결과 통과할 수 있었다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];
        int[] gap = new int[n-1];

        StringBuilder sb = new StringBuilder();
        ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();
        int i;

        for (i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }

        Arrays.sort(arr);

        // gap에 두 수의 차이를 저장
        for (i = 1; i &lt; n; i++) {
            gap[i-1] = arr[i] - arr[i-1];
        }
        Arrays.sort(gap);
        int min = gap[0];

        for (i = 2; i &lt;= min; i++) {

            boolean flag = true;
            for (int idx = 0; idx &lt; n-1; idx++) {
                if (gap[idx] % i != 0) {
                    flag = false;
                    break;
                }
            }

            if (flag) list.add(i);
        }

        for (int num : list)
            sb.append(num + &quot; &quot;);
        System.out.println(sb);
    }
}</code></pre><hr>
<h3 id="다른-풀이---2">다른 풀이 - 2</h3>
<p>두 수의 차이를 <code>gap</code> 배열에 저장하고 그 차이가 <code>i</code>로 나누어 떨어지면 된다.
=&gt; <strong>두 수의 차이의 최대공약수</strong>를 구한 다음 계속해서 최대공약수를 업데이트하여 최종적으로 <strong>모든 수(두 수의 차이)의 최대공약수</strong>를 구한다. 그리고 <strong>그 수의 2 이상의 약수를 출력</strong>한다.
<code>gap</code> 배열을 사용할 필요 없으며 최대공약수를 구하는 <code>gcd</code> 함수를 사용한다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static int gcd(int a, int b) {
        int r;
        while (b &gt; 0) {
            r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];

        StringBuilder sb = new StringBuilder();
        int i;

        for (i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }

        Arrays.sort(arr);
        int val = arr[1] - arr[0];

        for (i = 2; i &lt; n; i++) {
            val = gcd(val, arr[i] - arr[i-1]);
        }

        for (i = 2; i &lt;= val; i++) {
            if (val % i == 0)
                sb.append(i + &quot; &quot;);
        }
        System.out.println(sb);
    }
}</code></pre><h3 id="다른-풀이---3">다른 풀이 - 3</h3>
<p>최대공약수의 약수를 굳이 끝까지 탐색할 필요가 없다. 굳이 약수를 찾는데 val까지 찾을 필요 없이 <strong>val의 절반까지만 탐색</strong>해도 된다.</p>
<p>물론 이 때 주의해야 할 것은 마지막에 최대공약수 자신도 출력해주어야 한다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static int gcd(int a, int b) {
        int r;
        while (b &gt; 0) {
            r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];

        StringBuilder sb = new StringBuilder();
        int i;

        for (i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }

        Arrays.sort(arr);
        int val = arr[1] - arr[0];

        for (i = 2; i &lt; n; i++) {
            val = gcd(val, arr[i] - arr[i-1]);
        }

        for (i = 2; i &lt;= val/2; i++) {
            if (val % i == 0)
                sb.append(i + &quot; &quot;);
        }
        sb.append(val);
        System.out.println(sb);
    }
}</code></pre><h3 id="다른-풀이---4">다른 풀이 - 4</h3>
<p>탐색 범위를 (val / 2) 가 아닌 제곱근, 즉 √val 까지만 탐색하여 i와 val/i를 ArrayList에 넣고 리스트를 정렬한 다음 출력한다. 물론 이 때 주의해야 할 것은 마지막에 최대공약수 자신도 list에 넣어주어야 한다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static int gcd(int a, int b) {
        int r;
        while (b &gt; 0) {
            r = a % b;
            a = b;
            b = r;
        }
        return a;
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        int[] arr = new int[n];

        StringBuilder sb = new StringBuilder();
        int i;

        for (i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(br.readLine());
        }

        Arrays.sort(arr);
        int val = arr[1] - arr[0];

        for (i = 2; i &lt; n; i++) {
            val = gcd(val, arr[i] - arr[i-1]);
        }

        ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();
        for (i = 2; i &lt;= Math.sqrt(val); i++) {
            if (Math.sqrt(val) == i) {
                list.add(i);
            }
            else if (val % i == 0) {
                list.add(i);
                list.add(val/i);
            }
        }
        list.add(val);

        Collections.sort(list);

        for (int num : list)
            sb.append(num + &quot; &quot;);
        System.out.println(sb);
    }
}</code></pre><p><img src="https://velog.velcdn.com/images/serendipity-dev/post/ebeaf0fe-9bd2-4478-aee6-05771ef98c4a/image.png" alt=""></p>
<p>4번째 방법이 메모리와 실행시간이 가장 작다.
최대공약수의 약수를 탐색하는 탐색 범위를 줄일 때 가장 실행시간이 크게 감소한 것을 볼 수 있다.</p>
<p>참고: <a href="https://st-lab.tistory.com/155">https://st-lab.tistory.com/155</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1004 어린 왕자[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-1004-%EC%96%B4%EB%A6%B0-%EC%99%95%EC%9E%90Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-1004-%EC%96%B4%EB%A6%B0-%EC%99%95%EC%9E%90Java</guid>
            <pubDate>Fri, 02 Sep 2022 07:28:52 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/1004">https://www.acmicpc.net/problem/1004</a></p>
<h3 id="풀이">풀이</h3>
<p><strong>출발점과 속한 원의 개수와 도착점이 속한 원의 개수를 더한다.</strong> 단, 두 점이 같은 원에 속할 경우 그 원은 제외한다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
        import java.util.*;

public class Main {

    public static double distancePow(int x1, int y1, int x2, int y2) {
        return (Math.pow(x1-x2, 2) + Math.pow(y1-y2, 2));
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine());

        StringBuilder sb = new StringBuilder();

        while(t-- &gt; 0) {
            int cnt = 0;
            StringTokenizer st = new StringTokenizer(br.readLine());

            int x1 = Integer.parseInt(st.nextToken());
            int y1 = Integer.parseInt(st.nextToken());
            int x2 = Integer.parseInt(st.nextToken());
            int y2 = Integer.parseInt(st.nextToken());

            int n = Integer.parseInt(br.readLine());

            for (int i = 0; i &lt; n; i++) {
                st = new StringTokenizer(br.readLine());
                int x = Integer.parseInt(st.nextToken());
                int y = Integer.parseInt(st.nextToken());
                int r = Integer.parseInt(st.nextToken());

                double d1 = distancePow(x,y,x1,y1);
                double d2 = distancePow(x,y,x2,y2);
                r = r*r;

                if (d1 &lt; r &amp;&amp; d2 &gt; r) //출발점만 현재 원에 속하는 경우
                    cnt++;
                else if (d1 &gt; r &amp;&amp; d2 &lt; r) //도착점만 현재 원에 속하는 경우
                    cnt++;

            }

            sb.append(cnt+&quot;\n&quot;);
        }

        System.out.println(sb);
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2447 참외밭[Java]⭐]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2447-%EC%B0%B8%EC%99%B8%EB%B0%ADJava</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2447-%EC%B0%B8%EC%99%B8%EB%B0%ADJava</guid>
            <pubDate>Fri, 02 Sep 2022 06:39:51 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/2477">https://www.acmicpc.net/problem/2477</a></p>
<h3 id="풀이">풀이</h3>
<p>가장 긴 가로, 세로 길이를 구하고 곱하여 직사각형의 넓이를 구하고 그 넓이에서 빈 직사각형의 넓이를 빼야 한다.
빈 직사각형의 가로와 세로를 구하는 방법은 다음과 같다.</p>
<pre><code>가장 긴 가로 옆에 위치한 두 세로의 길이 차이가 빈 사각형의 세로가 된다.
같은 원리로 가장 긴 세로 양 옆에 위치한 두 가로의 차이가 빈 사각형의 가로가 된다.</code></pre><h3 id="코드">코드</h3>
<pre><code>import java.io.*;
        import java.util.*;

public class Main {

    static class Line {
        int dir, num;
        public Line(int dir, int num) {
            this.dir = dir;
            this.num = num;
        }

        @Override
        public boolean equals(Object o) {
            Line ob = (Line) o;
            if (this.dir == ob.dir &amp;&amp; this.num == ob.num)
                return true;
            else return false;
        }
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int k = Integer.parseInt(br.readLine());

        ArrayList&lt;Line&gt; list = new ArrayList&lt;&gt;();

        int maxWidth = 0, maxHeight = 0;
        Line wLine=null, hLine=null;

        for (int i = 0; i &lt; 6; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int dir = Integer.parseInt(st.nextToken());
            int num = Integer.parseInt(st.nextToken());

            list.add(new Line(dir, num));

            if (dir == 1 || dir == 2) {
                //가로 중 가장 긴 선분
                if (maxWidth &lt; num) {
                    maxWidth = Math.max(maxWidth, num);
                    wLine = new Line(dir, num);
                }
            }
            else {
                //세로 중 가장 긴 선분
                if (maxHeight &lt; num) {
                    maxHeight = Math.max(maxHeight, num);
                    hLine = new Line(dir, num);
                }
            }
        }

        //가로 중 가장 긴 선분 옆에 위치한 세로의 길이 차를 구함
        int wIdx = list.indexOf(wLine);
        int h = Math.abs(list.get((wIdx+5)%6).num - list.get((wIdx+1)%6).num);

        //세로 중 가장 긴 선분 옆에 위치한 가로의 길이 차를 구함
        int hIdx = list.indexOf(hLine);
        int w = Math.abs(list.get((hIdx+5)%6).num - list.get((hIdx+1)%6).num);

        System.out.println((maxWidth*maxHeight - w*h) * k);
    }
}</code></pre><ul>
<li>static class Line을 선언하여 방향과 길이를 저장한다.</li>
<li><code>public boolean equals(Object o)</code>를 Override한다.<ul>
<li><code>indexOf()</code> 메서드는 <code>equals()</code> 메소드를 사용하여 객체를 찾기 때문이다.</li>
</ul>
</li>
</ul>
<h3 id="다른-풀이">다른 풀이</h3>
<pre><code>import java.io.*;
        import java.util.*;

public class Main {

    static class Line {
        int dir, num;
        public Line(int dir, int num) {
            this.dir = dir;
            this.num = num;
        }
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int k = Integer.parseInt(br.readLine());

        ArrayList&lt;Line&gt; list = new ArrayList&lt;&gt;();

        int maxWidth = 0, maxHeight = 0, maxWidthIdx = 0, maxHeightIdx=0;

        for (int i = 0; i &lt; 6; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int dir = Integer.parseInt(st.nextToken());
            int num = Integer.parseInt(st.nextToken());

            list.add(new Line(dir, num));

            if (dir == 1 || dir == 2) {
                //가로 중 가장 긴 선분
                if (maxWidth &lt; num) {
                    maxWidth = Math.max(maxWidth, num);
                    maxWidthIdx = i;
                }
            }
            else {
                //세로 중 가장 긴 선분
                if (maxHeight &lt; num) {
                    maxHeight = Math.max(maxHeight, num);
                    maxHeightIdx = i;
                }
            }
        }

        //가로 중 가장 긴 선분 옆에 위치한 세로의 길이 차를 구함
        int h = Math.abs(list.get((maxWidthIdx+5)%6).num - list.get((maxWidthIdx+1)%6).num);

        //세로 중 가장 긴 선분 옆에 위치한 가로의 길이 차를 구함
        int w = Math.abs(list.get((maxHeightIdx+5)%6).num - list.get((maxHeightIdx+1)%6).num);

        System.out.println((maxWidth*maxHeight - w*h)*k);
    }
}</code></pre><ul>
<li><code>equals()</code> 메서드를 <code>Override</code> 할 필요 없이 인덱스도 따로 저장해 놓는다,</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트 게시판 프로젝트 - 12 | 게시글 페이징]]></title>
            <link>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-12-%EA%B2%8C%EC%8B%9C%EA%B8%80-%ED%8E%98%EC%9D%B4%EC%A7%95-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-12-%EA%B2%8C%EC%8B%9C%EA%B8%80-%ED%8E%98%EC%9D%B4%EC%A7%95-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Fri, 02 Sep 2022 04:56:37 GMT</pubDate>
            <description><![CDATA[<h2 id="게시글-페이징-기능-구현">게시글 페이징 기능 구현</h2>
<h3 id="게시판-컨트롤러-수정">게시판 컨트롤러 수정</h3>
<p><strong><code>BoardController</code></strong></p>
<pre><code>
    @GetMapping
    public String postList(Model model,
                           @RequestParam(required = false, defaultValue = &quot;0&quot;) Integer page,
                           @RequestParam(required = false, defaultValue = &quot;5&quot;) Integer size) {
        model.addAttribute(&quot;resultMap&quot;, boardService.findAll(page, size));
        return &quot;board/postList&quot;;
    }

    @GetMapping(&quot;/{postId}&quot;)
    public String postView(@PathVariable Long postId, Model model) {
        log.info(&quot;postView&quot;);

        Board post = boardService.findOne(postId).orElseThrow();
        model.addAttribute(&quot;post&quot;, post);

        return &quot;board/post&quot;;
    }
</code></pre><ul>
<li><code>postList()</code> : 게시글 조회 메서드<ul>
<li>게시글 조회 메서드에 <strong>페이징 처리를 위한 파라미터</strong>를 받도록 한다.</li>
<li><code>defaultValue</code>를 지정하여 게시글 조회 화면에서 기본적으로 <strong>첫페이지</strong>가 나타나고, <strong>게시글 5개</strong>가 나타나도록 한다.</li>
</ul>
</li>
</ul>
<h3 id="게시판-서비스-수정">게시판 서비스 수정</h3>
<p><strong><code>BoardService</code></strong></p>
<pre><code>
    /**
     * 게시글 전체 조회
     */
    public HashMap&lt;String, Object&gt; findAll(Integer page, Integer size) {
        HashMap&lt;String, Object&gt; listMap = new HashMap&lt;&gt;();
        Page&lt;Board&gt; list = boardRepository.findAll(PageRequest.of(page, size));

        listMap.put(&quot;list&quot;, list);
        listMap.put(&quot;paging&quot;, list.getPageable());
        listMap.put(&quot;totalCnt&quot;, list.getTotalElements());
        listMap.put(&quot;totalPage&quot;, list.getTotalPages());
        return listMap;
    }

</code></pre><p>Spring Data JPA에서는 페이지 처리를 위한 <code>PageRequest</code> 객체를 지원하므로 간단하게 페이징 처리를 할 수 있다.</p>
<ul>
<li><code>findAll()</code> : 게시글 전체 조회 메서드<ul>
<li><code>HashMap&lt;String, Object&gt;</code>를 반환</li>
</ul>
</li>
</ul>
<hr>
<h3 id="게시글-조회-뷰-템플릿-수정">게시글 조회 뷰 템플릿 수정</h3>
<p><strong><code>postList.html</code></strong></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;div id=&quot;wrapper&quot;&gt;
        &lt;div class=&quot;container&quot;&gt;
            &lt;div class=&quot;col-md-12&quot;&gt;
                &lt;div class=&quot;py-5 text-center&quot;&gt;
                    &lt;h2&gt;게시판&lt;/h2&gt;
                &lt;/div&gt;
                &lt;button class=&quot;btn btn-dark&quot;
                        th:onclick=&quot;|location.href=&#39;@{/}&#39;|&quot; type=&quot;button&quot;&gt;
                    홈 화면
                &lt;/button&gt;

                &lt;button class=&quot;btn btn-primary&quot;
                        th:onclick=&quot;|location.href=&#39;@{/board/register}&#39;|&quot; type=&quot;button&quot;&gt;
                    게시글 작성
                &lt;/button&gt;

                &lt;hr class=&quot;my-4&quot;&gt;

                &lt;table class=&quot;table&quot;&gt;
                    &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th width=&quot;10%&quot;&gt;게시글번호&lt;/th&gt;
                        &lt;th width=&quot;&quot;&gt;제목&lt;/th&gt;
                        &lt;th width=&quot;20%&quot;&gt;작성자&lt;/th&gt;
                        &lt;th width=&quot;20%&quot;&gt;작성일&lt;/th&gt;
                    &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                    &lt;tr th:each=&quot;list,index: ${resultMap.list}&quot; th:with=&quot;paging=${resultMap.paging}&quot;&gt;
                        &lt;td&gt;
                            &lt;span th:text=&quot;${(resultMap.totalCnt - index.index) - (paging.pageNumber * paging.pageSize)}&quot;&gt;&lt;/span&gt;
                        &lt;/td&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                            &lt;a th:href=&quot;@{/board/{postId}(postId=${list.id})}&quot;
                            th:text=&quot;${list.title}&quot;&gt;제목&lt;/a&gt;
                        &lt;/td&gt;
                        &lt;td th:text=&quot;${list.user.loginId}&quot;&gt;작성자&lt;/td&gt;
                        &lt;td th:text=&quot;${list.registerDate}&quot;&gt;작성일&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;

                &lt;div class=&quot;row&quot;&gt;
                    &lt;div class=&quot;col&quot;&gt;
                        &lt;ul class=&quot;pagination&quot;&gt;
                            &lt;li class=&quot;page-item&quot; th:if=&quot;${resultMap.totalPage} &gt; 0&quot; th:each=&quot;index : ${#numbers.sequence(1, resultMap.totalPage)}&quot;
                                th:with=&quot;paging=${resultMap.paging}&quot;&gt;
                                &lt;a class=&quot;page-link&quot;
                                   th:href=&quot;@{/board/(page=${index-1},size=${paging.pageSize})}&quot;&gt;
                                    &lt;span th:text=&quot;${index}&quot;&gt;&lt;/span&gt;
                                &lt;/a&gt;
                            &lt;/li&gt;
                        &lt;/ul&gt;
                    &lt;/div&gt;
                &lt;/div&gt;

                &lt;hr class=&quot;my-4&quot;&gt;

            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

&lt;/body&gt;</code></pre><ul>
<li><code>th:each=&quot;list,index: ${resultMap.list}&quot; th:with=&quot;paging=${resultMap.paging}&quot;</code></li>
<li><code>th:text=&quot;${(resultMap.totalCnt - index.index) - (paging.pageNumber * paging.pageSize)}&quot;</code> : 게시글 번호 지정</li>
<li><code>th:href=&quot;@{/board/{postId}(postId=${list.id})}&quot;</code> : 게시글 제목을 클릭하면 해당 게시글 상세 조회 화면으로 이동한다.</li>
<li><code>th:if=&quot;${resultMap.totalPage} &gt; 0&quot;</code> : page 개수가 1개 이상이라면, 태그를 출력한다.</li>
<li><code>th:href=&quot;@{/board/(page=${index-1},size=${paging.pageSize})}&quot;</code> : 쿼리 파라미터로 현재 페이지 번호와 페이지 크기를 전달한다.</li>
</ul>
<blockquote>
<ul>
<li><code>th:each</code> : 반복하려는 html 엘리먼트에 사용하여 콜렉션(Collection)을 반복
<code>th:each=&quot;콜렉션 변수명, status 변수명:${리스트}&quot;</code></li>
</ul>
</blockquote>
<ul>
<li><code>th:with</code> : 변수형태의 값을 재정의
<code>th:with=&quot;변수명=${...}&quot;</code>
참고: <a href="https://kitty-geno.tistory.com/124">https://kitty-geno.tistory.com/124</a></li>
</ul>
<h3 id="게시글-상세-조회-뷰-템플릿-수정">게시글 상세 조회 뷰 템플릿 수정</h3>
<ul>
<li>게시글 번호가 안 나타나도록 게시글 번호 <code>&lt;div&gt;</code>태그 삭제</li>
</ul>
<hr>
<h3 id="게시글-조회-화면">게시글 조회 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/2ed3cf6e-7e78-4a92-b659-7e8e40bba7f1/image.png" alt=""></p>
<ul>
<li><a href="http://localhost:8080/board">http://localhost:8080/board</a></li>
<li><a href="http://localhost:8080/board/?page=0&amp;size=5">http://localhost:8080/board/?page=0&amp;size=5</a>
<img src="https://velog.velcdn.com/images/serendipity-dev/post/4c7d65f4-89c9-4dd6-8056-2e17d75c0f50/image.png" alt=""></li>
<li><a href="http://localhost:8080/board/?page=1&amp;size=5">http://localhost:8080/board/?page=1&amp;size=5</a></li>
</ul>
<hr>
<h2 id="문제점">문제점</h2>
<p>게시글 조회 화면을 보면 <strong>가장 최근에 작성한 게시글이 1번</strong>이 되어 <strong>마지막 페이지</strong>에 나타난다.
내가 원하는 요구사항은 <strong>가장 최근에 작성한 게시글이 마지막 번호가 되어 가장 첫 페이지에 나타나도록 하는 것</strong>이다.</p>
<h3 id="해결">해결</h3>
<p>구글링을 통해서 해결 방법을 찾았다.</p>
<ul>
<li><code>BoardController</code>에서 <code>Pageable</code> 객체를 생성하고 정렬 순서, 사이즈 등의 정보를 넣은 다음, <code>boardService.findAll(pageable)</code>를 호출해 파라미터로 넘긴다.</li>
<li><code>BoardService</code>에서 <code>PageRequest</code> 객체 대신 <code>Pageable</code> 객체를 사용한다.</li>
<li>참고 : <a href="https://dev-coco.tistory.com/114">Spring Boot JPA 게시판 페이징 처리 구현</a></li>
</ul>
<h3 id="boardcontroller-수정"><code>BoardController</code> 수정</h3>
<pre><code>@GetMapping
    public String postList(Model model,
                           @PageableDefault(sort = &quot;id&quot;, direction = Sort.Direction.DESC, size = 5) Pageable pageable) {
        model.addAttribute(&quot;resultMap&quot;, boardService.findAll(pageable));
        return &quot;board/postList&quot;;
    }</code></pre><p><code>@PageableDefault</code> 어노테이션을 사용해 간단하게 구현한다.</p>
<ul>
<li>id를 기준으로 내림차순으로 정렬한다.</li>
</ul>
<blockquote>
<p><strong>@PageableDefault</strong></p>
</blockquote>
<ul>
<li><code>size</code> : 한 페이지에 담을 모델의 수를 정할 수 있다. 기본 값은 10이다.</li>
<li><code>sort</code> : 정렬의 기준이 되는 속성을 정한다.</li>
<li><code>direction</code> : 오름차순과 내림차순 중 기준을 선택할 수 있다.</li>
<li><code>Pageable pageable</code> : PageableDefault 값을 갖고 있는 변수를 선언한다.</li>
</ul>
<h3 id="boardservice-수정"><code>BoardService</code> 수정</h3>
<pre><code>    /**
     * 게시글 전체 조회
     */
    public HashMap&lt;String, Object&gt; findAll(Pageable page) {
        HashMap&lt;String, Object&gt; listMap = new HashMap&lt;&gt;();
        Page&lt;Board&gt; list = boardRepository.findAll(page);

        listMap.put(&quot;list&quot;, list);
        listMap.put(&quot;paging&quot;, list.getPageable());
        listMap.put(&quot;totalCnt&quot;, list.getTotalElements());
        listMap.put(&quot;totalPage&quot;, list.getTotalPages());
        return listMap;
    }</code></pre><ul>
<li>Spring Data JPA에서 페이징 처리와 정렬은 <code>findAll()</code> 메소드로 한다. </li>
<li><code>findAll()</code> 메소드 파라미터로 <code>pageable</code>을 넣어준다.</li>
</ul>
<blockquote>
<p><code>PageRequest</code>를 사용해도 page를 정렬 할 수 있다.
ex) <code>boardRepository.findAll(PageRequest.of(page, 5, Sort.by(Sort.Direction.DESC, &quot;id&quot;)));</code></p>
</blockquote>
<p><strong><code>postList.html</code> 수정</strong></p>
<pre><code>            &lt;div class=&quot;row&quot;&gt;
                    &lt;div class=&quot;col&quot;&gt;
                        &lt;ul class=&quot;pagination&quot;&gt;
                            &lt;li class=&quot;page-item&quot; th:if=&quot;${resultMap.totalPage} &gt; 0&quot; th:each=&quot;index : ${#numbers.sequence(1, resultMap.totalPage)}&quot;
                                th:with=&quot;paging=${resultMap.paging}&quot;&gt;
                                &lt;a class=&quot;page-link&quot;
                                   th:href=&quot;@{/board/(page=${index-1})}&quot;&gt;
                                    &lt;span th:text=&quot;${index}&quot;&gt;&lt;/span&gt;
                                &lt;/a&gt;
                            &lt;/li&gt;
                        &lt;/ul&gt;
                    &lt;/div&gt;
                &lt;/div&gt;</code></pre><ul>
<li>불필요한 <code>size</code> 파라미터 삭제</li>
</ul>
<h3 id="결과-화면">결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/6fa79495-b3b8-451e-af63-f1bb7fbac490/image.png" alt="">
<a href="http://localhost:8080/board">http://localhost:8080/board</a>
<a href="http://localhost:8080/board/?page=0">http://localhost:8080/board/?page=0</a>
<img src="https://velog.velcdn.com/images/serendipity-dev/post/58dcce1b-02bf-4e84-a507-b413f95a9bb3/image.png" alt="">
<a href="http://localhost:8080/board/?page=1">http://localhost:8080/board/?page=1</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1002 터렛[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-1002-%ED%84%B0%EB%A0%9BJava</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-1002-%ED%84%B0%EB%A0%9BJava</guid>
            <pubDate>Thu, 01 Sep 2022 09:05:05 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/1002">https://www.acmicpc.net/problem/1002</a></p>
<h3 id="풀이">풀이</h3>
<p>두 원의 중심의 거리(d)와 반지름 사이의 관계를 고려해 두 원의 접점을 찾는 문제다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine());
        StringBuilder sb = new StringBuilder();

        while (t-- &gt; 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int x1 = Integer.parseInt(st.nextToken());
            int y1 = Integer.parseInt(st.nextToken());
            int r1 = Integer.parseInt(st.nextToken());
            int x2 = Integer.parseInt(st.nextToken());
            int y2 = Integer.parseInt(st.nextToken());
            int r2 = Integer.parseInt(st.nextToken());

            double d = Math.sqrt(Math.pow(x2- x1, 2) + Math.pow(y2-y1, 2));

            if (d == 0 &amp;&amp; r1==r2)
                sb.append(&quot;-1\n&quot;);
            else if (d &gt; r1 + r2 || d &lt; Math.abs(r1- r2))
                sb.append(&quot;0\n&quot;);
            else if (d == r1+r2 || d == Math.abs(r1-r2))
                sb.append(&quot;1\n&quot;);
            else
                sb.append(&quot;2\n&quot;);
        }

        System.out.println(sb);

    }
}</code></pre><ul>
<li><code>d == 0 &amp;&amp; r1==r2</code> : 두 원이 같으면 접점의 개수는 무한대이므로 -1</li>
<li><code>d &gt; r1 + r2 || d &lt; Math.abs(r1- r2)</code> : 두 원의 접점이 없다.</li>
<li><code>d == r1+r2 || d == Math.abs(r1-r2)</code> : 두 원의 접점이 1개</li>
<li>그 이외의 경우는 두 원의 접점이 2개다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2004 조합 0의 개수[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2004-%EC%A1%B0%ED%95%A90%EC%9D%98%EA%B0%9C%EC%88%98Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2004-%EC%A1%B0%ED%95%A90%EC%9D%98%EA%B0%9C%EC%88%98Java</guid>
            <pubDate>Thu, 01 Sep 2022 04:24:32 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/2004">https://www.acmicpc.net/problem/2004</a></p>
<h3 id="풀이">풀이</h3>
<p>정수론 및 조합론의 이전 문제인 <a href="https://www.acmicpc.net/problem/1676">팩토리얼 0의 개수</a>의 풀이를 응용하면 된다.
팩토리얼 0의 개수를 구할 때는 5의 개수만 구하면 됐지만, 이번 문제는 2의 개수도 따로 구해주어야 한다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static int division(int n, int div) {
        int cnt = 0;

        while (n &gt;= div) {
            cnt += n / div;
            n /= div;
        }
        return cnt;
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int a = Integer.parseInt(st.nextToken());
        int b = Integer.parseInt(st.nextToken());
        int c = a - b;

        int two = 0, five = 0;

        two += division(a, 2);
        five += division(a, 5);

        two -= (division(b, 2) + division(c, 2));
        five -= (division(b, 5) + division(c, 5));

        System.out.println(Math.min(two, five));

    }
}</code></pre><ul>
<li><code>nCr = n! / r!(n-r)!</code></li>
<li>위의 공식을 사용하여 <code>nCr</code>을 소인수분해 했을 때 2의 개수와 5의 개수를 따로 구한다.</li>
<li>그 다음 두 수 중 최솟값을 출력한다.</li>
</ul>
<p>참고: <a href="https://st-lab.tistory.com/165?category=887114">백준 1676 팩토리얼 0의 개수 알고리즘 설명 글</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 9375 패션왕 신해빈[Java]⭐]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-9375-%ED%8C%A8%EC%85%98%EC%99%95-%EC%8B%A0%ED%95%B4%EB%B9%88Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-9375-%ED%8C%A8%EC%85%98%EC%99%95-%EC%8B%A0%ED%95%B4%EB%B9%88Java</guid>
            <pubDate>Thu, 01 Sep 2022 02:21:56 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/9375">https://www.acmicpc.net/problem/9375</a></p>
<h3 id="풀이">풀이</h3>
<p>처음 풀이에는 <code>map</code>과 <strong>조합</strong>을 사용한다.
<code>map</code>에는 <code>map(타입, 해당 타입의 옷 개수)</code>를 저장한다.
2가지 이상의 옷을 고를 경우, <strong>모든 조합의 경우</strong>를 구하여 <strong>각 조합에 속한 수들을 모두 곱한 값</strong>을 <code>cnt</code>에 더한다.
하지만 아래 코드로 풀이한 결과 50%에서 <strong>시간 초과</strong>가 떴다.</p>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    static int cnt;

    public static void combination(int size, int L, int start, ArrayList&lt;Integer&gt; list, int[] comb) {
        if (size == L) {
            int tmp = 1;
            for (int num : comb)
                tmp *= num;
            cnt += tmp;
        }
        else {
            for (int i = start; i &lt; list.size(); i++) {
                comb[L] = list.get(i);
                combination(size, L+1, i+1, list, comb);
            }
        }
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine());

        StringBuilder sb = new StringBuilder();

        while (t-- &gt; 0) {
            int n = Integer.parseInt(br.readLine());
            cnt = 0;
            HashMap&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
            ArrayList&lt;Integer&gt; list = new ArrayList&lt;&gt;();

            for (int i = 0; i &lt; n; i++) {
                StringTokenizer st = new StringTokenizer(br.readLine());
                String name = st.nextToken();
                String type = st.nextToken();
                map.put(type, map.getOrDefault(type, 0) + 1);
            }

            //map에 저장된 옷 개수를 list에 저장
            //옷을 1가지만 입는 경우의 수를 cnt에 더함
            for (String type : map.keySet()) {
                int num = map.get(type);
                list.add(num);
                cnt += num;
            }

            int m = list.size();
            //옷을 2~m가지 입는 경우의 수를 cnt에 더하는 재귀함수
            for (int i = 2; i &lt;= m; i++) {
                int[] comb = new int[i];
                combination(i, 0, 0, list, comb);
            }

            sb.append(cnt+ &quot;\n&quot;);
        }

        System.out.println(sb);
    }
}</code></pre><p>질문 검색을 통해 답을 찾았다.
<code>map</code>을 사용하여 <code>map(타입, 해당 타입의 옷 개수)</code>를 저장하고,
해당 타입의 옷 개수에 1을 더해서 입지 않은 경우까지 고려하고, 각 타입의 옷 개수를 모두 곱해주고 아무것도 입지 않은 경우를 빼주면 된다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine());

        StringBuilder sb = new StringBuilder();

        while (t-- &gt; 0) {
            int n = Integer.parseInt(br.readLine());
            HashMap&lt;String, Integer&gt; map = new HashMap&lt;&gt;();

            for (int i = 0; i &lt; n; i++) {
                StringTokenizer st = new StringTokenizer(br.readLine());
                String name = st.nextToken();
                String type = st.nextToken();
                map.put(type, map.getOrDefault(type, 0) + 1);
            }

            int cnt = 1;
            for (String type : map.keySet()) {
                int num = map.get(type);
                cnt *= (num+1);
            }
            cnt--;

            sb.append(cnt+ &quot;\n&quot;);
        }

        System.out.println(sb);
    }
}</code></pre><p>참고: <a href="https://st-lab.tistory.com/164">https://st-lab.tistory.com/164</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 11051 이항 계수 2[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-11051-%EC%9D%B4%ED%95%AD-%EA%B3%84%EC%88%98-2Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-11051-%EC%9D%B4%ED%95%AD-%EA%B3%84%EC%88%98-2Java</guid>
            <pubDate>Wed, 31 Aug 2022 16:12:18 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/11051">https://www.acmicpc.net/problem/11051</a></p>
<h3 id="풀이">풀이</h3>
<ul>
<li>처음에는 아래의 식을 사용하여 재귀함수를 이용하여 풀었다.
<code>nCr = n-1Cr + n-1Cr-1</code></li>
<li>하지만 입력 범위가 1000까지 가기 때문에 <strong>메모이제이션</strong>을 사용하여 조합 수를 배열에 저장하여 풀었다.</li>
<li>하지만 다음 코드를 제출하면 <code>틀렸습니다</code> 가 뜬다.<pre><code>import java.io.*;
import java.util.*;
</code></pre></li>
</ul>
<p>public class Main {</p>
<pre><code>static int[][] cal = new int[1001][1001];

public static int comb(int a, int b) {
    if (cal[a][b] != 0)
        return cal[a][b];

    if (b == 0 || a == b) {
        return cal[a][b] = 1;
    }

    return cal[a][b] = comb(a-1, b) + comb(a-1, b-1);
}

public static void main(String[] args) throws IOException {

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer st = new StringTokenizer(br.readLine());
    int a = Integer.parseInt(st.nextToken());
    int b = Integer.parseInt(st.nextToken());

    System.out.println(comb(a, b) % 10007);
}</code></pre><p>}</p>
<pre><code>- 위의 코드가 틀린 이유는 입력 범위가 `1 ≤ N ≤ 1000` 이므로 너무 숫자가 커져 **overflow**가 발생하여 잘못된 값을 발생시키기 때문이다.
- 이를 해결하기 위해 모듈러 연산의 성질을 이용한다.
![](https://velog.velcdn.com/images/serendipity-dev/post/75ea496f-48ea-4203-98bc-2797a5070a7d/image.png)


### 코드</code></pre><p>import java.io.<em>;
import java.util.</em>;</p>
<p>public class Main {</p>
<pre><code>static int[][] cal = new int[1001][1001];

public static int comb(int a, int b) {
    if (cal[a][b] != 0)
        return cal[a][b];

    if (b == 0 || a == b) {
        return cal[a][b] = 1;
    }

    return cal[a][b] = (comb(a-1, b)%10007 + comb(a-1, b-1)%10007)%10007;
}

public static void main(String[] args) throws IOException {

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer st = new StringTokenizer(br.readLine());
    int a = Integer.parseInt(st.nextToken());
    int b = Integer.parseInt(st.nextToken());

    System.out.println(comb(a, b));
}</code></pre><p>}</p>
<p>```</p>
<p>참고: <a href="https://st-lab.tistory.com/162">https://st-lab.tistory.com/162</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트 게시판 프로젝트 - 11 | 로그인 인증 체크]]></title>
            <link>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-11-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D-%EC%B2%B4%ED%81%AC</link>
            <guid>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-11-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D-%EC%B2%B4%ED%81%AC</guid>
            <pubDate>Wed, 31 Aug 2022 04:08:21 GMT</pubDate>
            <description><![CDATA[<h3 id="공통-관심-사항">공통 관심 사항</h3>
<p>요구사항을 보면 <strong>로그인 한 사용자만</strong> 게시글 작성, 상세 조회, 수정, 삭제가 가능하다.
여러 컨트롤러에서 로그인 여부를 체크하는 로직을 하나하나 작성하면 되겠지만, 등록, 수정, 삭제, 조회 등등 컨트롤러의 여러 로직에 공통으로 로그인 여부를 확인해야 한다. 더 큰 문제는 향후 로그인과 관련된 로직이 변경될 때 이다. 작성한 모든 로직을 다 수정해야 할 수 있다.
이렇게 <strong>애플리케이션 여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사</strong>(cross-cutting concern)라고 한다. 여기서는 등록, 수정, 삭제, 조회 등등 여러 로직에서 공통으로 인증에 대해서 관심을 가지고 있다.
이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, <strong>웹과 관련된 공통 관심사</strong>는 <strong>서블릿 필터 또는 스프링 인터셉터</strong>를 사용하는 것이 좋다. 웹과 관련된 공통 관심사를 처리할 때는 <strong>HTTP의 헤더나 URL의 정보들이 필요</strong>한데, <strong>서블릿 필터나 스프링 인터셉터</strong>는 <code>HttpServletRequest</code>를 제공한다.</p>
<h3 id="스프링-인터셉터">스프링 인터셉터</h3>
<p><strong>서블릿 필터</strong>가 서블릿이 제공하는 기술이라면, <strong>스프링 인터셉터는 스프링 MVC가 제공하는 기술</strong>이다. 둘다 웹과 관련된 공통 관심 사항을 처리하지만, 적용되는 순서와 범위, 그리고 사용 방법이 다르다. 스프링 인터셉터가 훨씬 더 많은 기능을 제공한다.
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다. <strong>스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용</strong>하는 것이 더 편리하다.</p>
<h3 id="스프링-인터셉터---로그인-인증-체크">스프링 인터셉터 - 로그인 인증 체크</h3>
<p>스프링 인터셉터를 사용하여 로그인 인증 체크 기능을 개발한다.
<strong><code>LoginCheckInterceptor</code></strong></p>
<pre><code>package hello.board.interceptor;

import hello.board.controller.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        log.info(&quot;인증 체크 인터셉터 실행 {}&quot;, requestURI);

        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute(SessionConst.LOGIN_USER) == null) {
            log.info(&quot;미인증 사용자 요청&quot;);

            //로그인으로 redirect
            response.sendRedirect(&quot;/login?redirectURL=&quot; + requestURI);
            return false;
        }

        return true;
    }
}
</code></pre><ul>
<li>스프링의 인터셉터를 사용하려면 <code>HandlerInterceptor</code> 인터페이스를 구현하면 된다.</li>
<li>인증이라는 것은 컨트롤러 호출 전에만 호출되면 되기 때문에 <code>preHandle</code>만 구현하면 된다.</li>
<li><code>response.sendRedirect(&quot;/login?redirectURL=&quot; + requestURI);</code><ul>
<li><strong>미인증 사용자는 로그인 화면으로 리다이렉트</strong> 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면, 원하는 경로를 다시 찾아가야 하는 불편함이 있다. 예를 들어서 게시글 상세 조회 면을 보려고 들어갔다가 로그인 화면으로 이동하면, 로그인 이후에 다시 게시글 상세 조회 화면으로 들어가는 것이 좋다. 이런 부분이 개발자 입장에서는 좀 귀찮을 수 있어도 사용자 입장으로 보면 편리한 기능이다. 이러한 기능을 위해 <strong>현재 요청한 경로인 <code>requestURI</code> 를 <code>/login</code> 에 쿼리 파라미터로 함께 전달</strong>한다. 물론 <strong><code>/login</code> 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발</strong>해야 한다.</li>
<li><strong>Status Code: 302</strong></li>
<li>응답 헤더 <code>Location: http://localhost:8080/login?redirectURL=/board/1</code></li>
</ul>
</li>
<li><code>return false</code> : 인터셉터나 컨트롤러가 더는 호출되지 않는다. 앞서 redirect 를 사용했기 때문에 redirect 가 응답으로 적용되고 요청이 끝난다.</li>
</ul>
<blockquote>
<p>Ctrl + o → 인터페이스에서 오버라이드할 메서드 선택</p>
</blockquote>
<h3 id="웹-설정-정보-추가---인터셉터-등록">웹 설정 정보 추가 - 인터셉터 등록</h3>
<p><strong><code>WebConfig</code></strong></p>
<pre><code>package hello.board;

import hello.board.interceptor.LoginCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor())
                .order(1)
                .addPathPatterns(&quot;/**&quot;)
                .excludePathPatterns(&quot;/&quot;, &quot;/signup&quot;, &quot;/login&quot;, &quot;/logout&quot;, &quot;/board&quot;,
                        &quot;/*.ico&quot;, &quot;/css/**&quot;, &quot;/error&quot;);
    }
}
</code></pre><p><code>WebMvcConfigurer</code> 가 제공하는 <code>addInterceptors()</code> 를 사용해서 인터셉터를 등록할 수 있다.</p>
<ul>
<li><code>registry.addInterceptor(new LoginCheckInterceptor())</code> : 인터셉터를 등록한다.</li>
<li><code>order(1)</code> : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.</li>
<li><code>addPathPatterns()</code> : 인터셉터를 적용할 URL 패턴을 지정한다.</li>
<li><code>excludePathPatterns()</code> : 인터셉터에서 제외할 패턴을 지정한다.</li>
</ul>
<p>인터셉터를 적용하거나 하지 않을 부분은 <code>addPathPatterns</code>와 <code>excludePathPatterns</code>에 작성하면 된다.</p>
<ul>
<li>기본적으로 모든 경로에 해당 인터셉터를 적용하되 ( <code>/**</code> ), 홈( <code>/</code>), 회원가입( <code>/signup</code> ), 로그인( <code>/login</code> ), 로그아웃 (<code>/logout</code> ), 게시글 조회( <code>/board</code> ), 리소스 조회( <code>/css/**</code> ), 오류( <code>/error</code> )와 같은 부분은 로그인 체크 인터셉터를 적용하지 않는다.</li>
</ul>
<blockquote>
<p><strong>스프링의 URL 경로</strong>
스프링이 제공하는 URL 경로는 서블릿 기술이 제공하는 URL 경로와 완전히 다르다. 더욱 자세하고,
세밀하게 설정할 수 있다.
자세한 내용은 다음을 참고하자.
<strong>PathPattern 공식 문서</strong></p>
</blockquote>
<pre><code>? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named &quot;spring&quot;
{spring:[a-z]+} regexp [a-z]+ 와 일치하고, &quot;spring&quot; 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처</code></pre><p>링크: <a href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html">https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html</a></p>
<h3 id="로그인-컨트롤러-수정">로그인 컨트롤러 수정</h3>
<p><strong><code>LoginController</code></strong></p>
<pre><code>    @PostMapping(&quot;/login&quot;)
    public String login(@Valid @ModelAttribute(&quot;loginForm&quot;) UserLoginForm loginForm, BindingResult bindingResult,
                        HttpServletRequest request,
                        @RequestParam(defaultValue = &quot;/&quot;) String redirectURL) {

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors = {}&quot;, bindingResult);
            return &quot;login/loginForm&quot;;
        }

        User loginUser = loginService.login(loginForm.getLoginId(), loginForm.getPassword());

        if (loginUser == null) {
            log.info(&quot;login Fail&quot;);
            bindingResult.reject(&quot;loginFail&quot;, &quot;아이디 또는 비밀번호가 맞지 않습니다.&quot;);
            return &quot;login/loginForm&quot;;
        }

        //로그인 성공
        //세션이 있으면 세션 반환, 없으면 신규 세션을 생성
        HttpSession session = request.getSession();

        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_USER, loginUser);

        return &quot;redirect:&quot; + redirectURL;
    }</code></pre><ul>
<li>로그인에 성공하면 처음 요청한 URL로 이동</li>
<li>로그인 체크 인터셉터에서, 미인증 사용자는 요청 경로를 포함해서 <code>/login</code> 에 <code>redirectURL</code> 요청 파라미터를 추가해서 요청했다. 이 값을 사용해서 로그인 성공시 해당 경로로 redirect 한다.</li>
<li>로그인 리다이렉트 결과
URL : <code>http://localhost:8080/login?redirectURL=/board/1</code></li>
</ul>
<hr>
<h2 id="마무리">마무리</h2>
<p>여기까지 SpringBoot, Gradle, JPA, MySQL, Thymeleaf를 이용하여 간단한 로그인 기능과 게시판 CRUD 기능을 구현하였다.
첫 프로젝트라 디테일한 부분도 부족하고, 강의에서 배운 내용을 활용하고 CRUD 기능을 구현하는데 초점을 맞췄기 때문에 복잡한 기능은 구현하지 않았다.
이 프로젝트에서 추가적으로 기능을 더 구현할 수도 있고, 다른 프로젝트(팀 프로젝트나 클론 코딩)를 시작할 예정이다.</p>
<p><a href="https://github.com/Soomin-Lim/Board_Project">프로젝트 깃헙 링크</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 14889 스타트와 링크[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-14889-%EC%8A%A4%ED%83%80%ED%8A%B8%EC%99%80-%EB%A7%81%ED%81%ACJava</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-14889-%EC%8A%A4%ED%83%80%ED%8A%B8%EC%99%80-%EB%A7%81%ED%81%ACJava</guid>
            <pubDate>Tue, 30 Aug 2022 14:34:30 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/14889">https://www.acmicpc.net/problem/14889</a></p>
<h3 id="풀이">풀이</h3>
<p><strong>DFS</strong>를 사용하여 <strong>팀을 만들 수 있는 모든 경우</strong>를 탐색한 다음 최솟값을 구한다. 이 때 <strong>조합</strong>을 사용하여 <strong>모든 조합의 경우의 수를 탐색</strong>하고, 구분은 <code>boolean[] check</code> 배열을 사용한다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    static int n, answer = Integer.MAX_VALUE;
    static int[][] s;
    static boolean[] check;

    public static void comb(int L, int start) {
        if (L == n/2) {
            int st = 0, link = 0;

            for (int i = 0; i &lt; n; i++){
                for (int j = 0; j &lt; n; j++) {
                    if (check[i] &amp;&amp; check[j])
                        st += s[i][j];
                    else if (!check[i] &amp;&amp; !check[j])
                        link += s[i][j];
                }
            }

            answer = Math.min(answer, Math.abs(st-link));
            return;
        }

        for (int i = start; i &lt; n; i++) {
            check[i] = true;
            comb(L+1, i+1);
            check[i] = false;
        }

    }


    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        s = new int[n][n];
        check = new boolean[n];

        for (int i = 0; i &lt; n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; n; j++)
                s[i][j] = Integer.parseInt(st.nextToken());
        }

        comb(0, 0);
        System.out.println(answer);
    }
}</code></pre><ul>
<li><strong>조합</strong>을 만드는 <strong>재귀함수</strong>를 사용한다.</li>
<li><code>L == n/2</code>이면 팀이 만들어졌으므로 <strong>이중 for문</strong>을 사용해 각 팀의 능력치 합을 구한다.</li>
<li>두 팀의 능력치 차이의 최솟값을 <code>answer</code>에 저장한다.</li>
</ul>
<hr>
<h3 id="다른-풀이">다른 풀이</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    static int n, answer = Integer.MAX_VALUE;
    static int[][] s;
    static boolean[] check;

    public static void comb(int L, int start) {
        if (L == n/2) {
            int st = 0, link = 0;

            for (int i = 0; i &lt; n-1; i++){
                for (int j = i+1; j &lt; n; j++) {
                    if (check[i] &amp;&amp; check[j]) {
                        st += s[i][j];
                        st += s[j][i];
                    }
                    else if (!check[i] &amp;&amp; !check[j]) {
                        link += s[i][j];
                        link += s[j][i];
                    }
                }
            }

            answer = Math.min(answer, Math.abs(st-link));

            if (answer == 0) {
                System.out.println(0);
                System.exit(0);
            }

            return;
        }

        for (int i = start; i &lt; n; i++) {
            check[i] = true;
            comb(L+1, i+1);
            check[i] = false;
        }

    }


    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        s = new int[n][n];
        check = new boolean[n];

        for (int i = 0; i &lt; n; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; n; j++)
                s[i][j] = Integer.parseInt(st.nextToken());
        }

        comb(0, 0);
        System.out.println(answer);
    }
}</code></pre><p><img src="https://velog.velcdn.com/images/serendipity-dev/post/583f5e72-b815-4174-84a4-da9912f51c91/image.png" alt=""></p>
<ul>
<li><code>s[i][j]</code>, <code>s[j][i]</code>를 한 번에 더하기 위해서는 <strong>대각선을 기준으로 위쪽 구역만 탐색</strong>하면 된다.</li>
<li><code>answer</code> 값이 0이면 답이 될 수 있는 가장 최솟값이므로 <code>System.exit(0)</code>을 호출해 프로그램을 종료한다.</li>
</ul>
<p>참고: <a href="https://st-lab.tistory.com/122?category=862595">https://st-lab.tistory.com/122?category=862595</a></p>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/7e1a58d8-859d-4734-a478-a9ae992ae4e5/image.png" alt="">
첫번째 방법이 더 근소하게 메모리와 시간을 적게 쓴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 14888 연산자 끼워넣기[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-14888-%EC%97%B0%EC%82%B0%EC%9E%90-%EB%81%BC%EC%9B%8C%EB%84%A3%EA%B8%B0Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-14888-%EC%97%B0%EC%82%B0%EC%9E%90-%EB%81%BC%EC%9B%8C%EB%84%A3%EA%B8%B0Java</guid>
            <pubDate>Tue, 30 Aug 2022 14:05:13 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/14888">https://www.acmicpc.net/problem/14888</a>
삼성 SW 역량 테스트 기출 문제</p>
<h3 id="풀이">풀이</h3>
<p><strong>DFS</strong>를 사용하여 모든 경우의 수를 탐색해 최솟값과 최댓값을 구한다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.util.*;

public class Main {

    static int n, min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
    static int[] arr;
    static int[] operator = new int[4];

    public static void operation(int idx, int value) {
        if (idx == n) {
            min = Math.min(min, value);
            max = Math.max(max, value);
            return;
        }

        int num = arr[idx];
        for (int i = 0; i &lt; 4; i++) {
            if (operator[i] != 0) {
                operator[i]--;
                switch(i) {
                    case 0: operation(idx+1, value+num); break;
                    case 1: operation(idx+1, value-num); break;
                    case 2: operation(idx+1, value*num); break;
                    case 3: operation(idx+1, value/num); break;
                }
                operator[i]++;
            }
        }

    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        n = Integer.parseInt(br.readLine());
        arr = new int[n];

        StringTokenizer st = new StringTokenizer(br.readLine());

        for (int i = 0; i &lt; n; i++) {
            arr[i] = Integer.parseInt(st.nextToken());
        }

        st = new StringTokenizer(br.readLine());
        for (int i = 0; i &lt; 4; i++) {
            operator[i] = Integer.parseInt(st.nextToken());
        }

        operation(1, arr[0]);

        System.out.print(max + &quot;\n&quot; + min);
    }
}</code></pre><ul>
<li><code>operator</code> 배열에 각 연산자의 개수를 저장한다.</li>
<li>재귀함수 내 for문에서 연산자 개수가 0 이상인 경우(<code>operator[i] != 0</code>) 해당 연산자 개수를 1 감소하고 재귀호출을 한다.</li>
<li>모든 피연산자를 다 쓴 경우(<code>idx == n</code>) 값을 비교하여 <code>min</code>, <code>max</code> 값을 업데이트한다. </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트 게시판 프로젝트 - 10 | 게시글 수정, 삭제 기능 개발]]></title>
            <link>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-10-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EC%88%98%EC%A0%95-%EC%82%AD%EC%A0%9C-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-10-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EC%88%98%EC%A0%95-%EC%82%AD%EC%A0%9C-%EA%B8%B0%EB%8A%A5-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Tue, 30 Aug 2022 11:08:41 GMT</pubDate>
            <description><![CDATA[<h2 id="게시글-수정-및-삭제-기능-개발">게시글 수정 및 삭제 기능 개발</h2>
<h3 id="게시판-컨트롤러-수정-및-추가">게시판 컨트롤러 수정 및 추가</h3>
<p><strong><code>BoardController</code></strong></p>
<pre><code>package hello.board.controller.board;

import hello.board.controller.SessionConst;
import hello.board.entity.Board;
import hello.board.entity.User;
import hello.board.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;

@Controller
@Slf4j
@RequiredArgsConstructor
@RequestMapping(&quot;/board&quot;)
public class BoardController {

    private final BoardService boardService;

    @GetMapping
    public String postList(Model model) {
        model.addAttribute(&quot;list&quot;, boardService.findAll());
        return &quot;board/postList&quot;;
    }

    @GetMapping(&quot;/{postId}&quot;)
    public String postView(@PathVariable Long postId, Model model) {
        log.info(&quot;postView&quot;);

        Board post = boardService.findOne(postId).orElseThrow();
        model.addAttribute(&quot;post&quot;, post);

        return &quot;board/post&quot;;
    }

    @GetMapping(&quot;/register&quot;)
    public String registerForm(@ModelAttribute PostForm postForm) {
        return &quot;board/registerForm&quot;;
    }

    @PostMapping(&quot;/register&quot;)
    public String register(@Valid @ModelAttribute PostForm postForm, BindingResult bindingResult, @SessionAttribute(name = SessionConst.LOGIN_USER, required = false) User loginUser, RedirectAttributes redirectAttributes) {

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors = {}&quot;, bindingResult);
            return &quot;board/registerForm&quot;;
        }

        Long registerId = boardService.register(postForm.getTitle(), postForm.getContent(), loginUser.getId());
        redirectAttributes.addAttribute(&quot;postId&quot;, registerId);

        return &quot;redirect:/board/{postId}&quot;;
    }

    @GetMapping(&quot;/{postId}/edit&quot;)
    public String editForm(@PathVariable Long postId, Model model) {

        Board post = boardService.findOne(postId).orElseThrow();

        PostForm postForm = new PostForm();
        postForm.setTitle(post.getTitle());
        postForm.setContent(post.getContent());

        model.addAttribute(&quot;postForm&quot;, postForm);
        model.addAttribute(&quot;postId&quot;, postId);

        return &quot;board/editForm&quot;;
    }

    @PostMapping(&quot;/{postId}/edit&quot;)
    public String edit(@PathVariable Long postId, @Valid @ModelAttribute PostForm postForm, BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors = {}&quot;, bindingResult);
            return &quot;board/editForm&quot;;
        }

        boardService.updateBoard(postId, postForm.getTitle(), postForm.getContent());

        return &quot;redirect:/board/{postId}&quot;;
    }

    @PostMapping(&quot;/{postId}/delete&quot;)
    public String delete(@PathVariable Long postId) {
        boardService.deleteById(postId);
        return &quot;redirect:/board&quot;;
    }
}
</code></pre><ul>
<li><code>register()</code> <strong>게시글 등록 메서드 수정</strong><ul>
<li><strong><code>RedirectAttributes</code></strong>를 사용하여 <strong>새로 만들어진 게시글의 상세 조회 화면으로 redirect</strong>하도록 한다.</li>
</ul>
</li>
<li><code>editForm()</code> : 게시글 수정 폼<ul>
<li><code>@PathVariable</code>로 URL에서 게시글 id(<code>postId</code>)를 가져온다.</li>
<li><code>boardService.findOne(postId).orElseThrow()</code>로 해당 게시글을 가져온다.</li>
<li>새로운 <code>PostForm</code> 객체를 생성 후, 제목과 내용을 설정하고, <code>model</code>에 <code>postForm</code>, <code>postId</code>를 넣고 뷰 화면으로 넘긴다.</li>
</ul>
</li>
<li><code>edit()</code> : 게시글 수정<ul>
<li>Bean Validation 기술을 사용하여 <code>postForm</code> 객체를 검증한다. 에러가 있으면 <code>board/editForm</code> 뷰 화면을 다시 보여준다.</li>
<li>검증에 통과하면 <code>boardService.updateBoard()</code>를 호출하여 게시글을 수정하고, 게시글 상세 조회 화면으로 <strong>redirect</strong>한다.</li>
</ul>
</li>
<li><code>delete()</code> : 게시글 삭제<ul>
<li><code>@PathVariable</code>로 URL에서 게시글 id(<code>postId</code>)를 가져온다.</li>
<li><code>boardService.deleteById(postId);</code>로 해당 게시글을 삭제한다.</li>
<li>게시글 조회 화면으로 <strong>redirect</strong> 한다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>RedirectAttributes</strong>
<code>RedirectAttributes</code>를 사용하면 <strong>URL 인코딩</strong>도 해주고, <strong>pathVarible, 쿼리 파라미터</strong>까지 처리해준다.</p>
</blockquote>
<ul>
<li>예시) <code>redirectAttributes.addAttribute(&quot;postId&quot;, registerId);</code>
<code>redirectAttributes.addAttribute(&quot;status&quot;, true);</code></li>
<li><code>redirect:/board/{postId}</code><ul>
<li>pathVariable 바인딩: {postId}</li>
</ul>
</li>
<li>나머지는 쿼리 파라미터로 처리: <code>?status=true</code>
=&gt; 자동으로 pathVariable 바인딩을하며, 바인딩이 안되는 것은 쿼리 파라미터로 넘어간다.</li>
</ul>
<h3 id="수정용-폼-뷰-템플릿">수정용 폼 뷰 템플릿</h3>
<p><code>board/editForm.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
    &lt;style&gt;
    .container{
        max-width: 700px;
    }
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;게시글 수정&lt;/h2&gt;
    &lt;/div&gt;
    &lt;form th:action th:object=&quot;${postForm}&quot; method=&quot;post&quot;&gt;
        &lt;div&gt;
            &lt;label for=&quot;title&quot;&gt;제목&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;title&quot; th:field=&quot;*{title}&quot; class=&quot;form-control&quot;
                   th:errorclass=&quot;field-error&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{title}&quot; /&gt;
        &lt;/div&gt;
        &lt;div class=&quot;mb-3&quot;&gt;
            &lt;label for=&quot;content&quot;&gt;내용&lt;/label&gt;
            &lt;textarea class=&quot;form-control&quot; rows=&quot;5&quot;
                      id=&quot;content&quot; name=&quot;content&quot;
                      th:value=&quot;*{content}&quot; th:text=&quot;*{content}&quot;&gt;&lt;/textarea&gt;
        &lt;/div&gt;
        &lt;hr class=&quot;my-4&quot;&gt;

        &lt;button class=&quot;btn btn-primary&quot; type=&quot;submit&quot;&gt;
            수정 완료
        &lt;/button&gt;

        &lt;button class=&quot;btn btn-secondary&quot;
                th:onclick=&quot;|location.href=&#39;@{/board/{postId}(postId=${postId})}&#39;|&quot;
                type=&quot;button&quot;&gt;취소
        &lt;/button&gt;

    &lt;/form&gt;

&lt;/div&gt;
&lt;/body&gt;</code></pre><h3 id="수정-화면">수정 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/cfba6f90-a08d-41f0-b2e6-c1cbdf301b3b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/ad00f987-866f-4e76-ad9f-be395281a968/image.png" alt="">
DB에도 반영된 것을 확인할 수 있다.</p>
<h3 id="삭제-후-조회">삭제 후 조회</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/7334cf1d-59f5-4beb-a5c2-cc29c13be696/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/d1fe4ab4-3200-41d8-9b76-2c6c77a11d48/image.png" alt=""></p>
<p>DB에도 반영된 것을 확인할 수 있다.</p>
<hr>
<h3 id="게시글-상세-조회-뷰-템플릿-변경">게시글 상세 조회 뷰 템플릿 변경</h3>
<p><code>board/post.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
    &lt;style&gt;
    .container{
        max-width: 700px;
    }
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;게시글&lt;/h2&gt;
    &lt;/div&gt;

    &lt;button class=&quot;btn btn-dark pull-right&quot;
            onclick=&quot;location.href=&#39;board/postList.html&#39;&quot;
            th:onclick=&quot;|location.href=&#39;@{/board}&#39;|&quot;
            type=&quot;button&quot;&gt;목록으로
    &lt;/button&gt;

    &lt;div&gt;
        &lt;label for=&quot;postId&quot;&gt;게시판번호&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;postId&quot; name=&quot;postId&quot; class=&quot;form-control&quot;
               value=&quot;1&quot; th:value=&quot;${post.id}&quot; readonly&gt;
    &lt;/div&gt;

    &lt;div&gt;
        &lt;label for=&quot;postTitle&quot;&gt;제목&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;postTitle&quot; name=&quot;postTitle&quot; class=&quot;form-control&quot;
               value=&quot;제목&quot; th:value=&quot;${post.title}&quot; readonly&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;label for=&quot;postContent&quot;&gt;내용&lt;/label&gt;
        &lt;textarea class=&quot;form-control&quot; rows=&quot;5&quot;
                  id=&quot;postContent&quot; name=&quot;postContent&quot;
                  th:value=&quot;${post.content}&quot; th:text=&quot;${post.content}&quot; readonly&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;label for=&quot;writer&quot;&gt;작성자&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;writer&quot; name=&quot;writer&quot; class=&quot;form-control&quot;
               value=&quot;작성자&quot; th:value=&quot;${post.user.loginId}&quot; readonly&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;label for=&quot;registerDate&quot;&gt;작성일&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;registerDate&quot; name=&quot;registerDate&quot; class=&quot;form-control&quot;
               value=&quot;작성자&quot; th:value=&quot;${post.registerDate}&quot; readonly&gt;
    &lt;/div&gt;
    &lt;hr class=&quot;my-4&quot;&gt;

    &lt;form th:action=&quot;@{/board/{postId}/delete(postId=${post.id})}&quot; method=&quot;post&quot;&gt;

        &lt;button class=&quot;btn btn-primary&quot;
                th:onclick=&quot;|location.href=&#39;@{/board/{postId}/edit(postId=${post.id})}&#39;|&quot;
                type=&quot;button&quot;&gt;수정&lt;/button&gt;

        &lt;button class=&quot;btn btn-danger&quot; type=&quot;submit&quot;&gt;삭제&lt;/button&gt;
    &lt;/form&gt;

&lt;/div&gt; &lt;!-- /container --&gt;

&lt;/body&gt;</code></pre><ul>
<li>게시글을 삭제 기능을 구현하기 위해 form 태그를 새로 생성</li>
<li><code>&lt;form th:action=&quot;@{/board/{postId}/delete(postId=${post.id})}&quot; method=&quot;post&quot;&gt;</code></li>
<li><code>&lt;button class=&quot;btn btn-danger&quot; type=&quot;submit&quot;&gt;삭제&lt;/button&gt;</code></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2580 스도쿠[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2580-%EC%8A%A4%EB%8F%84%EC%BF%A0Java</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-2580-%EC%8A%A4%EB%8F%84%EC%BF%A0Java</guid>
            <pubDate>Mon, 29 Aug 2022 15:18:55 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/2580">https://www.acmicpc.net/problem/2580</a></p>
<h3 id="풀이">풀이</h3>
<p>처음에는 2차원 배열과 이중 for문을 통해 배열을 탐색해서 <code>arr[i][j] == 0</code>이면 1~9까지의 숫자를 넣고 check한 다음, 통과하면 재귀함수<code>sudoku(i,j+1)</code>를 호출해서 계속 탐색해서 마지막까지 오면 배열을 출력하도록 했다.
하지만 재귀함수를 호출하여 이중 for문을 쓰면 원하는 위치에서 탐색을 시작해도 배열의 탐색이 이루어지지 않는다. 매개변수 없이 재귀함수(<code>sudoku()</code>)를 정해도 이전 재귀함수로 돌아가는 시점(<code>return</code>)도 정하기 매우 어렵다.</p>
<p>그래서 <strong>0인 위치를 <code>ArrayList</code></strong>에 넣고, 그 위치에 1~9까지의 숫자를 넣고 check한 다음, 통과하면 재귀함수 <code>sudoku(idx + 1)</code>을 호출한다. 여기서 <strong><code>idx</code>는 <code>ArrayList</code>의 인덱스</strong>다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {

    static int[][] arr = new int[9][9];
    static ArrayList&lt;int[]&gt; points;

    static public boolean check(int row, int col) {
        int n = arr[row][col];
        arr[row][col] = -1; //체크할 때 현재 위치는 제외하기 위해 -1로 값을 변경한다

        //같은 열이나 같은 행에서 같은 숫자가 있는지 체크
        for (int idx = 0; idx &lt; 9; idx++) {
            if (arr[row][idx] == n || arr[idx][col] == n) {
                return false;
            }
        }

        //3x3 정사각형 안에 같은 숫자가 있는지 체크
        int tmpRow = (row / 3) * 3;
        int tmpCol = (col / 3) * 3;

        for (int i = tmpRow; i &lt; tmpRow+3; i++) {
            for (int j = tmpCol; j &lt; tmpCol+3; j++) {
                if (arr[i][j] == n) {
                    return false;
                }
            }
        }

        arr[row][col] = n;
        return true;
    }

    static public void sudoku(int idx) {

        if (idx == points.size()) {

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i &lt; 9; i++) {
                for (int j = 0; j &lt; 9; j++)
                    sb.append(arr[i][j] + &quot; &quot;);
                sb.append(&quot;\n&quot;);
            }
            System.out.println(sb);

            System.exit(0);
        }

        int[] tmp = points.get(idx);

        for (int k = 1; k &lt;= 9; k++) {

            arr[tmp[0]][tmp[1]] = k;

            if (check(tmp[0], tmp[1])) {
                sudoku(idx + 1);
            }

            arr[tmp[0]][tmp[1]] = 0;

        }

    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        points = new ArrayList&lt;&gt;();

        for (int i = 0; i &lt; 9; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; 9; j++) {
                int n = Integer.parseInt(st.nextToken());
                arr[i][j] = n;
                if (n == 0)
                    points.add(new int[]{i, j});
            }
        }

        sudoku(0);

    }
}</code></pre><ul>
<li><code>System.exit(0)</code> : 프로그램을 바로 종료한다.</li>
<li><code>arr[tmp[0]][tmp[1]] = k;</code> : for문 안에서 1~9까지 숫자를 집어넣는다.</li>
<li><strong>중요:</strong> <code>arr[tmp[0]][tmp[1]] = 0;</code> : 다시 0을 집어넣어야 한다. 그렇지 않으면, 이전 함수로 돌아갈 때 9가 남겨져 있는 채로 return 될 수 있다.</li>
</ul>
<hr>
<h3 id="수정한-버전">수정한 버전</h3>
<pre><code>import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {

    static int[][] arr = new int[9][9];
    static ArrayList&lt;int[]&gt; points;

    static public boolean check(int row, int col, int value) {

        //같은 열이나 같은 행에서 같은 숫자가 있는지 체크
        for (int idx = 0; idx &lt; 9; idx++) {
            if (arr[row][idx] == value || arr[idx][col] == value) {
                return false;
            }
        }

        //3x3 정사각형 안에 같은 숫자가 있는지 체크
        int tmpRow = (row / 3) * 3;
        int tmpCol = (col / 3) * 3;

        for (int i = tmpRow; i &lt; tmpRow+3; i++) {
            for (int j = tmpCol; j &lt; tmpCol+3; j++) {
                if (arr[i][j] == value) {
                    return false;
                }
            }
        }

        return true;
    }

    static public void sudoku(int idx) {

        if (idx == points.size()) {

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i &lt; 9; i++) {
                for (int j = 0; j &lt; 9; j++)
                    sb.append(arr[i][j] + &quot; &quot;);
                sb.append(&quot;\n&quot;);
            }
            System.out.println(sb);

            System.exit(0);
        }

        int[] tmp = points.get(idx);

        for (int k = 1; k &lt;= 9; k++) {

            if (check(tmp[0], tmp[1], k)) {
                arr[tmp[0]][tmp[1]] = k;
                sudoku(idx + 1);
            }
            arr[tmp[0]][tmp[1]] = 0;
        }

    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        points = new ArrayList&lt;&gt;();

        for (int i = 0; i &lt; 9; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; 9; j++) {
                int n = Integer.parseInt(st.nextToken());
                arr[i][j] = n;
                if (n == 0)
                    points.add(new int[]{i, j});
            }
        }

        sudoku(0);

    }
}</code></pre><ul>
<li><code>check(row, col, value)</code></li>
<li>check 메서드를 통과한 후, <code>arr[tmp[0]][tmp[1]] = k;</code></li>
<li>이렇게 하면 check 메서드에서 <code>arr[row][col] = -1;</code>을 하지 않아도 된다.</li>
</ul>
<hr>
<h3 id="다른-풀이">다른 풀이</h3>
<pre><code>import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {

    static int[][] arr = new int[9][9];

    static public boolean check(int row, int col, int value) {

        //같은 열이나 같은 행에서 같은 숫자가 있는지 체크
        for (int idx = 0; idx &lt; 9; idx++) {
            if (arr[row][idx] == value || arr[idx][col] == value) {
                return false;
            }
        }

        //3x3 정사각형 안에 같은 숫자가 있는지 체크
        int tmpRow = (row / 3) * 3;
        int tmpCol = (col / 3) * 3;

        for (int i = tmpRow; i &lt; tmpRow+3; i++) {
            for (int j = tmpCol; j &lt; tmpCol+3; j++) {
                if (arr[i][j] == value) {
                    return false;
                }
            }
        }

        return true;
    }

    static public void sudoku(int row, int col) {

        if (col == 9) {
            sudoku(row + 1, 0);
            return;
        }

        if (row == 9) {

            StringBuilder sb = new StringBuilder();
            for (int i = 0; i &lt; 9; i++) {
                for (int j = 0; j &lt; 9; j++)
                    sb.append(arr[i][j] + &quot; &quot;);
                sb.append(&quot;\n&quot;);
            }
            System.out.println(sb);

            System.exit(0);
        }

        if (arr[row][col] == 0) {
            for (int k = 1; k &lt;= 9; k++) {
                if (check(row, col, k)) {
                    arr[row][col] = k;
                    sudoku(row, col + 1);
                }
            }
            arr[row][col] = 0;
            return;
        }

        sudoku(row, col+1);

    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        for (int i = 0; i &lt; 9; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 0; j &lt; 9; j++) {
                arr[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        sudoku(0, 0);
    }
}</code></pre><ul>
<li>재귀함수: <code>sudoku(row, col)</code> 위치를 매개변수로 전달</li>
<li><code>sudoku(0, 0)</code>부터 시작해서 <code>sudoku(row, col+1)</code>을 재귀호출한다.</li>
<li><code>col == 9</code>이면 <code>sudoku(row+1, 0)</code><ul>
<li><code>return</code>을 꼭 해야 한다. 아니면 런타임 에러(ArrayIndexOutOfBounds)가 난다.</li>
</ul>
</li>
</ul>
<p>참고: <a href="https://st-lab.tistory.com/119">https://st-lab.tistory.com/119</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트 게시판 프로젝트 - 9 | 게시글 등록, 조회, 상세 조회 기능 개발]]></title>
            <link>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-9-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C-4</link>
            <guid>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-9-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C-4</guid>
            <pubDate>Mon, 29 Aug 2022 07:08:14 GMT</pubDate>
            <description><![CDATA[<h3 id="홈-화면-수정">홈 화면 수정</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/f51089a9-9274-4c76-8373-bba0e5e8a89b/image.png" alt="">
<img src="https://velog.velcdn.com/images/serendipity-dev/post/cb55be07-20d7-49b3-9765-147bfc11cc56/image.png" alt=""></p>
<hr>
<h2 id="게시글-등록-및-조회-기능-개발">게시글 등록 및 조회 기능 개발</h2>
<h3 id="board-엔티티-수정">Board 엔티티 수정</h3>
<p><code>registerDate</code> 타입을 <code>LocalDateTime</code>으로 하면 게시글 조회 화면에서
<code>년-월-일T시:분:초</code> 라고 나타나기 때문에 <code>DateTimeFormatter</code>를 사용해서 <code>registerDate</code>를 String 타입으로 저장한다.</p>
<pre><code>package hello.board.entity;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = &quot;board_id&quot;)
    private Long id;

    private String title;
    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;user_id&quot;)
    private User user;

    private String registerDate;

    @Builder
    public Board(String title, String content, User user, LocalDateTime registerDate) {
        this.title = title;
        this.content = content;
        this.user = user;
        this.registerDate = DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;).format(registerDate);
    }

    //==생성 메서드==//
    public static Board createBoard(String title, String content, User user) {
        return Board.builder()
                .title(title).content(content).user(user)
                .registerDate(LocalDateTime.now())
                .build();
    }

    //==비즈니스 메서드==//
    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }
}
</code></pre><ul>
<li>생성자 메서드에 <code>LocalDateTime</code>을 보내면, <code>DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;).format(registerDate);</code>를 통해 포맷한 문자열을 <code>registerDate</code>에 저장한다.</li>
</ul>
<p><strong>ddl도 수정</strong>한다.</p>
<pre><code>create table board (
       board_id bigint not null auto_increment,
        title varchar(255),
        content text,
        user_id bigint,
        register_date varchar(20),
        primary key (board_id),
        constraint fk_user_board
        foreign key (user_id)
        references users (user_id) on update cascade
    ) engine=InnoDB default charset=utf8mb4;</code></pre><ul>
<li><code>register_date varchar(20)</code></li>
</ul>
<hr>
<h3 id="게시글-등록용-폼-객체">게시글 등록용 폼 객체</h3>
<h4 id="postform">PostForm</h4>
<pre><code>package hello.board.controller.board;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Getter @Setter
public class PostForm {

    @NotBlank(message = &quot;제목을 입력해주세요.&quot;)
    private String title;

    private String content;
}
</code></pre><h3 id="게시판-컨트롤러">게시판 컨트롤러</h3>
<h4 id="boardcontroller">BoardController</h4>
<pre><code>package hello.board.controller.board;

import hello.board.controller.SessionConst;
import hello.board.entity.Board;
import hello.board.entity.User;
import hello.board.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@Controller
@Slf4j
@RequiredArgsConstructor
@RequestMapping(&quot;/board&quot;)
public class BoardController {

    private final BoardService boardService;

    @GetMapping
    public String postList(Model model) {
        model.addAttribute(&quot;list&quot;, boardService.findAll());
        return &quot;board/postList&quot;;
    }

    @GetMapping(&quot;/{postId}&quot;)
    public String postView(@PathVariable Long postId, Model model) {
        log.info(&quot;postView&quot;);

        Board post = boardService.findOne(postId).orElseThrow();
        model.addAttribute(&quot;post&quot;, post);

        return &quot;board/post&quot;;
    }

    @GetMapping(&quot;/register&quot;)
    public String registerForm(@ModelAttribute PostForm postForm) {
        return &quot;board/registerForm&quot;;
    }

    @PostMapping(&quot;/register&quot;)
    public String register(@Valid @ModelAttribute PostForm postForm, BindingResult bindingResult, @SessionAttribute(name = SessionConst.LOGIN_USER, required = false) User loginUser) {

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors = {}&quot;, bindingResult);
            return &quot;board/registerForm&quot;;
        }

        boardService.register(postForm.getTitle(), postForm.getContent(), loginUser.getId());

        return &quot;redirect:/board&quot;;
    }
}
</code></pre><ul>
<li><code>postList()</code>: 게시글 전체 조회<ul>
<li><code>boardService.findAll()</code>의 반환 값인 <code>List&lt;Board&gt;</code>를 <code>model</code>에 담아서 뷰 템플릿으로 전달한다.</li>
</ul>
</li>
<li><code>postView()</code>: 게시글 상세 조회<ul>
<li>URL로 받은 <code>postId</code>로 게시글을 찾아 <code>model</code>에 담아서 뷰 템플릿에 전달한다.</li>
</ul>
</li>
<li><code>registerForm()</code>: 게시글 등록 폼 화면</li>
<li><code>register()</code>: 게시글 등록<ul>
<li><code>@Valid</code> 어노테이션으로 <code>PostForm</code> 객체를 검증한다.</li>
<li>검증에 실패하면, 에러 메시지와 함께 게시글 등록 폼 화면을 다시 보여준다.</li>
<li><code>@SessionAttribute</code>로 현재 로그인한 회원(<code>loginUser</code>)의 정보를 얻어 id 값을 찾고, <code>boardService.register()</code>를 호출한다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="문제점">문제점</h2>
<p>게시글의 내용(<code>content</code>)은 아무것도 입력하지 않아도 된다. 하지만, 내용에 아무것도 입력하지 않으면 
<code>BoardController</code>의 <code>register()</code> 메서드의 
<code>boardService.register(postForm.getTitle(), postForm.getContent(), loginUser.getId());</code> 에서 <strong><code>NPE</code></strong>가 발생한다.</p>
<h3 id="해결">해결</h3>
<p><code>PostForm</code>에 <code>@NotNull</code> 어노테이션을 추가한다.</p>
<pre><code>package hello.board.controller.board;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Getter @Setter
public class PostForm {

    @NotBlank(message = &quot;제목을 입력해주세요.&quot;)
    private String title;

    @NotNull
    private String content;
}
</code></pre><hr>
<h3 id="게시글-조회-뷰-템플릿">게시글 조회 뷰 템플릿</h3>
<p><code>board/postList.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;div id=&quot;wrapper&quot;&gt;
        &lt;div class=&quot;container&quot;&gt;
            &lt;div class=&quot;col-md-12&quot;&gt;
                &lt;div class=&quot;py-5 text-center&quot;&gt;
                    &lt;h2&gt;게시판&lt;/h2&gt;
                &lt;/div&gt;
                &lt;button class=&quot;btn btn-dark&quot;
                        th:onclick=&quot;|location.href=&#39;@{/}&#39;|&quot; type=&quot;button&quot;&gt;
                    홈 화면
                &lt;/button&gt;

                &lt;button class=&quot;btn btn-primary&quot;
                        th:onclick=&quot;|location.href=&#39;@{/board/register}&#39;|&quot; type=&quot;button&quot;&gt;
                    게시글 작성
                &lt;/button&gt;

                &lt;hr class=&quot;my-4&quot;&gt;

                &lt;table class=&quot;table&quot;&gt;
                    &lt;thead&gt;
                    &lt;tr&gt;
                        &lt;th width=&quot;10%&quot;&gt;게시글번호&lt;/th&gt;
                        &lt;th width=&quot;&quot;&gt;제목&lt;/th&gt;
                        &lt;th width=&quot;20%&quot;&gt;작성자&lt;/th&gt;
                        &lt;th width=&quot;20%&quot;&gt;작성일&lt;/th&gt;
                    &lt;/tr&gt;
                    &lt;/thead&gt;
                    &lt;tbody&gt;
                    &lt;tr th:each=&quot;post : ${list}&quot;&gt;
                        &lt;td&gt;
                            &lt;a th:href=&quot;@{/board/{postId}(postId=${post.id})}&quot;
                            th:text=&quot;${post.id}&quot;&gt;게시글 번호&lt;/a&gt;
                        &lt;/td&gt;
                        &lt;td&gt;
                            &lt;a th:href=&quot;@{/board/{postId}(postId=${post.id})}&quot;
                            th:text=&quot;${post.title}&quot;&gt;제목&lt;/a&gt;
                        &lt;/td&gt;
                        &lt;td th:text=&quot;${post.user.loginId}&quot;&gt;작성자&lt;/td&gt;
                        &lt;td th:text=&quot;${post.registerDate}&quot;&gt;작성일&lt;/td&gt;
                    &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                &lt;hr class=&quot;my-4&quot;&gt;

            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

&lt;/body&gt;</code></pre><h3 id="게시글-등록-폼-뷰-템플릿">게시글 등록 폼 뷰 템플릿</h3>
<p><code>board/registerForm.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
    &lt;style&gt;
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;게시글 등록&lt;/h2&gt;
    &lt;/div&gt;
    &lt;form th:action th:object=&quot;${postForm}&quot; method=&quot;post&quot;&gt;
        &lt;div&gt;
            &lt;label for=&quot;title&quot;&gt;제목&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;title&quot; th:field=&quot;*{title}&quot; class=&quot;form-control&quot;
                   th:errorclass=&quot;field-error&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{title}&quot; /&gt;
        &lt;/div&gt;
        &lt;div class=&quot;mb-3&quot;&gt;
            &lt;label for=&quot;content&quot;&gt;내용&lt;/label&gt;
            &lt;textarea class=&quot;form-control&quot; rows=&quot;5&quot;
                      id=&quot;content&quot; name=&quot;content&quot; th:value=&quot;*{content}&quot;&gt;&lt;/textarea&gt;
        &lt;/div&gt;
        &lt;hr class=&quot;my-4&quot;&gt;

        &lt;button class=&quot;btn btn-primary btn-lg&quot; type=&quot;submit&quot;&gt;
            작성
        &lt;/button&gt;
        &lt;button class=&quot;btn btn-secondary btn-lg&quot;
                th:onclick=&quot;|location.href=&#39;@{/board}&#39;|&quot;
                type=&quot;button&quot;&gt;
            취소
        &lt;/button&gt;

    &lt;/form&gt;

&lt;/div&gt;
&lt;/body&gt;</code></pre><ul>
<li>내용(<code>content</code>)을 적는 칸에는 <code>input</code> 태그 대신 <code>textarea</code> 태그를 사용하여, 여러 줄의 텍스트를 입력할 수 있는 입력 칸을 만든다.</li>
</ul>
<h3 id="게시글-상세-조회-뷰-템플릿">게시글 상세 조회 뷰 템플릿</h3>
<p><code>board/post.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
    &lt;style&gt;
    .container{
        max-width: 700px;
    }
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;게시글&lt;/h2&gt;
    &lt;/div&gt;

    &lt;button class=&quot;btn btn-dark pull-right&quot;
            onclick=&quot;location.href=&#39;board/postList.html&#39;&quot;
            th:onclick=&quot;|location.href=&#39;@{/board}&#39;|&quot;
            type=&quot;button&quot;&gt;목록으로
    &lt;/button&gt;

    &lt;div&gt;
        &lt;label for=&quot;postId&quot;&gt;게시판번호&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;postId&quot; name=&quot;postId&quot; class=&quot;form-control&quot;
               value=&quot;1&quot; th:value=&quot;${post.id}&quot; readonly&gt;
    &lt;/div&gt;

    &lt;div&gt;
        &lt;label for=&quot;postTitle&quot;&gt;제목&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;postTitle&quot; name=&quot;postTitle&quot; class=&quot;form-control&quot;
               value=&quot;제목&quot; th:value=&quot;${post.title}&quot; readonly&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;label for=&quot;postContent&quot;&gt;내용&lt;/label&gt;
        &lt;textarea class=&quot;form-control&quot; rows=&quot;5&quot;
                  id=&quot;postContent&quot; name=&quot;postContent&quot;
                  th:value=&quot;${post.content}&quot; th:text=&quot;${post.content}&quot; readonly&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;label for=&quot;writer&quot;&gt;작성자&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;writer&quot; name=&quot;writer&quot; class=&quot;form-control&quot;
               value=&quot;작성자&quot; th:value=&quot;${post.user.loginId}&quot; readonly&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;label for=&quot;registerDate&quot;&gt;작성일&lt;/label&gt;
        &lt;input type=&quot;text&quot; id=&quot;registerDate&quot; name=&quot;registerDate&quot; class=&quot;form-control&quot;
               value=&quot;작성자&quot; th:value=&quot;${post.registerDate}&quot; readonly&gt;
    &lt;/div&gt;
    &lt;hr class=&quot;my-4&quot;&gt;

    &lt;button class=&quot;btn btn-primary&quot;
            th:onclick=&quot;|location.href=&#39;@{/board/{postId}/edit(postId=${post.id})}&#39;|&quot;
            type=&quot;button&quot;&gt;수정&lt;/button&gt;

    &lt;button class=&quot;btn btn-danger&quot;
            th:onclick=&quot;|location.href=&#39;@{/board/{postId}/delete(postId=${post.id})}&#39;|&quot;
            type=&quot;button&quot;&gt;삭제&lt;/button&gt;


&lt;/div&gt; &lt;!-- /container --&gt;

&lt;/body&gt;</code></pre><ul>
<li>내용(<code>content</code>)을 적는 칸에는 <code>input</code> 태그 대신 <code>textarea</code> 태그를 사용하여, 여러 줄의 텍스트를 입력할 수 있는 입력 칸을 만든다.<ul>
<li>주의할 점: <code>textarea</code> 태그는 <strong><code>th:text</code></strong>를 사용해야지 넘겨받은 변수내용을 표기할 수 있다. <code>th:value</code>만 사용한다면 값이 표기되지 않는다.</li>
</ul>
</li>
<li><code>input</code> 태그와 <code>textarea</code> 태그의 속성으로 <strong><code>readonly</code></strong>를 사용하여 수정이 불가능하도록 한다.</li>
</ul>
<h3 id="게시글-등록-화면">게시글 등록 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/23a047e9-80d8-438e-8fff-8f16c6f4c8cb/image.png" alt=""></p>
<h3 id="게시글-조회-화면">게시글 조회 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/cd082b6d-45ca-40b2-ba88-12bd6c1a1a5c/image.png" alt=""></p>
<h3 id="게시글-상세-조회-화면">게시글 상세 조회 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/f141e6ea-738f-4d5f-aa40-2276858f16b5/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 9663 N-Queen[Java]]]></title>
            <link>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-9663-N-QueenJava</link>
            <guid>https://velog.io/@serendipity-dev/%EB%B0%B1%EC%A4%80-9663-N-QueenJava</guid>
            <pubDate>Sun, 28 Aug 2022 15:22:46 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/9663">https://www.acmicpc.net/problem/9663</a></p>
<h3 id="풀이">풀이</h3>
<p>백트래킹의 대표적인 문제다.
맨 첫번째 줄부터 하나씩 퀸을 놓은 다음, 그 다음 줄에 퀸을 놓는데 만약 퀸을 놓을 수 없는 자리면 그 칸은 더이상 탐색을 하지 않고 그 줄의 다음 칸에 퀸을 놓는다.</p>
<h3 id="코드">코드</h3>
<pre><code>import java.io.*;
import java.math.BigInteger;
import java.util.*;

public class Main {

    static int n, m;
    static int[][] queen;
    static int answer = 0;

    static public boolean check(int row, int col) {
        for (int i = 1; i &lt; row; i++) { //같은 열에 퀸이 있는 경우
            if (queen[i][col] == 1) {
                return false;
            }
        }
        for (int i = row-1, j = col-1; i &gt; 0 &amp;&amp; j &gt; 0; i--, j--) { //현재 놓은 위치에 대각선에 퀸이 있는 경우
            if (queen[i][j] == 1) {
                return false;
            }
        }
        for (int i = row-1, j = col + 1; i &gt; 0 &amp;&amp; j &lt;= n; i--, j++) { //현재 놓은 위치에 대각선에 퀸이 있는 경우
            if (queen[i][j] == 1)
                return false;
        }
        return true;
    }

    static public void nQueen(int row) {
        if (row &gt; n) {
            answer++;
            return;
        }

        for (int i = 1; i &lt;= n; i++) {
            queen[row][i] = 1;
            if (check(row, i)) {
                nQueen(row + 1);
            }
            queen[row][i] = 0;
        }
    }

    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        n = Integer.parseInt(br.readLine());

        queen = new int[n+1][n+1];

        nQueen(1);

        System.out.println(answer);
    }
}</code></pre><ul>
<li><code>check</code> 함수를 통해 현재 칸에 퀸을 놓을 수 있는지 체크한다. 만약 놓을 수 있으면 <code>nQueen(row + 1)</code>을 호출하여 다음 줄에 퀸을 놓는다.</li>
<li><code>row &gt; n</code>이라면, 모든 줄에 퀸을 놓은 것이므로, <code>answer++</code>한다.</li>
</ul>
<hr>
<h3 id="다른-풀이">다른 풀이</h3>
<ul>
<li>2차원 배열이 아닌 <strong>1차원 배열</strong>을 사용한다. 각 원소의 <strong>index를 &#39;열</strong>&#39;이라 생각하고, <strong>원소 값을 &#39;행</strong>&#39;이라 생각하는 것이다.</li>
<li>depth는 각 줄을 의미하고, 0부터 시작한다.</li>
<li><code>depth == N</code>이면 <code>count++</code>한다.</li>
<li>check 함수로 퀸이 같은 줄에 놓여있는지, 대각선 위치에 있는지 확인한다.<ul>
<li><code>arr[col] == arr[i]</code></li>
<li><code>Math.abs(col - i) == Math.abs(arr[col] - arr[i])</code><pre><code>import java.util.*;
</code></pre></li>
</ul>
</li>
</ul>
<p>public class Main {</p>
<pre><code>static int N;
static int[] arr;
static int count = 0;

public static void main(String[] args) {
    // TODO Auto-generated method stub

    Scanner sc = new Scanner(System.in);

    N = sc.nextInt();
    arr = new int[N];

    N_queen(0);

    System.out.println(count);
}

static void N_queen(int depth) {

    if(depth == N) {
        count++;
        return;
    }


    for(int i = 0; i &lt; N; i ++) {
        arr[depth] = i;

        if(check(depth) == true) {
            N_queen(depth + 1);
        }
    }
}

static boolean check(int col) {

    for(int i = 0; i &lt; col; i ++) {

        if(arr[col] == arr[i]) {
            return false;

        }else if(Math.abs(col - i) == Math.abs(arr[col] - arr[i])) {
            return false;
        }
    }

    return true;
}</code></pre><p>}</p>
<p>```
참고: <a href="https://st-lab.tistory.com/118">https://st-lab.tistory.com/118</a>
<a href="https://velog.io/@ilil1/%EB%B0%B1%EC%A4%80-9663%EB%B2%88-java">https://velog.io/@ilil1/%EB%B0%B1%EC%A4%80-9663%EB%B2%88-java</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스프링 부트 게시판 프로젝트 - 8 | 로그인 기능 개발]]></title>
            <link>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-8-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C-3</link>
            <guid>https://velog.io/@serendipity-dev/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8-%EA%B2%8C%EC%8B%9C%ED%8C%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-8-%EC%9B%B9-%EA%B3%84%EC%B8%B5-%EA%B0%9C%EB%B0%9C-3</guid>
            <pubDate>Sun, 28 Aug 2022 08:41:28 GMT</pubDate>
            <description><![CDATA[<h2 id="로그인-컨트롤러-서비스-개발">로그인 컨트롤러, 서비스 개발</h2>
<h3 id="로그인-폼-전송용-객체">로그인 폼 전송용 객체</h3>
<ul>
<li>로그인 ID와 비밀번호를 전송하기 위한 폼 전송용 객체를 따로 생성한다.</li>
<li><code>controller</code> 패키지 내 <code>login</code> 패키지를 생성하여 <code>login</code> 패키지 안에서 생성한다.</li>
</ul>
<p><strong><code>UserLoginForm</code></strong></p>
<pre><code>package hello.board.controller.login;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;

@Getter @Setter
public class UserLoginForm {

    @NotBlank
    private String loginId;

    @NotBlank
    private String password;
}
</code></pre><h3 id="로그인-서비스">로그인 서비스</h3>
<p><strong><code>LoginService</code></strong></p>
<pre><code>package hello.board.service;

import hello.board.entity.User;
import hello.board.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class LoginService {

    private final UserRepository userRepository;

    /**
     * 로그인
     * @return null 로그인 실패
     */
    public User login(String loginId, String password) {
        return userRepository.findByLoginId(loginId)
                .filter(u -&gt; u.getPassword().equals(password))
                .orElse(null);
    }
}</code></pre><ul>
<li><strong>핵심 비즈니스 로직 : 로그인이 되는지 판단하는 로직</strong>
로그인의 핵심 비즈니스 로직은 회원을 조회한 다음에 파라미터로 넘어온 password와 비교해서 같으면 회원을 반환하고, 만약 password가 다르면 <code>null</code>을 반환한다.</li>
</ul>
<blockquote>
</blockquote>
<pre><code class="language-java">Optional&lt;User&gt; findUser = userRepository.findByLoginId(loginId);
User user = findUser.get();
if (user.getPassword().equals(password)) {
        return user;
}
else return null;</code></pre>
<p>⇒ 스트림 사용</p>
<pre><code class="language-java">return userRepository.findByLoginId(loginId)
                .filter(u -&gt; u.getPassword().equals(password))
                .orElse(null);</code></pre>
<h3 id="로그인-컨트롤러">로그인 컨트롤러</h3>
<ul>
<li>login 패키지 안에서 생성한다.</li>
</ul>
<p><strong><code>loginController</code></strong></p>
<pre><code>package hello.board.controller.login;

import hello.board.entity.User;
import hello.board.service.LoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import javax.validation.Valid;

@Controller
@Slf4j
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    @GetMapping(&quot;/login&quot;)
    public String loginForm(@ModelAttribute(&quot;loginForm&quot;) UserLoginForm loginForm) {
        return &quot;login/loginForm&quot;;
    }

    @PostMapping(&quot;/login&quot;)
    public String login(@Valid @ModelAttribute(&quot;loginForm&quot;) UserLoginForm loginForm, BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors = {}&quot;, bindingResult);
            return &quot;login/loginForm&quot;;
        }

        User loginUser = loginService.login(loginForm.getLoginId(), loginForm.getPassword());

        if (loginUser == null) {
            log.info(&quot;login Fail&quot;);
            bindingResult.reject(&quot;loginFail&quot;, &quot;아이디 또는 비밀번호가 맞지 않습니다&quot;);
            return &quot;login/loginForm&quot;;
        }

        //로그인 성공 TODO

        return &quot;redirect:/&quot;;
    }
}</code></pre><ul>
<li>로그인 컨트롤러는 <strong>로그인 서비스를 호출</strong>해서 <strong>로그인에 성공하면 홈 화면으로 이동</strong>하고, 로그인에 실패하면 <code>bindingResult.reject()</code> 를 사용해서 <strong>글로벌 오류</strong>( <code>ObjectError</code>)를 생성한다. 그리고 정보를 <strong>다시 입력하도록 로그인 폼을 뷰 템플릿</strong>으로 사용한다.</li>
<li>글로벌 오류는 직접 작성하는 것이 낫다. (DB 조회 과정을 거치는 경우도 있기 때문)</li>
</ul>
<h3 id="로그인-폼-뷰-템플릿">로그인 폼 뷰 템플릿</h3>
<p><code>templates/login/loginForm.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
    &lt;style&gt;
    .container {
    max-width: 560px;
    }
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
&lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;로그인&lt;/h2&gt;
    &lt;/div&gt;
    &lt;form action=&quot;item.html&quot; th:action th:object=&quot;${loginForm}&quot; method=&quot;post&quot;&gt;

        &lt;div th:if=&quot;${#fields.hasGlobalErrors()}&quot;&gt;
            &lt;p class=&quot;field-error&quot; th:each=&quot;err : ${#fields.globalErrors()}&quot;
               th:text=&quot;${err}&quot;&gt;전체 오류 메시지&lt;/p&gt;
        &lt;/div&gt;

        &lt;div&gt;
            &lt;label for=&quot;loginId&quot;&gt;ID&lt;/label&gt;
            &lt;input type=&quot;text&quot; id=&quot;loginId&quot; th:field=&quot;*{loginId}&quot; class=&quot;form-control&quot;
                   th:errorclass=&quot;field-error&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{loginId}&quot; /&gt;
        &lt;/div&gt;
        &lt;div&gt;
            &lt;label for=&quot;password&quot;&gt;비밀번호&lt;/label&gt;
            &lt;input type=&quot;password&quot; id=&quot;password&quot; th:field=&quot;*{password}&quot;
                   class=&quot;form-control&quot;
                   th:errorclass=&quot;field-error&quot;&gt;
            &lt;div class=&quot;field-error&quot; th:errors=&quot;*{password}&quot; /&gt;
        &lt;/div&gt;

        &lt;hr class=&quot;my-4&quot;&gt;
        &lt;div class=&quot;row&quot;&gt;
            &lt;div class=&quot;col&quot;&gt;
                &lt;button class=&quot;w-100 btn btn-primary btn-lg&quot; type=&quot;submit&quot;&gt;
                    로그인&lt;/button&gt;
            &lt;/div&gt;
            &lt;div class=&quot;col&quot;&gt;
                &lt;button class=&quot;w-100 btn btn-secondary btn-lg&quot;
                        th:onclick=&quot;|location.href=&#39;@{/}&#39;|&quot;
                        type=&quot;button&quot;&gt;취소&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/form&gt;
&lt;/div&gt; &lt;!-- /container --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><h3 id="로그인-화면">로그인 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/fb60a0d7-1bfd-4e4d-a305-9013e2cb9d9f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/985dee7a-6a15-4c06-b661-c33d625aae0f/image.png" alt=""></p>
<hr>
<h2 id="로그인-상태-유지">로그인 상태 유지</h2>
<ul>
<li><strong>쿠키와 세션(<code>HttpSession</code>)</strong>을 사용하여 로그인 상태를 유지하고, 로그인이 되면 홈 화면에 사용자의 로그인 ID가 보이도록 한다.<h4 id="서블릿-http-세션">서블릿 HTTP 세션</h4>
</li>
<li>서블릿은 세션을 위해 <code>HttpSession</code> 이라는 기능을 제공하는데, <strong>세션을 생성, 조회, 삭제</strong>하는 기능을 제공한다. 추가로 <strong>세션을 일정시간 사용하지 않으면 해당 세션을 삭제하는 기능</strong>을 제공한다.</li>
<li><strong>서블릿을 통해 HttpSession 을 생성</strong>하면 다음과 같은 <strong>쿠키를 생성</strong>한다. 쿠키 이름이 <code>JSESSIONID</code>이고, 값은 추정 불가능한 랜덤 값이다.</li>
</ul>
<p><code>Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05</code></p>
<blockquote>
<p><strong><a href="https://tofusand-dev.tistory.com/89#token-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D">Cookie, Session, Token 의 차이점</a></strong>
세션 / 쿠키 방식과 토큰의 가장 큰 차이점은 <strong>세션 / 쿠키</strong>는 <strong>세션 저장소에 유저 정보</strong>를 넣는 반면, <strong>JWT</strong> 는 <strong>토큰 안에 유저의 정보</strong>들이 넣어진다는 점 입니다. 클라이언트 입장에서는 HTTP 헤더에 세션 ID 나 토큰을 실어서 보내준다는 점에선 동일하지만, <strong>서버 측에서는 인증을 위해 암호화를 한다 vs 별도의 저장소를 이용한다</strong> 의 차이가 발생합니다.</p>
</blockquote>
<h4 id="sessionconst">SessionConst</h4>
<pre><code>package hello.board.controller;

public abstract class SessionConst {
    public static final String LOGIN_USER = &quot;loginUser&quot;;
}</code></pre><ul>
<li><code>HttpSession</code> 에 데이터를 보관하고 조회할 때, 같은 이름이 중복되어 사용되므로, 상수를 하나 정의했다.</li>
<li>생성하는 객체가 아니라 static final로 글자만 참조할 것이기 때문에, <strong>추상 클래스나 인터페이스</strong>로 생성한다.</li>
</ul>
<h3 id="로그인-컨트롤러-1">로그인 컨트롤러</h3>
<pre><code>package hello.board.controller.login;

import hello.board.controller.SessionConst;
import hello.board.entity.User;
import hello.board.service.LoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;

@Controller
@Slf4j
@RequiredArgsConstructor
public class LoginController {

    private final LoginService loginService;

    @GetMapping(&quot;/login&quot;)
    public String loginForm(@ModelAttribute(&quot;loginForm&quot;) UserLoginForm loginForm) {
        return &quot;login/loginForm&quot;;
    }

    @PostMapping(&quot;/login&quot;)
    public String login(@Valid @ModelAttribute(&quot;loginForm&quot;) UserLoginForm loginForm, BindingResult bindingResult, HttpServletRequest request) {

        if (bindingResult.hasErrors()) {
            log.info(&quot;errors = {}&quot;, bindingResult);
            return &quot;login/loginForm&quot;;
        }

        User loginUser = loginService.login(loginForm.getLoginId(), loginForm.getPassword());

        if (loginUser == null) {
            log.info(&quot;login Fail&quot;);
            bindingResult.reject(&quot;loginFail&quot;, &quot;아이디 또는 비밀번호가 맞지 않습니다&quot;);
            return &quot;login/loginForm&quot;;
        }

        //로그인 성공
        //세션이 있으면 세션 반환, 없으면 신규 세션을 생성
        HttpSession session = request.getSession();

        //세션에 로그인 회원 정보 보관
        session.setAttribute(SessionConst.LOGIN_USER, loginUser);

        return &quot;redirect:/&quot;;
    }

    @PostMapping(&quot;/logout&quot;)
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        return &quot;redirect:/&quot;;
    }
}
</code></pre><p><strong>세션 생성과 조회</strong>
세션을 생성하려면 <code>request.getSession(true)</code> 를 사용하면 된다. <code>public HttpSession getSession(boolean create);</code></p>
<ul>
<li><code>request.getSession(true)</code> : default<ul>
<li>세션이 있으면 기존 세션을 반환한다.</li>
<li>세션이 없으면 <strong>새로운 세션을 생성해서 반환</strong>한다.</li>
</ul>
</li>
<li><code>request.getSession(false)</code><ul>
<li>세션이 있으면 기존 세션을 반환한다.</li>
<li>세션이 없으면 새로운 세션을 생성하지 않는다. <strong>null 을 반환</strong>한다.</li>
</ul>
</li>
</ul>
<p><strong>세션에 로그인 회원 정보 보관</strong>
<code>session.setAttribute(SessionConst.LOGIN_USER, loginUser);</code></p>
<p><strong>로그아웃</strong>
<code>session.invalidate()</code> : 세션을 제거한다.</p>
<blockquote>
<p><strong>HttpSession 동작원리</strong></p>
</blockquote>
<ul>
<li><code>request.getSession()</code>을 하면 
<code>request</code> 정보에서 얻어온 UUID값으로 이뤄진 쿠키의 value 값을 보고  Session들을 모아둔 Session저장소에서 동일한 <code>sessionId(=UUID)</code> 값이 있는지 찾는다.  </li>
<li>Session저장소에서  <code>sessionId</code>는  key값으로 쓰인다. </li>
<li>동일한 <code>sessionId</code>가 있으면 해당 Session을 가져오고, sessionId가 없으면 Session을 새로 만들어 반환한다. </li>
<li><code>session.setAttribute(SessionConst.LOGIN_USER, loginUser)</code>를 하면 <code>request.getSession</code>으로 가져온 특정 Session 내에 <code>key(=SESSIONCONST.LOGIN_USER)</code>와 <code>value(=loginUser)</code>를 저장시켜 나중에, <code>sessionId</code>를 통해 특정 Session을 가져올 때 가져온 Session 내에서 <code>key(loginUser)</code>를 가지고 loginUser 값을 가져올 수 있다.
참고 : <a href="https://www.inflearn.com/questions/520956">인프런 커뮤니티 질문 글</a></li>
</ul>
<blockquote>
<p><strong>세션 생성</strong> 후, <strong>응답에 쿠키로 <code>jsessionid</code></strong>가 담겨져 <strong>클라이언트에게 전달</strong>된다. <code>jsessionid</code>가 <strong>발급되고 응답에 쿠키로 넣는 것</strong>은 <strong>tomcat</strong>이 처리한다.
⇒ <strong>세션 생성 시, 세션 ID 생성 및 쿠키 생성을 tomcat이 다 처리한다.</strong></p>
</blockquote>
<h3 id="홈-컨트롤러-수정">홈 컨트롤러 수정</h3>
<p><code>HomeController - homeLogin()</code></p>
<pre><code>package hello.board.controller;

import hello.board.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttribute;

@Controller
@Slf4j
public class HomeController {

//    @GetMapping(&quot;/&quot;)
//    public String home() {
//        log.info(&quot;home controller&quot;);
//        return &quot;home&quot;;
//    }

    @GetMapping(&quot;/&quot;)
    public String loginHome(@SessionAttribute(name = SessionConst.LOGIN_USER, required = false) User loginUser, Model model) {

        if (loginUser == null) {
            return &quot;home&quot;;
        }

        model.addAttribute(&quot;user&quot;, loginUser);
        return &quot;loginHome&quot;;
    }
}</code></pre><ul>
<li><code>@SessionAttribute</code>를 사용하여 세션을 찾고, 세션에 들어있는 데이터를 찾는 과정을 편리하게 처리한다.</li>
</ul>
<h3 id="홈-화면---로그인-사용자-전용">홈 화면 - 로그인 사용자 전용</h3>
<p><code>templates/loginHome.html</code></p>
<pre><code>&lt;!DOCTYPE HTML&gt;
&lt;html xmlns:th=&quot;http://www.thymeleaf.org&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;link th:href=&quot;@{/css/bootstrap.min.css}&quot;
          href=&quot;../css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot; style=&quot;max-width: 600px&quot;&gt;
    &lt;div class=&quot;py-5 text-center&quot;&gt;
        &lt;h2&gt;홈 화면&lt;/h2&gt;
    &lt;/div&gt;

    &lt;h4 class=&quot;mb-3&quot; th:text=&quot;|로그인: ${user.loginId}|&quot;&gt;로그인 사용자 이름&lt;/h4&gt;

    &lt;hr class=&quot;my-4&quot;&gt;

    &lt;div class=&quot;row&quot;&gt;
        &lt;div class=&quot;col&quot;&gt;
            &lt;button class=&quot;w-100 btn btn-info btn-lg&quot; type=&quot;button&quot;
                    th:onclick=&quot;|location.href=&#39;@{/board}&#39;|&quot;&gt;
                게시판
            &lt;/button&gt;
        &lt;/div&gt;
        &lt;div class=&quot;col&quot;&gt;
            &lt;form th:action=&quot;@{/logout}&quot; method=&quot;post&quot;&gt;
                &lt;button class=&quot;w-100 btn btn-dark btn-lg&quot; type=&quot;submint&quot;&gt;
                    로그아웃
                &lt;/button&gt;
            &lt;/form&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;hr class=&quot;my-4&quot;&gt;

&lt;/div&gt; &lt;!-- /container --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><hr>
<h2 id="문제점">문제점</h2>
<p>로그인을 처음 시도하면 홈 화면이 뜨지 않고 Welcom Page가 뜬다.
<img src="https://velog.velcdn.com/images/serendipity-dev/post/497c7daa-5ab5-4a5e-8d49-f5df4330c80f/image.png" alt="">
로그를 찍어본 결과, login을 하면 홈 화면으로 리다이렉트 되어야 하는데 리다이렉트 되지 않는다.
하지만 다른 창에서 다시 localhost로 접속하면 로그인 화면이 제대로 뜬다.</p>
<h3 id="해결">해결</h3>
<p><strong>로그인을 처음 시도</strong>하면 URL이 다음과 같이 jsessionid 를 포함하고 있는 것을 확인할 수 있다.
<code>http://localhost:8080/;jsessionid=6B84F014CBDF6C3D24DE6E532C83B291</code>
이것은 <strong>웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지</strong>하는 방법이다.
URL에 jsessionid가 같이 전달되어 컨트롤러에서 해당되는 URL을 찾지 못하고 Welcome Page가 뜨는 것이다.
URL 전달 방식을 끄고 <strong>항상 쿠키를 통해서만 세션을 유지하고 싶으면</strong> 다음 옵션을 넣어주면 된다. 이렇게 하면 <strong>URL에 jsessionid가 노출되지 않는다.</strong>
<code>application.properties</code>
<code>server.servlet.session.tracking-modes=cookie</code></p>
<blockquote>
<h3 id="trackingmodes">TrackingModes</h3>
<p>로그인을 처음 시도하면 URL이 다음과 같이 jsessionid 를 포함하고 있는 것을 확인할 수 있다.
<code>http://localhost:8080/;jsessionid=F59911518B921DF62D09F0DF8F83F872</code>
이것은 <strong>웹 브라우저가 쿠키를 지원하지 않을 때 쿠키 대신 URL을 통해서 세션을 유지하는 방법</strong>이다. 이 방법을 사용하려면 <strong>URL에 이 값을 계속 포함해서 전달해야 한다</strong>. 타임리프 같은 템플릿은 엔진을 통해서 링크를 걸면 <code>jsessionid</code>를 URL에 자동으로 포함해준다. <strong>서버 입장에서 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 <code>jsessionid</code>도 함께 전달한다.</strong></p>
</blockquote>
<p>정상적으로 로그인 화면이 뜨는 것을 확인할 수 있다.</p>
<h3 id="로그인-성공-화면">로그인 성공 화면</h3>
<p><img src="https://velog.velcdn.com/images/serendipity-dev/post/b609e941-8f24-403d-b6ee-53d789d1ba08/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>