<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>2025-pictoletter.log</title>
        <link>https://velog.io/</link>
        <description>한줄소개불가</description>
        <lastBuildDate>Tue, 26 Aug 2025 08:01:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>2025-pictoletter.log</title>
            <url>https://velog.velcdn.com/images/2025-pictoletter/profile/bb4afd76-7a32-4432-bc40-bb1b30486046/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 2025-pictoletter.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/2025-pictoletter" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[20250826 - @media 반응형]]></title>
            <link>https://velog.io/@2025-pictoletter/20250826-media-%EB%B0%98%EC%9D%91%ED%98%95</link>
            <guid>https://velog.io/@2025-pictoletter/20250826-media-%EB%B0%98%EC%9D%91%ED%98%95</guid>
            <pubDate>Tue, 26 Aug 2025 08:01:24 GMT</pubDate>
            <description><![CDATA[<ul>
<li>반응형<pre><code>body{
background-color:lightblue;
}
/*화면 너비가 500 이하 일때*/
@media(max-width:500px){
body{
  background-color:red;
  font-size:16px;
}
}
</code></pre></li>
</ul>
<p>/<em>화면 너비가 601 이상 일때</em>/
@media(min-width:601px){
    body{
    background-color:gold;
    font-size:26px;
  }
}</p>
<h1>안녕하세요</h1>
<p>welcome</p>
```

<pre><code>&lt;html&gt;
&lt;header&gt;&lt;/header&gt;
&lt;style&gt;
body, ul, li {
  margin:0;
  padding:0;
  list-style:none;
  font-family:verdana;
}
a{
    color:inherit;
    text-decoration:none;
}

.navigation {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  list-style: none;
  margin: 0;
  background: deepskyblue;
}

.navigation &gt; li &gt; a {
  text-decoration: none;
  display: block;
  padding: 1em;
  color: white;
}

.navigation &gt; li:hover &gt; a {
  background: #1565C0;
}

@media (max-width: 800px) {
  .navigation {
    justify-content: space-around;
    /*요소 주변에 동일한 간격 배분 (양 끝도 여백 있음)*/
  }
}

@media (max-width: 600px) {
  .navigation {
    flex-direction: column;
    padding: 0;
  }
  .navigation &gt; li &gt; a {
    text-align: center;
    padding: 10px;
    border-top: 1px solid rgba(255, 255, 255,0.3); 
    border-bottom: 1px solid rgba(0, 0, 0, 0.1); 
  }
  .navigation &gt; li:last-of-type &gt; a {
    border-bottom: none;
  }
}


.tmenu {
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
  list-style: none;
  margin: 0;
  background: deepskyblue;

}

.tmenu &gt; ul &gt; li{
 display: flex;
}

.tmenu &gt; ul &gt; li &gt; a {
  text-decoration: none;
  display: block;
  padding: 1em;
  color: white;
  justify-content: flex-end;
}

.tmenu &gt; ul &gt; li:hover &gt; a {
  background: #1565C0;
}

.container {
  display:flex;  /* container 안의 자식 요소들이 가로 방향으로 나란히 정렬 */
  flex-direction:column; /* Flexbox 레이아웃에서 자식 요소들을 세로 방향(위에서 아래로) 정렬하도록 설정하는 속성입니다. 기본값인 row가 가로 정렬이라면, column은 그 반대 방향 */
  min-height:100dvh;
  background-color:darkcyan;

  flex-grow:0;/*Flexbox에서 요소가 남는 공간을 전혀 차지하지 않도록 설정. 0이면 절대 확장하지 않음 */


}
.container &gt; header {
  background-color:red; /* 임시 */
}
.container &gt; section {
  background-color:gold; /* 임시 */
  display:flex; /* 하위 엘리멘트 적용을 위한 선언*/
  flex-direction:row;
  flex-grow:1;
}

.container &gt; section &gt; nav {
  background-color:#CFCFCF; /* 임시 */
  flex-basis:150px; /* 현재 기준은 width 값을 150 */
  flex-shrink:0; /*줄지마세요*/
}

.container &gt; section &gt; nav &gt; ul &gt; li:hover{
  background-color:black;
  color:white;
  cursor:pointer;
}

.container &gt; section &gt; main {
  background-color:white; /* 임시 */
    flex-grow:1;
  flex-shrink:1; /*줄지마세요*/
}
.container &gt; section &gt; aside {
  background-color:#00FCCC; /* 임시 */
  flex-basis:120px;
  flex-shrink:0; 
}

.container &gt; footer {
  background-color:green; /* 임시 */
  display:flex; /* Flexbox 레이아웃을 활성화하는 핵심 속성 */
  justify-content:space-between; /* 주축(main axis)을 기준으로 자식 요소들을 가운데 정렬하는 속성 
  반대방향 정렬은 flex-direction: row; */  
  padding-left:10px;
  padding-right:10px;
}
&lt;/style&gt;
&lt;body&gt;
&lt;div class=&quot;container&quot;&gt;
    &lt;ul class=&quot;navigation&quot;&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Here&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;There&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Some&lt;/a&gt;&lt;/li&gt;
            &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Nowhere&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
&lt;/div&gt;


&lt;!--
&lt;div class=&quot;tmenu&quot;&gt;
    &lt;ul&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Here&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;There&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Some&lt;/a&gt;&lt;/li&gt;
        &lt;li&gt;&lt;a href=&quot;#&quot;&gt;Nowhere&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
&lt;/div&gt;
--&gt; 
&lt;/body&gt;
&lt;/html&gt;
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[20250825 - CSS Display layout]]></title>
            <link>https://velog.io/@2025-pictoletter/20250825-JS-JQuery</link>
            <guid>https://velog.io/@2025-pictoletter/20250825-JS-JQuery</guid>
            <pubDate>Tue, 26 Aug 2025 04:24:19 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>lvh svh dvh
<img src="https://velog.velcdn.com/images/2025-pictoletter/post/f50ed983-4bf9-41db-bc79-744cad672ae2/image.png" alt=""></p>
</li>
<li><p>display layout</p>
<pre><code>&lt;html&gt;
&lt;header&gt;&lt;/header&gt;
&lt;style&gt;
body, ul, li {
margin:0;
padding : 0px;
}
a{
  color:inherit;
  text-decoration:none;
}
</code></pre></li>
</ul>
<p>.container {
  display:flex;  /* container 안의 자식 요소들이 가로 방향으로 나란히 정렬 <em>/
  flex-direction:column; /</em> Flexbox 레이아웃에서 자식 요소들을 세로 방향(위에서 아래로) 정렬하도록 설정하는 속성입니다. 기본값인 row가 가로 정렬이라면, column은 그 반대 방향 <em>/
  min-height:100dvh;
  background-color:darkcyan;
  flex-grow:0;/</em>Flexbox에서 요소가 남는 공간을 전혀 차지하지 않도록 설정. 0이면 절대 확장하지 않음 */</p>
<p>}
.container &gt; header {
  background-color:red; /* 임시 <em>/
}
.container &gt; section {
  background-color:gold; /</em> 임시 <em>/
  display:flex; /</em> 하위 엘리멘트 적용을 위한 선언*/
  flex-direction:row;
  flex-grow:1;
}</p>
<p>.container &gt; section &gt; nav {
  background-color:#CFCFCF; /* 임시 <em>/
  flex-basis:150px; /</em> 현재 기준은 width 값을 150 <em>/
  flex-shrink:0; /*줄지마세요</em>/
}</p>
<p>.container &gt; section &gt; nav &gt; ul &gt; li:hover{
  background-color:black;
  color:white;
  cursor:pointer;
}</p>
<p>.container &gt; section &gt; main {
  background-color:white; /* 임시 <em>/
    flex-grow:1;
  flex-shrink:1; /*줄지마세요</em>/
}
.container &gt; section &gt; aside {
  background-color:#00FCCC; /* 임시 */
  flex-basis:120px;
  flex-shrink:0; 
}</p>
<p>.container &gt; footer {
  background-color:green; /* 임시 <em>/
  display:flex; /</em> Flexbox 레이아웃을 활성화하는 핵심 속성 <em>/
  justify-content:space-between; /</em> 주축(main axis)을 기준으로 자식 요소들을 가운데 정렬하는 속성 
  반대방향 정렬은 flex-direction: row; */<br>  padding-left:10px;
  padding-right:10px;
}
</style></p>
<body>
<div class="container">
  <header>
    <h1>레이아웃 배우기</h1><!-- <h1>열심히 배우기</h1> -->
  </header>
  <section class="content">
    <nav>
        <ul>
            <li>nav menu-1</li>
            <li>nav menu-2</li>
            <li>nav menu-3</li>
            <li>nav menu-4</li>
        </ul>
    </nav>
    <main>
        hello there 
    </main>
    <aside>
        something
    </aside>
  </section>
      <footer>
            <a href="#">Legal Notice</a>
            <a href="#">SBS Academy</a>
            <a href="#">leftright@</a>
    </footer>
</div>  
</body>
</html>

<pre><code>- side bar menu</code></pre><div class="left-side-bar">
    <div class="icon-status">
        <span>▶</span>
        <span>▼</span>
    </div>

<pre><code>&lt;nav class=&quot;menu-box-1&quot;&gt;
    &lt;ul&gt;
        &lt;li&gt;
            &lt;a href=&quot;#&quot;&gt;1차 메뉴 아이템 1&lt;/a&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 1&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 2&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 3&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 4&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
        &lt;li&gt;
            &lt;a href=&quot;#&quot;&gt;1차 메뉴 아이템 2&lt;/a&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 1&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 2&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 3&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 4&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
        &lt;li&gt;
            &lt;a href=&quot;#&quot;&gt;1차 메뉴 아이템 3&lt;/a&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 1&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 2&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 3&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 4&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
        &lt;li&gt;
            &lt;a href=&quot;#&quot;&gt;1차 메뉴 아이템 4&lt;/a&gt;
            &lt;ul&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 1&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 2&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 3&lt;/a&gt;&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;#&quot;&gt;2차 메뉴 아이템 4&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/li&gt;
    &lt;/ul&gt;
&lt;/nav&gt;</code></pre></div>

<p>/* 노말라이즈 */
body, ul, li {
    margin:0;
    padding:0;
    list-style:none;
}</p>
<p>a {
    color:inherit;
    text-decoration:none;
}</p>
<p>/* 커스텀 */</p>
<p>/* 좌측 사이드 바 시작 */
.left-side-bar {
    position:absolute;
    top:0;
    left:-225px;
    width:250px;
    height:100%;
    background-color:#efefef;
    transition: left .3s;
}</p>
<p>.left-side-bar:hover {
    left:0;
}</p>
<p>/* 좌측 사이드 바 - 상태 아이콘 */
.left-side-bar &gt; .icon-status &gt; span {
    display:block;
    text-align:right;
    padding:5px;
}</p>
<p>.left-side-bar &gt; .icon-status &gt; span:last-child {
    display:none;
}</p>
<p>.left-side-bar:hover &gt; .icon-status &gt; span:last-child {
    display:block;
}</p>
<p>.left-side-bar:hover &gt; .icon-status &gt; span:first-child {
    display:none;
}</p>
<p>/* 좌측 사이드 바 - 메뉴 박스 1 */
.left-side-bar &gt; .menu-box-1 ul {
    background-color:#efefef;
}</p>
<p>.left-side-bar &gt; .menu-box-1 &gt; ul ul {
    display:none;
    position:absolute;
    left:100%;
    top:0;
}</p>
<p>.left-side-bar &gt; .menu-box-1 &gt; ul &gt; li {
    position:relative;
}</p>
<p>.left-side-bar &gt; .menu-box-1 ul &gt; li &gt; a {
    display:block;
    padding:10px;
    white-space:nowrap;
}</p>
<p>.left-side-bar &gt; .menu-box-1 ul &gt; li:hover &gt; a {
    background-color:black;
    color:white;
}</p>
<p>.left-side-bar &gt; .menu-box-1 &gt; ul &gt; li:hover &gt; ul {
    display:block;
}</p>
<p>/* 좌측 사이드 바 끝 */
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250822 - 학습일지 - CSS]]></title>
            <link>https://velog.io/@2025-pictoletter/20250822-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-CSS</link>
            <guid>https://velog.io/@2025-pictoletter/20250822-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-CSS</guid>
            <pubDate>Fri, 22 Aug 2025 06:16:57 GMT</pubDate>
            <description><![CDATA[<ul>
<li>opacity transition - copen 재확인요<pre><code>div {
width: 200px;
height: 200px;
background-color: red;
transition: width 3s, opacity 3s;
opacity:0.22;
}
</code></pre></li>
</ul>
<p>.class_div:hover {
  width: 100%;
  transition : width 100%;
  opacity:1;
}</p>
<div class="class_div"></div>

<pre><code>- before after</code></pre><ul>
    <li>연예인</li>
    <li>연예인</li>
    <li>연예인</li>
    <li>연예인</li>
</ul>

<p>ul &gt; li {
    position: relative;
    margin: 20px;
    padding: 10px;
    background-color: pink;
    width: 100px;
}</p>
<p>ul &gt; li::after {
    content: &quot;&quot;;
    position: absolute;
    width: 0;
    height: 2px;
    background-color: black;
    bottom: 0;
    left: 0;
    transition: width .3s;
}</p>
<p>ul &gt; li:hover::after {
    width: 100%;
}</p>
<pre><code>- table css</code></pre><p>.con {
  width: 1000px;
}
.con.table {
  width: 100%;
  border-collapse: collapse;
}
table th,
table td {
  border: 1px black solid;
  text-align: center;
  padding: 10px;
}
table tbody &gt; tr:hover {
  background-color: blue;
}
table tbody &gt; tr &gt; td:hover {
  background-color: yellow;
}</p>
<div class="con">
  <table border="1">
    <thead>
      <tr>
        <th>교시</th>
        <th>월</th>
        <th>화</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1교시</td>
        <td>수학</td>
        <td>영어</td>
      </tr>
    </tbody>
  </table>
</div>
```]]></description>
        </item>
        <item>
            <title><![CDATA[20250825 - 학습일지 -overflow 외]]></title>
            <link>https://velog.io/@2025-pictoletter/20250825-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-overflow-%EC%99%B8</link>
            <guid>https://velog.io/@2025-pictoletter/20250825-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-overflow-%EC%99%B8</guid>
            <pubDate>Fri, 22 Aug 2025 06:16:02 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>overflow 속성 추가 확인요</p>
<pre><code>DIV{
WIDTH:500PX;
BORDER : 10PX RED SOLID;
white-space:nowrap;

text-overflow : ellipsis;
/*
overflow : hidden;
overflow-x : hidden;
text-overflow : ellipsis;... 처리

*/
}</code></pre></li>
<li><p>Emmet 코딩</p>
<pre><code>nav.menumox&gt;ul&gt;li*5&gt;a{href=#}
nav.menumox&gt;ul&gt;li*5&gt;a{href=#}{1차 메뉴구성}+ul&gt;li*3&gt;a{href=#}{2차메뉴구성}코드를 입력하세요</code></pre></li>
<li><p>메뉴 따라하기</p>
<pre><code>body, ul, li {
margin:0;
padding:0;
list-style:none;
}
a {
color:inherit;
text-decoration:none;
}
</code></pre></li>
</ul>
<p>.menu-box-1 {
  text-align:center;
}</p>
<p>.menu-box-1 ul {
  background-color:#afafaf;
}
.menu-box-1 &gt; ul {
  display:inline-block;
  padding:0 20px;
  border-radius:10px;
}</p>
<p>.menu-box-1 &gt; ul &gt; li {
  display:inline-block;
}
.menu-box-1 ul &gt; li {
  position:relative;
}</p>
<p>.menu-box-1 ul &gt; li &gt; a {
  display:block;
  padding:10px;
  white-space:nowrap;
}</p>
<p>.menu-box-1 ul &gt; li:hover &gt; a {
  background-color:black;
  color:white;
}</p>
<p>.menu-box-1 &gt; ul ul {
  display:none;
  position:absolute;
  top:100%;
  left:0;
}</p>
<p>.menu-box-1 &gt; ul ul ul {
  left:100%;
  top:0;
}</p>
<p>.menu-box-1 ul &gt; li:hover &gt; ul {
  display:block;
}</p>
<p>.menu-box-1 &gt; ul &gt; li:last-child &gt; ul ul {
  left:auto;
  right:100%;
}</p>
<nav class="menu-box-1">
  <ul>
    <li>
      <a href="#">1차 메뉴 아이템 1</a>
      <ul>
        <li>
          <a href="#">2차 메뉴 아이템 1</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 2</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 3</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 4</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
      </ul>
    </li>
    <li>
      <a href="#">1차 메뉴 아이템 2</a>
      <ul>
        <li>
          <a href="#">2차 메뉴 아이템 1</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 2</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 3</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 4</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
      </ul>
    </li>
    <li>
      <a href="#">1차 메뉴 아이템 3</a>
      <ul>
        <li>
          <a href="#">2차 메뉴 아이템 1</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 2</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 3</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 4</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
      </ul>
    </li>
    <li>
      <a href="#">1차 메뉴 아이템 4</a>
      <ul>
        <li>
          <a href="#">2차 메뉴 아이템 1</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 2</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 3</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
        <li>
          <a href="#">2차 메뉴 아이템 4</a>
          <ul>
            <li><a href="#">3차 메뉴 아이템 1</a></li>
            <li><a href="#">3차 메뉴 아이템 2</a></li>
            <li><a href="#">3차 메뉴 아이템 3</a></li>
            <li><a href="#">3차 메뉴 아이템 4</a></li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>
</nav>코드를 입력하세요
```]]></description>
        </item>
        <item>
            <title><![CDATA[20250825 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250825-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250825-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Tue, 19 Aug 2025 04:12:56 GMT</pubDate>
            <description><![CDATA[<ul>
<li>active 상용 예시<pre><code>body, ul, li{
padding:0px;
margin:0px;
list-style:none;
}
a{
color : inherit;
text-decoration : none;
}
.menu-1{
    color : white;
    text-align:center;
          background: #4b545f;
          background: linear-gradient(top, #4f5964 0%, #5f6975 40%);
}
</code></pre></li>
</ul>
<p>.menu-1 &gt; ul{
  text-align:center;
  padding : 0 10px;
}
.menu-1 &gt; ul &gt; li{ 
  display:inline-block;
  text-align:center;
}</p>
<p>.menu-1 &gt; ul &gt; li &gt; a {
    padding : 10px;
    display: block;
}
.menu-1 &gt; ul &gt; li:hover &gt; a {
    padding : 10px;
    display: block;
    color : black;
    background-color : white;
}</p>
<p>.menu-1 &gt; ul &gt; li.active &gt; a {
  background-color: white;
  color: black;
}</p>
<nav class="menu-1">
  <ul>
    <li><a href="#">메뉴 목록 1</a></li>
    <li><a href="#">메뉴 목록 2</a></li>
    <li><a href="#">메뉴 목록 3</a></li>
    <li class="active"><a href="#">메뉴 목록 4</a></li>
  </ul>
</nav>

<pre><code>- 시맨틱 태그</code></pre><body>
  <header>
    <h1>창훈의 웹사이트</h1>
    <nav>
      <a href="#home">홈</a>
      <a href="#about">소개</a>
    </nav>
  </header>

  <main>
    <section>
      <article>
        <h2>최신 블로그 글</h2>
        <p>HTML5의 시맨틱 태그에 대해 알아봅시다.</p>
      </article>
      <aside>
        <h3>추천 글</h3>
        <ul>
          <li><a href="#">CSS 기초</a></li>
          <li><a href="#">JavaScript 입문</a></li>
        </ul>
      </aside>
    </section>
  </main>

  <footer>
    <p>© 2025 창훈. 모든 권리 보유.</p>
  </footer>
</body>

<pre><code>- 3번 답</code></pre><div class="menu-box">
  <!-- 메뉴 -->
  <div>
    <!-- 메뉴 아이템 -->
    <div>
      <!-- 메뉴 아이템 텍스트 -->
      <div>메뉴 아이템 1</div>
    </div>
    <div>
      <!-- 메뉴 아이템 텍스트 -->
      <div>메뉴 아이템 2</div>
    </div>
    <div>
      <!-- 메뉴 아이템 텍스트 -->
      <div>메뉴 아이템 3</div>
    </div>
    <div>
      <!-- 메뉴 아이템 텍스트 -->
      <div>메뉴 아이템 4</div>
    </div>
  </div>
</div>
/* 노말라이즈 */
a {
  color:inherit;
  text-decoration:none;
}

<p>/* 커스텀 */</p>
<p>/* 메뉴박스 */
.menu-box {
  text-align:center;
}</p>
<p>/* 메뉴 <em>/
.menu-box &gt; div {
  /</em> 메뉴에는 배경색이 있다. <em>/
  background-color:#dfdfdf;
  /</em> 메뉴는 너비가 최대한 줄어든다. */
  display:inline-block;
  border-radius:5px;
  padding:0 10px;
}</p>
<p>/* 메뉴 아이템 <em>/
.menu-box &gt; div &gt; div {
  /</em> 메뉴 아이템은 너비가 최대한 줄어들고 한줄에 여러개 나온다. */
  display:inline-block;
}</p>
<p>/* 메뉴 아이템 텍스트 <em>/
.menu-box &gt; div &gt; div &gt; div {
  padding:10px;
  /</em> 커서를 포인터로 바꿈 */
  cursor:pointer;
}</p>
<p>/* 마우스가 올려진 메뉴 아이템의 자식인 텍스트 */
.menu-box &gt; div &gt; div:hover &gt; div {
  background-color:black;
  color:white;
}</p>
<pre><code>- normalize </code></pre><ul>
  <li>항목 1</li>
  <li>항목 2</li>
</ul>
<br><br>
<a href="#">링크 1</a>
body, ul, li{
  padding:0px;
  margin:0px;
  list-style:none;
}
a {
  color:inherit;
  text-decoration:none;
}
```
- H 태그
```
<h1>H[1~6] tag</h1>
```
- div 태그
- ul[ol]/li
```
<ul>
  <li>항목 1</li>
  <li>항목 2</li>
  <li><div>항목</div><div>항목</div></li>
</ul>
<ol>
  <li>항목 1</li>
  <li>항목 2</li>
  <li><div>항목</div><div>항목</div></li>
</ol>
```]]></description>
        </item>
        <item>
            <title><![CDATA[20250814 - 학습일지 - js/css/html]]></title>
            <link>https://velog.io/@2025-pictoletter/20250814-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-jscsshtml</link>
            <guid>https://velog.io/@2025-pictoletter/20250814-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-jscsshtml</guid>
            <pubDate>Thu, 14 Aug 2025 04:58:42 GMT</pubDate>
            <description><![CDATA[<h4 id="let-const">let, const</h4>
<p>Block Scope는 변수나 상수가 중괄호 {}로 둘러싸인 블록 내부에서만 접근 가능하다는 의미.</p>
<ul>
<li>Before ES6 (2015), JavaScript did not have Block Scope.</li>
<li>JavaScript had Global Scope and Function Scope.</li>
<li>ES6 introduced the two new JavaScript keywords: let and const.</li>
<li>These two keywords provided Block Scope in JavaScript<pre><code>const PI;
PI = 3.14159265359; // reference error 
</code></pre></li>
</ul>
<p>// You can create a constant array:
const cars = [&quot;Saab&quot;, &quot;Volvo&quot;, &quot;BMW&quot;]; // ok</p>
<p>// You can change an element: // ok
cars[0] = &quot;Toyota&quot;;</p>
<p>// You can add an element: // ok
cars.push(&quot;Audi&quot;);</p>
<p>const cars = [&quot;Saab&quot;, &quot;Volvo&quot;, &quot;BMW&quot;];</p>
<p>cars = [&quot;Toyota&quot;, &quot;Volvo&quot;, &quot;Audi&quot;];    // ERROR</p>
<pre><code>- const object</code></pre><p>// You can create a const object:
const car = {type:&quot;Fiat&quot;, model:&quot;500&quot;, color:&quot;white&quot;};</p>
<p>// You can change a property:
car.color = &quot;red&quot;;</p>
<p>// You can add a property:
car.owner = &quot;Johnson&quot;;</p>
<p>{ //-- from the new block scope
  const car = {type:&quot;Fiat&quot;, model:&quot;500&quot;, color:&quot;white&quot;};</p>
<p>  car = {type:&quot;Volvo&quot;, model:&quot;EX60&quot;, color:&quot;red&quot;}    // ERROR
}</p>
<pre><code></code></pre><p>// -- from the new block scope
const x = 2;     // Allowed
x = 2;           // Not allowed
var x = 2;       // Not allowed
let x = 2;       // Not allowed
const x = 2;     // Not allowed</p>
<p>{ //- from another new block scoppe
  const x = 2;   // Allowed
  x = 2;         // Not allowed
  var x = 2;     // Not allowed
  let x = 2;     // Not allowed
  const x = 2;   // Not allowed
}</p>
<pre><code>- javascript hositing sample</code></pre><p>alert (carName);
const carName = &quot;Volvo&quot;; // ERROR</p>
<pre><code>
### JavaScript Arithmetic Operators
![](https://velog.velcdn.com/images/2025-pictoletter/post/01a1dae3-1c11-4ed0-9e22-880f46966d1a/image.png)
### JavaScript Assignment Operators
![](https://velog.velcdn.com/images/2025-pictoletter/post/172748c8-9408-441f-ae92-f87af3d1aacd/image.png)
### JavaScript Comparison Operators
![](https://velog.velcdn.com/images/2025-pictoletter/post/731b1830-54c2-4508-a7b1-37005eca02ae/image.png)

- javascript 삼항연산자(ternary operator) 예</code></pre><p>// if-else 구문
let status;
if (age &gt;= 18) {
  status = &#39;성인&#39;;
} else {
  status = &#39;미성년자&#39;;
}</p>
<p>// 삼항 연산자 사용
let status2 = age &gt;= 18 ? &#39;성인&#39; : &#39;미성년자&#39;;</p>
<p>// 중첩 예시
let grade = score &gt;= 90
  ? &#39;A&#39;
  : score &gt;= 80
    ? &#39;B&#39;
    : score &gt;= 70
      ? &#39;C&#39;
      : &#39;F&#39;;</p>
<p> // 가독성 예시 
// 1. 함수로 분리
function getGrade(score) {
  if (score &gt;= 90) return &#39;A&#39;;
  if (score &gt;= 80) return &#39;B&#39;;
  if (score &gt;= 70) return &#39;C&#39;;
  return &#39;F&#39;;
}</p>
<p>// 2. 배열-반복 활용
const thresholds = [
  { min: 90, grade: &#39;A&#39; },
  { min: 80, grade: &#39;B&#39; },
  { min: 70, grade: &#39;C&#39; },
];</p>
<p>function getGrade2(score) {
  const found = thresholds.find(t =&gt; score &gt;= t.min);
  return found ? found.grade : &#39;F&#39;;
}</p>
<pre><code>#### JavaScript Logical Operators
![](https://velog.velcdn.com/images/2025-pictoletter/post/308aaee2-dfcf-4259-a498-451a9a9bd33a/image.png)

#### JavaScript Type Operators
![](https://velog.velcdn.com/images/2025-pictoletter/post/373a3059-f87a-411a-b278-ccca902da16a/image.png)

#### JavaScript Bitwise Operators
![](https://velog.velcdn.com/images/2025-pictoletter/post/92bca01a-1fdc-48e8-8996-bf927fc25776/image.png)</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[20250813 - 학습일지 - js/css/html]]></title>
            <link>https://velog.io/@2025-pictoletter/20250813-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-jscsshtml</link>
            <guid>https://velog.io/@2025-pictoletter/20250813-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-jscsshtml</guid>
            <pubDate>Wed, 13 Aug 2025 09:50:04 GMT</pubDate>
            <description><![CDATA[<h3 id="javascript-개요">javascript 개요</h3>
<p><img src="https://velog.velcdn.com/images/2025-pictoletter/post/463ddb89-a6db-43ad-aadf-ed68d8a1a912/image.png" alt=""></p>
<ul>
<li>The var keyword was used in all JavaScript code from 1995 to 2015.</li>
<li>The let and const keywords were added to JavaScript in 2015.</li>
<li>The var keyword should only be used in code written for older browsers.</li>
</ul>
<h3 id="javascript-identifiers--names">JavaScript Identifiers / Names</h3>
<p>Identifiers are JavaScript names.
Identifiers are used to name variables and keywords, and functions.
The rules for legal names are the same in most programming languages.
A JavaScript name must begin with:</p>
<ul>
<li>letter (A-Z or a-z)</li>
<li>A dollar sign ($)</li>
<li>Or an underscore (_)
Subsequent characters may be letters, digits, underscores, or dollar signs.<h4 id="javascript-uses-the-unicode-character-set">JavaScript uses the Unicode character set.</h4>
<h4 id="constant-array">Constant Array</h4>
<pre><code>// You can create a constant array:
const cars = [&quot;Saab&quot;, &quot;Volvo&quot;, &quot;BMW&quot;];
</code></pre></li>
</ul>
<p>// You can change an element:
cars[0] = &quot;Toyota&quot;;</p>
<p>// You can add an element:
cars.push(&quot;Audi&quot;);</p>
<p>const cars = [&quot;Saab&quot;, &quot;Volvo&quot;, &quot;BMW&quot;];
cars = [&quot;Toyota&quot;, &quot;Volvo&quot;, &quot;Audi&quot;];    // ERROR</p>
<pre><code>#### document.getElementById</code></pre><!DOCTYPE html>
<html>
<head>
<script>
function myFunction() {
  const demo = document.getElementById("demo");
  demo.innerHTML = (demo.innerHTML === "Paragraph changed.") ? "Try it." : "Paragraph changed.";
}

<p></script></p>
</head>
<body>

<h2>Demo JavaScript in Head</h2>

<p id="demo">A Paragraph.</p>

<p><button type="button" onclick="myFunction()">Try it</button></p>
</body>
</html> 
```
유사참조 : document.getElementById("demo").innerText = "Hello World";

<h3 id="using-consolelog">Using console.log()</h3>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;

&lt;script&gt;
console.log(5 + 6);
&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre><h2 id="css">CSS</h2>
<pre><code>&lt;section&gt;
  &lt;div&gt;1. hello&lt;/div&gt;
  &lt;div&gt;2. hello&lt;/div&gt;
  &lt;nav&gt;n. hello&lt;/nav&gt;
  &lt;div&gt;3. hello&lt;/div&gt;
  &lt;div&gt;4. hello&lt;/div&gt;
  &lt;article&gt;a. hello&lt;/article&gt;
  &lt;div&gt;5. hello&lt;/div&gt;
  &lt;div&gt;6. hello&lt;/div&gt;
&lt;/section&gt;


section, div{
  background-color: yellowgreen;
  display:inline-block;
}
section &gt; div:nth-child(1){
  color:blue;
}
section &gt; div:nth-of-child(2){
  background-color:blue;
}
section &gt; div:last-child{
  color:green;
}</code></pre><h4 id="개념---block-요소-좌측가운데우측-정렬하는-방법">개념 - block 요소 좌측,가운데,우측 정렬하는 방법</h4>
<pre><code>div {
    width:100px;
    height:100px;
    background-color:red;
}
div:nth-child(2) {
    margin-left:auto;
    margin-right:auto;
}
div:nth-child(3) {
    margin-left:auto;    
}</code></pre><pre><code>div {
    width:100px;
    height:100px;
    margin-left:0; /* 이 코드는 사실 쓸 필요는 없다. 왜냐하면 원래 0 이니까 */
    background-color:red;
}

div:nth-child(2) {
    background-color:gold;
    margin-left:auto;
    margin-right:auto;
}

div:nth-child(3) {
    background-color:green;
    margin-right:0;
    margin-left:auto;
}

section {
    border:5px dotted gray;
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[20250811 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250811-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250811-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Mon, 11 Aug 2025 09:49:58 GMT</pubDate>
            <description><![CDATA[<p>jpa specification : 동적 검색조건을 생성하는 인터페이스</p>
<pre><code>    private Specification&lt;Question&gt; search(String kw) {
        return new Specification&lt;&gt;() {
            private static final long serialVersionUID = 1L;
            @Override
            public Predicate toPredicate(Root&lt;Question&gt; q, CriteriaQuery&lt;?&gt; query, CriteriaBuilder cb) {
                query.distinct(true);  // 중복을 제거
                Join&lt;Question, SiteUser&gt; u1 = q.join(&quot;author&quot;, JoinType.LEFT);
                Join&lt;Question, Answer&gt; a = q.join(&quot;answerList&quot;, JoinType.LEFT);
                Join&lt;Answer, SiteUser&gt; u2 = a.join(&quot;author&quot;, JoinType.LEFT);
                return cb.or(cb.like(q.get(&quot;subject&quot;), &quot;%&quot; + kw + &quot;%&quot;), // 제목
                        cb.like(q.get(&quot;content&quot;), &quot;%&quot; + kw + &quot;%&quot;),      // 내용
                        cb.like(u1.get(&quot;username&quot;), &quot;%&quot; + kw + &quot;%&quot;),    // 질문 작성자
                        cb.like(a.get(&quot;content&quot;), &quot;%&quot; + kw + &quot;%&quot;),      // 답변 내용
                        cb.like(u2.get(&quot;username&quot;), &quot;%&quot; + kw + &quot;%&quot;));   // 답변 작성자
            }
        };
    }</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[20250808 - retrun viewName with binding]]></title>
            <link>https://velog.io/@2025-pictoletter/20250808-retrun-viewName-with-binding</link>
            <guid>https://velog.io/@2025-pictoletter/20250808-retrun-viewName-with-binding</guid>
            <pubDate>Fri, 08 Aug 2025 09:47:15 GMT</pubDate>
            <description><![CDATA[<p>좋은 질문이에요! <code>return &quot;question_form&quot;</code>처럼 뷰를 다시 렌더링할 때 <strong>사용자가 입력한 값이 그대로 남아 있는 이유</strong>는 주로 다음과 같은 <strong>Spring MVC의 동작 방식과 개발자의 코드 처리</strong> 덕분입니다.</p>
<hr>
<h3 id="🧠-왜-입력값이-유지될까">🧠 왜 입력값이 유지될까?</h3>
<p>입력값이 유지되는 이유는 보통 다음 두 가지 방식 중 하나로 처리되기 때문입니다:</p>
<hr>
<h3 id="1️⃣-model에-사용자-입력값을-다시-담아서-뷰에-전달">1️⃣ <strong>Model에 사용자 입력값을 다시 담아서 뷰에 전달</strong></h3>
<p>예를 들어, 사용자가 폼을 제출했는데 권한 문제나 내부 오류로 인해 등록이 실패한 경우, 컨트롤러에서 다음과 같이 처리합니다:</p>
<pre><code class="language-java">@PostMapping(&quot;/question/create&quot;)
public String createQuestion(@ModelAttribute QuestionForm questionForm, Model model) {
    if (권한없음 || 내부오류) {
        model.addAttribute(&quot;questionForm&quot;, questionForm); // 사용자가 입력한 값 유지
        model.addAttribute(&quot;errorMessage&quot;, &quot;권한이 없습니다.&quot;);
        return &quot;question_form&quot;; // 입력값을 포함한 뷰 반환
    }

    // 정상 처리 로직...
}</code></pre>
<ul>
<li><code>@ModelAttribute</code>로 받은 <code>questionForm</code> 객체는 사용자의 입력값을 담고 있음</li>
<li>이를 다시 <code>model.addAttribute()</code>로 뷰에 넘기면, 템플릿에서 해당 값을 그대로 출력 가능</li>
</ul>
<hr>
<h3 id="2️⃣-thymeleaf나-jsp에서-폼-필드에-바인딩된-값-출력">2️⃣ <strong>Thymeleaf나 JSP에서 폼 필드에 바인딩된 값 출력</strong></h3>
<p>템플릿에서 다음과 같이 작성되어 있다면, <code>questionForm</code>의 필드 값이 자동으로 채워집니다:</p>
<pre><code class="language-html">&lt;input type=&quot;text&quot; name=&quot;title&quot; th:value=&quot;${questionForm.title}&quot; /&gt;
&lt;textarea name=&quot;content&quot; th:text=&quot;${questionForm.content}&quot;&gt;&lt;/textarea&gt;</code></pre>
<ul>
<li><code>th:value</code>나 <code>th:text</code>는 모델에 담긴 객체의 값을 그대로 출력</li>
<li>따라서 사용자가 입력한 값이 사라지지 않고 유지됨</li>
</ul>
<hr>
<h3 id="💡-추가-팁-bindingresult를-활용한-검증-오류-처리">💡 추가 팁: BindingResult를 활용한 검증 오류 처리</h3>
<p>입력값 검증 실패 시에도 입력값을 유지하려면 <code>BindingResult</code>를 함께 사용합니다:</p>
<pre><code class="language-java">@PostMapping(&quot;/question/create&quot;)
public String createQuestion(@Valid QuestionForm questionForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return &quot;question_form&quot;; // 검증 실패 시 입력값 유지
    }

    // 등록 처리...
}</code></pre>
<p>Spring은 <code>BindingResult</code>가 오류를 포함하고 있을 경우, 자동으로 입력값을 다시 뷰에 전달해줍니다.</p>
<hr>
<h3 id="✅-요약">✅ 요약</h3>
<table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>Model</code>에 입력값을 다시 담음</td>
<td>컨트롤러에서 사용자 입력값을 뷰로 전달</td>
</tr>
<tr>
<td>템플릿에서 값 바인딩</td>
<td><code>${questionForm.title}</code> 등으로 입력값 출력</td>
</tr>
<tr>
<td><code>BindingResult</code> 사용</td>
<td>검증 오류 시 자동으로 값 유지</td>
</tr>
</tbody></table>
<hr>
<p>혹시 특정 상황에서 값이 사라지는 문제가 있다면, 코드 예시를 보여주시면 함께 분석해볼 수도 있어요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250808 - 학습일지 : Annotation]]></title>
            <link>https://velog.io/@2025-pictoletter/20250808-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-Annotation</link>
            <guid>https://velog.io/@2025-pictoletter/20250808-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-Annotation</guid>
            <pubDate>Fri, 08 Aug 2025 05:01:13 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Annotation 을 &quot;주석&quot;이라고 번역하기에는 문제가 있음
TTA에서는 &quot;원시데이터를 보강하는 프로세스. 정적 또는 동적 객체, 수행 작업, 환경조건과 같은 원시데이터를 설명하는 메타데이터를 포함&quot;이라고 설명하고 있으나 조금 많이 부족한 설명을 참조한 것 같음(출처 : 단체표준 TTAK.KO-10.1498-Part1 자율주행 AI 학습용 데이터 참조 모델 - 제1부: 일반 요구사항)</p>
</blockquote>
<h3 id="1-requiredargsconstructor-lombok">1. @RequiredArgsConstructor (Lombok)</h3>
<ul>
<li>Lombok에서 제공하는 어노테이션 </li>
<li>클래스의 final 필드 또는 @NonNull 필드만을 매개변수로 받는 생성자를 자동으로 생성</li>
<li>불변 객체 설계와 Spring의 생성자 기반 의존성 주입에 매우 유용(먼 개뼉다구 같은 소리야).</li>
<li>적용대상<ul>
<li>final 필드</li>
<li>@NonNull이 붙은 필드
즉, 필수로 초기화되어야 하는 필드들만 생성자의 매개변수로 포함.<pre><code>@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private String optionalField;
}
// 위 코드는 다음과 같은 생성자를 자동으로 생성한다.
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}</code></pre></li>
<li>@Null 필드 포함<pre><code>@RequiredArgsConstructor
public class EmailService {
@NonNull
private String smtpServer;
private String senderName;
}
</code></pre></li>
</ul>
</li>
</ul>
<p>public EmailService(String smtpServer) {
    if (smtpServer == null) {
        throw new NullPointerException(&quot;smtpServer is marked non-null but is null&quot;);
    }
    this.smtpServer = smtpServer;
}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250807 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250807-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250807-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Thu, 07 Aug 2025 09:33:01 GMT</pubDate>
            <description><![CDATA[<h3 id="form-방식-로그인과-로그아웃을-위한-securityconfigjava-등록">form 방식 로그인과 로그아웃을 위한 SecurityConfig.java 등록</h3>
<h4 id="spring-security-httpformlogin-메소드를-이용한-설정">Spring Security http.formLogin() 메소드를 이용한 설정</h4>
<pre><code> @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorizeHttpRequests) -&gt; authorizeHttpRequests
                        .requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
                .csrf((csrf) -&gt; csrf
                        .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
                .headers((headers) -&gt; headers
                        .addHeaderWriter(new XFrameOptionsHeaderWriter(
                                XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
                .formLogin((formLogin) -&gt; formLogin
                        .loginPage(&quot;/user/login&quot;)
                        .defaultSuccessUrl(&quot;/&quot;))
                .logout((logout) -&gt; logout
                        .logoutRequestMatcher(new AntPathRequestMatcher(&quot;/user/logout&quot;))
                        .logoutSuccessUrl(&quot;/&quot;)
                        .invalidateHttpSession(true))
        ;
        return http.build();
    }

    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }</code></pre><ul>
<li>람다식 미적용<pre><code>http.formLogin()
  .loginPage(&quot;/user/login&quot;)    //
  .defaultSuccessUrl(&quot;/&quot;);</code></pre></li>
<li>람다식 적용    <pre><code>http.formLogin(formLogin -&gt; 
  formLogin
  .loginPage(&quot;/user/login&quot;)               // 커스텀 로그인 페이지
  .loginProcessingUrl(&quot;/user/doLogin&quot;)    // 로그인 처리 URL
  .usernameParameter(&quot;email&quot;)             // 사용자명 파라미터 이름 변경
  .passwordParameter(&quot;passwd&quot;)            // 비밀번호 파라미터 이름 변경
  .defaultSuccessUrl(&quot;/dashboard&quot;)        // 로그인 성공 시 이동 경로
  .failureUrl(&quot;/user/login?error=true&quot;)   // 로그인 실패 시 이동 경로
);</code></pre></li>
<li><ul>
<li>안 쓰는 경우?**</li>
</ul>
</li>
<li>OAuth2 로그인 (http.oauth2Login())</li>
<li>JWT 기반 인증 (필터 커스터마이징)</li>
<li>API 서버에서 토큰 기반 인증만 사용하는 경우</li>
</ul>
<h4 id="spring-security-httpformlogout-메소드를-이용한-설정">Spring Security http.formLogout() 메소드를 이용한 설정</h4>
<ul>
<li>logoutRequestMatcher : 로그아웃을 처리할 URL을 지정합니다. 기본은 POST /logout이지만, 여기서는 GET /user/logout도 허용하기 위해 AntPathRequestMatcher를 사용</li>
<li>logoutSuccessUrl(&quot;/&quot;) : 로그아웃 성공 후 리다이렉트할 경로입니다. 보통 홈이나 로그인 페이지로 설정</li>
<li>invalidateHttpSession(true) : 로그아웃 시 현재 HTTP 세션을 무효화합니다. 즉, 세션에 저장된 사용자 정보나 인증 정보가 모두 삭제.<pre><code>.logout((logout) -&gt; logout
  .logoutRequestMatcher(new AntPathRequestMatcher(&quot;/user/logout&quot;)) // get/post 방식 로그아웃 처리
  .logoutSuccessUrl(&quot;/&quot;)       //로그아웃 처리 후 url
  .invalidateHttpSession(true) // 로그아웃 후 세션 파기
  .deleteCookies(&quot;JSESSIONID&quot;)  // 쿠키 내 세션정보 파기
)</code></pre><h3 id="bean-생성">@Bean 생성</h3>
</li>
<li>Spring에서 @Bean을 사용하는 이유는 객체를 직접 생성하지 않고, Spring이 대신 관리하도록 하기 위해서입니다. 이걸 <strong>&quot;의존성 주입(DI, Dependency Injection)&quot;</strong>이라고 함.</li>
<li>관련 학습사항 : @Component, @Service, @Configuration 등</li>
</ul>
<h3 id="idpassword-불일치-처리">id/password 불일치 처리</h3>
<pre><code>        &lt;div th:if=&quot;${param.error}&quot;&gt;
            &lt;div class=&quot;alert alert-danger&quot;&gt;
                사용자ID 또는 비밀번호를 확인해 주세요.
            &lt;/div&gt;
        &lt;/div&gt;</code></pre><ul>
<li>로그인 폼 양식과 같이 출력하는 경우. login?error 형식의 파라미디터 값이 전달되는 경우에만 화면출력됨. <h3 id="로그인아웃-상태-메뉴바-토글적용">로그인/아웃 상태 메뉴바 토글적용</h3>
<pre><code>      &lt;li class=&quot;nav-item&quot;&gt;
          &lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAnonymous()&quot; th:href=&quot;@{/user/login}&quot;&gt;로그인&lt;/a&gt;
          &lt;a class=&quot;nav-link&quot; sec:authorize=&quot;isAuthenticated()&quot; th:href=&quot;@{/user/logout}&quot;&gt;로그아웃&lt;/a&gt;
      &lt;/li&gt;</code></pre></li>
<li>Thymeleaf + Spring Security를 함께 사용할 때 로그인/로그아웃 링크를 사용자 ROLE 인증 상태에 따라 조건부로 보여주는 방식.<h3 id="securityconfigjava---보안설정과-db조회">SecurityConfig.java - 보안설정과 DB조회</h3>
<h4 id="user-repository-내-사용자-정보-찾기-기능-추가">User Repository 내 사용자 정보 찾기 기능 추가</h4>
<pre><code>package com.mysite.sbb.user;
</code></pre></li>
</ul>
<p>import java.util.Optional;</p>
<p>import org.springframework.data.jpa.repository.JpaRepository;</p>
<p>public interface UserRepository extends JpaRepository&lt;SiteUser, Long&gt; {
    Optional<SiteUser> findByusername(String username);
}</p>
<pre><code>- findByusername 메소드를 추가하여 SiteUser 정보 확인

#### com.mysite.sbb.user 패키지에 UserRole.java를 추가하여 관리자 유형관리
</code></pre><p>package com.mysite.sbb.user;</p>
<p>import lombok.Getter;</p>
<p>@Getter
public enum UserRole {
    ADMIN(&quot;ROLE_ADMIN&quot;),
    USER(&quot;ROLE_USER&quot;);</p>
<pre><code>UserRole(String value) {
    this.value = value;
}

private String value;</code></pre><p>}</p>
<pre><code>enum 열거 자료형을 이용하여 ADMIN, USRER 라는 상수를 등록하고 각각의 값을 부여. 이때 ROLE_ 이라는 접두사를 반드시 적용. @Setter가 없는 이유는 상수 선언이기 때문

#### UserSecurityService 서비스 생성 </code></pre><p>package com.mysite.sbb.user;</p>
<p>import java.util.ArrayList;
import java.util.List;
import java.util.Optional;</p>
<p>import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;</p>
<p>import lombok.RequiredArgsConstructor;</p>
<p>@RequiredArgsConstructor
@Service
public class UserSecurityService implements UserDetailsService {</p>
<pre><code>private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional&lt;SiteUser&gt; _siteUser = this.userRepository.findByusername(username);
    if (_siteUser.isEmpty()) {
        throw new UsernameNotFoundException(&quot;사용자를 찾을수 없습니다.&quot;);
    }
    SiteUser siteUser = _siteUser.get();
    List&lt;GrantedAuthority&gt; authorities = new ArrayList&lt;&gt;();
    if (&quot;admin&quot;.equals(username)) {
        authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
    } else {
        authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
    }
    return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);
}</code></pre><p>}</p>
<pre><code>- @Override 적용 이유는 기존 메소드를 수정 사용하기 위해 적용
-  스프링 시큐리티가 로그인 시 사용할 UserSecurityService는 스프링 시큐리티가 제공하는 UserDetailsService 인터페이스를 구현(implements)해야 한다. 스프링 시큐리티의 UserDetailsService는 loadUserByUsername 메서드를 구현하도록 강제하는 인터페이스이다. loadUserByUsername 메서드는 사용자명(username)으로 스프링 시큐리티의 사용자(User) 객체를 조회하여 리턴하는 메서드. 사용자정보를 DB에서 조회한 이후 반환하는 기본값은 &quot;UserDetails&quot; 객체로서 Spring Security가 인증에 필요한 정보를 가지고 있음
![](https://velog.velcdn.com/images/2025-pictoletter/post/979cde7e-ae0b-4898-bc28-bc795f9509b7/image.png)</code></pre><p>// 일반적 적용단계
@Service // 서비스계층임을 명시하면서 Spring이 자동으로 Bean에 등록하고 의존성 주입 대상으로 등록하기 위해 사용
public class CustomUserDetailsService implements UserDetailsService {</p>
<pre><code>private final UserRepository userRepository;

public CustomUserDetailsService(UserRepository userRepository) {
    this.userRepository = userRepository;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUsername(username)
        .orElseThrow(() -&gt; new UsernameNotFoundException(&quot;사용자를 찾을 수 없습니다: &quot; + username));

    return new org.springframework.security.core.userdetails.User(
        user.getUsername(),
        user.getPassword(),
        getAuthorities(user)
    );
}

private Collection&lt;? extends GrantedAuthority&gt; getAuthorities(User user) {
    return List.of(new SimpleGrantedAuthority(&quot;ROLE_&quot; + user.getRole()));
}</code></pre><p>}
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250805 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250805-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-ffb5ktdq</link>
            <guid>https://velog.io/@2025-pictoletter/20250805-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-ffb5ktdq</guid>
            <pubDate>Tue, 05 Aug 2025 09:48:37 GMT</pubDate>
            <description><![CDATA[<p>레포지토리(Repository)는 일반적으로 데이터 접근 계층에서 사용되는 인터페이스로, 데이터베이스와 상호작용하며 CRUD(Create, Read, Update, Delete) 연산을 수행합니다. Spring Data JPA를 사용하면 JpaRepository 인터페이스를 상속받아 기본적인 CRUD 메소드들을 자동으로 사용할 수 있습니다. 추가적으로 메소드 이름만으로 쿼리를 생성하거나, @Query 어노테이션을 사용하여 직접 쿼리를 정의할 수도 있습니다. </p>
<h4 id="spring-data-jpa-기본-제공-메소드">Spring Data JPA 기본 제공 메소드:</h4>
<p>save(entity): 엔티티 저장 또는 업데이트.
findById(id): ID로 엔티티 조회. 반환 타입은 Optional.
findAll(): 모든 엔티티 조회.
existsById(id): ID로 엔티티 존재 여부 확인.
deleteById(id): ID로 엔티티 삭제.
delete(entity): 엔티티 삭제.
deleteAll(): 모든 엔티티 삭제.
count(): 엔티티 개수 조회.</p>
<h4 id="메소드-이름으로-쿼리-생성">메소드 이름으로 쿼리 생성:</h4>
<p>find...By, read...By, query...By, count...By, get...By 등의 접두사와 조건절을 조합하여 메소드 이름을 정의하면, 스프링 데이터 JPA가 자동으로 해당 쿼리를 생성합니다. 예를 들어, <code>findByUsername(String username)</code>은 username으로 엔티티를 조회하는 쿼리를 생성합니다.</p>
<h4 id="query-어노테이션-사용">@Query 어노테이션 사용:</h4>
<p>@Query 어노테이션을 사용하여 JPQL 또는 네이티브 쿼리를 직접 정의할 수 있습니다. 예를 들어, 다음과 같이 사용할 수 있습니다.</p>
<pre><code>@Query(&quot;SELECT u FROM User u WHERE u.email = ?1&quot;)
User findByEmail(String email);</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[20250805 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250805-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250805-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Tue, 05 Aug 2025 06:44:13 GMT</pubDate>
            <description><![CDATA[<h2 id="스프링시큐리티-적용">스프링시큐리티 적용</h2>
<h4 id="의존성-추가">의존성 추가</h4>
<pre><code>(... 생략 ...)

dependencies {
    (... 생략 ...)
    implementation &#39;org.springframework.boot:spring-boot-starter-security&#39;
    implementation &#39;org.thymeleaf.extras:thymeleaf-extras-springsecurity6&#39;
}

(... 생략 ...)</code></pre><ul>
<li>스프링시큐리티와 스프링부츠 버젼에 맞추어 사용되어야 함</li>
</ul>
<h3 id="root-폴더-내에-securityconfigjava-추가">root 폴더 내에 SecurityConfig.java 추가</h3>
<ul>
<li>SecurityConfig.java를 추가하기 전에는 접근시 아래의 화면이 전시된다
<img src="https://velog.velcdn.com/images/2025-pictoletter/post/40324dc3-3229-4cbb-95f9-1d73e0f5c811/image.png" alt=""></li>
<li>이때 아래의 생성 비밀번호를 입력하여 접근가능하다.
<img src="https://velog.velcdn.com/images/2025-pictoletter/post/412feaba-888c-4690-aba6-90f8fc10fb00/image.png" alt=""></li>
</ul>
<pre><code>package com.mysite.sbb;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
// import org.springframework.*;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests((authorizeHttpRequests) -&gt; authorizeHttpRequests
                        .requestMatchers(new AntPathRequestMatcher(&quot;/**&quot;)).permitAll())
                .csrf((csrf) -&gt; csrf
                        .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))
        ;
        return http.build();
    }
}</code></pre><ul>
<li>AntPathRequestMatcher(&quot;/**&quot;).permitAll() 구문은 모든 root directory 에 대한 접근을 허가한 내용이다.<h4 id="csrfcross-site-request-forgery-검증">CSRF(Cross-Site Request Forgery) 검증.</h4>
<pre><code>.csrf((csrf) -&gt; csrf
                      .ignoringRequestMatchers(new AntPathRequestMatcher(&quot;/h2-console/**&quot;)))</code></pre><h4 id="x-frame-options는-웹-페이지가-iframe-frame-object-태그-안에서-렌더링될-수-있는지를-제어하는-http-응답-헤더입니다-주로-클릭재킹clickjacking-공격을-방지하기-위해-사용됩니다">X-Frame-Options는 웹 페이지가 iframe, frame, object 태그 안에서 렌더링될 수 있는지를 제어하는 HTTP 응답 헤더입니다. 주로 클릭재킹(clickjacking) 공격을 방지하기 위해 사용됩니다.</h4>
<pre><code>              .headers((headers) -&gt; headers
                      .addHeaderWriter(new XFrameOptionsHeaderWriter(
                              XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))</code></pre><img src="https://velog.velcdn.com/images/2025-pictoletter/post/90c3e288-63c1-40b8-b105-1fa32d66571e/image.png" alt=""><h4 id="언제-어떤-옵션을-써야-할까">언제 어떤 옵션을 써야 할까?</h4>
</li>
<li>공개 웹사이트: DENY 권장</li>
<li>내부 시스템에서 iframe 사용: SAMEORIGIN</li>
<li>특정 외부 도메인에서 iframe 허용: ALLOW-FROM은 비추천 → 대신 Content-Security-Policy: frame-ancestors 사용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250801 - 학습일지 - 질문등록화면 만들기]]></title>
            <link>https://velog.io/@2025-pictoletter/20250801-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-%EC%A7%88%EB%AC%B8%EB%93%B1%EB%A1%9D%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@2025-pictoletter/20250801-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-%EC%A7%88%EB%AC%B8%EB%93%B1%EB%A1%9D%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 05 Aug 2025 02:22:09 GMT</pubDate>
            <description><![CDATA[<h4 id="question_listhtml에서-질문등록하기-버튼추가">question_list.html에서 질문등록하기 버튼추가</h4>
<pre><code>&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
    &lt;table class=&quot;table&quot;&gt;
        (... 생략 ...)
    &lt;/table&gt;
    &lt;a th:href=&quot;@{/question/create}&quot; class=&quot;btn btn-primary&quot;&gt;질문 등록하기&lt;/a&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre><h4 id="questioncontroller-클래스에-questioncreate-mapping-추가">QuestionController 클래스에 /question/create Mapping 추가</h4>
<pre><code>public class QuestionController {
    (... 생략 ...)
    @GetMapping(&quot;/create&quot;)
    public String questionCreate() {
        return &quot;question_form&quot;;
    }
}</code></pre><ul>
<li><a href="/question/create" class="btn btn-primary">질문 등록하기</a>은 get method를 수신하기 때문에 @GetMapping(&quot;/create&quot;)를 선언하고 return &quot;question_form&quot;; 을 통해 템플릿을 출력한다.
= 현재 <a href="http://localhost:8080/question/create">http://localhost:8080/question/create</a> 상태임<h4 id="question-등록-양식-만들기">Question 등록 양식 만들기</h4>
</li>
<li>templates 폴더에 question_form,html을 만들어 아래와 같이 등록<pre><code>&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container&quot;&gt;
  &lt;h5 class=&quot;my-3 border-bottom pb-2&quot;&gt;질문등록&lt;/h5&gt;
  &lt;form th:action=&quot;@{/question/create}&quot; method=&quot;post&quot;&gt;
      &lt;div class=&quot;mb-3&quot;&gt;
          &lt;label for=&quot;subject&quot; class=&quot;form-label&quot;&gt;제목&lt;/label&gt;
          &lt;input type=&quot;text&quot; name=&quot;subject&quot; id=&quot;subject&quot; class=&quot;form-control&quot;&gt;
      &lt;/div&gt;
      &lt;div class=&quot;mb-3&quot;&gt;
          &lt;label for=&quot;content&quot; class=&quot;form-label&quot;&gt;내용&lt;/label&gt;
          &lt;textarea name=&quot;content&quot; id=&quot;content&quot; class=&quot;form-control&quot; rows=&quot;10&quot;&gt;&lt;/textarea&gt;
      &lt;/div&gt;
      &lt;input type=&quot;submit&quot; value=&quot;저장하기&quot; class=&quot;btn btn-primary my-2&quot;&gt;
  &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre></li>
<li>layout.html 템플릿 내역 : &lt;th:block layout:fragment=&quot;content&quot;&gt;</th:block><pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;... 생략 ...&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;!-- 시작 - 기본 템플릿 안에 삽입될 내용 --&gt;
&lt;th:block layout:fragment=&quot;content&quot;&gt;&lt;/th:block&gt;
&lt;!-- 끝 - 기본 템플릿 안에 삽입될 내용 --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><h4 id="question-등록요청-처리--questioncontroller-수정">Question 등록요청 처리 : QuestionController 수정</h4>
</li>
<li><input type="submit" value="저장하기" > 버튼은 POST방식으로 데이터를 전송하기 때문에 이를 수신할 수 있도록 변경한다.<pre><code>package com.mysite.sbb.question;
</code></pre></li>
</ul>
<p>(... 생략 ...)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
(... 생략 ...)
public class QuestionController {</p>
<pre><code>(... 생략 ...)

@GetMapping(&quot;/create&quot;)
public String questionCreate() {
    return &quot;question_form&quot;;
}

@PostMapping(&quot;/create&quot;)
public String questionCreate(@RequestParam(value=&quot;subject&quot;) String subject, @RequestParam(value=&quot;content&quot;) String content) {

    this.questionService.create(subject, content); // 저장 
    return &quot;redirect:/question/list&quot;; // 질문 저장후 질문목록으로 이동
}</code></pre><p>}</p>
<pre><code>- POST와 REQUEST PARAMETER를 인식할 수있는 크래스를 IMPORT 하는 선언이 동시에 필요하다.
  - @GetMapping(&quot;/create&quot;)은 최초 입력화면을 전시하기 위함이고 @PostMapping(&quot;/create&quot;)은 데이터를 등록하기 위한 선언이다.
  - 질문 저장후 질문목록으로 이동하기 위해 return &quot;redirect:/question/list&quot;; 가 설정되어 있다.
- QuestionService 클래스의 create 메서드를 호출, subject, content를 저장할 수 있도록 처리 
- POST 방식으로 전송되는 입력결과를 처리하기 위해 선언된 questionCreate 메소드에는 subject, content 입력값을 매개변수로 선언되어 있다.

#### 수정결과 등록 : QuestionService.class </code></pre><p>(... 생략 ...)
import java.time.LocalDateTime;
(... 생략 ...)
public class QuestionService {</p>
<pre><code>(... 생략 ...)

public void create(String subject, String content) {
    Question q = new Question();
    q.setSubject(subject);
    q.setContent(content);
    q.setCreateDate(LocalDateTime.now());
    this.questionRepository.save(q);
}</code></pre><p>}</p>
<pre><code>- QuestionService 클래스에 subject, content를 매개변수로 받아 Jpa save() 메소드를 호출하여 DB에 저장가능

### form &amp; Spring Boot Validation
- build.grandle 내에 dependencies 추가</code></pre><p>(... 생략 ...)
dependencies {
    (... 생략 ...)
    implementation &#39;org.springframework.boot:spring-boot-starter-validation&#39;
}
(... 생략 ...)</p>
<pre><code>- Spring Boot Validation 라이브러리 내 사용가능 내역
![](https://velog.velcdn.com/images/2025-pictoletter/post/fa192b79-e2ab-48e7-9392-4cd5a33e77fc/image.png)
#### QuestionForm.class 등록
- SprintBoot validation 기능을 사용하기 위해서는 정합성 검증 대상 form element가 class롤 정의되어야 한다. : QuestionForm.java</code></pre><p>package com.mysite.sbb.question;</p>
<p>import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;</p>
<p>import lombok.Getter;
import lombok.Setter;</p>
<p>@Getter
@Setter
public class QuestionForm {
    @NotEmpty(message=&quot;제목은 필수항목입니다.&quot;)
    @Size(max=200)
    private String subject;</p>
<pre><code>@NotEmpty(message=&quot;내용은 필수항목입니다.&quot;)
private String content;</code></pre><p>}</p>
<pre><code>- private String subject, content 상단에 선언된 @NotEmpty()는 필수항목을 표시함. @NotNull, @NotEmpty, @NotBlank을 구분하시오
- @Size(min, max, and more) 등을 제어할 수 있다.
사용예시 : @Size(min = 5, max = 20, message = &quot;Name must be between 5 and 20 characters&quot;)

#### QuestionController.java 
- form validation을 수행하기 위해서 단순히 파라미터 값만을 수신하는 부분을 변경하는 것에서 출발한다.</code></pre><p>(... 생략 ...)
import jakarta.validation.Valid;
import org.springframework.validation.BindingResult;
(... 생략 ...)
public class QuestionController {</p>
<pre><code>(... 생략 ...)

@PostMapping(&quot;/create&quot;)
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        return &quot;question_form&quot;;
    }
    this.questionService.create(questionForm.getSubject(), questionForm.getContent());
    return &quot;redirect:/question/list&quot;;
}</code></pre><p>}</p>
<pre><code>- 기존 QuestionControll 클래스의 메소드인 public String questionCreate의 파라미터 정의가 (@Valid QuestionForm questionForm, BindingResult bindingResult)으로 변경되어 있다.
- BindingResult 매개변수는 항상 @Valid 매개변수 바로 뒤에 위치해야 한다. 만약 두 매개변수의 위치가 정확하지 않다면 @Valid만 적용되어 입력값 검증 실패 시 400 오류가 발생한다.

#### question_form.html : validation 결과 반영

#### QuestionForm.java
#### 실제 검증 절차_텍스트_</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[20250801 - 학습일지 - Thymeleaf Page Layouts]]></title>
            <link>https://velog.io/@2025-pictoletter/20250801-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-Thymeleaf-Page-Layouts</link>
            <guid>https://velog.io/@2025-pictoletter/20250801-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80-Thymeleaf-Page-Layouts</guid>
            <pubDate>Fri, 01 Aug 2025 05:47:25 GMT</pubDate>
            <description><![CDATA[<h3 id="thymeleaf-page-layouts">Thymeleaf Page Layouts</h3>
<h4 id="layouthtml---content-body-layout-적용예시">layout.html - content body layout 적용예시</h4>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot;content=&quot;width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
    &lt;title&gt;Title&lt;/title&gt;
    &lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr&quot; crossorigin=&quot;anonymous&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;!-- 시작 - 기본 템플릿 안에 삽입될 내용 --&gt;
  &lt;th:block layout:fragment=&quot;content&quot;&gt;&lt;/th:block&gt;
  &lt;!-- 끝 - 기본 템플릿 안에 삽입될 내용 --&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><ul>
<li>&lt;th:block layout:fragment=&quot;content&quot;&gt;</th:block><h4 id="question_detailhtml">question_detail.html</h4>
<pre><code>&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
  &lt;!-- 질문 --&gt;
  &lt;h2 class=&quot;border-bottom py-2&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/h2&gt;
  &lt;div class=&quot;card my-3&quot;&gt;
      &lt;div class=&quot;card-body&quot;&gt;
          &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${question.content}&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
              &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
                  &lt;div th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
              &lt;/div&gt;
          &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- 답변의 갯수 표시 --&gt;
  &lt;h5 class=&quot;border-bottom my-3 py-2&quot;
      th:text=&quot;|${#lists.size(question.answerList)}개의 답변이 있습니다.|&quot;&gt;&lt;/h5&gt;
  &lt;!-- 답변 반복 시작 --&gt;
  &lt;div class=&quot;card my-3&quot; th:each=&quot;answer : ${question.answerList}&quot;&gt;
      &lt;div class=&quot;card-body&quot;&gt;
          &lt;div class=&quot;card-text&quot; style=&quot;white-space: pre-line;&quot; th:text=&quot;${answer.content}&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;d-flex justify-content-end&quot;&gt;
              &lt;div class=&quot;badge bg-light text-dark p-2 text-start&quot;&gt;
                  &lt;div th:text=&quot;${#temporals.format(answer.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/div&gt;
              &lt;/div&gt;
          &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
  &lt;!-- 답변 반복 끝  --&gt;
  &lt;!-- 답변 작성 --&gt;
  &lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; method=&quot;post&quot; class=&quot;my-3&quot;&gt;
      &lt;textarea name=&quot;content&quot; id=&quot;content&quot; rows=&quot;10&quot; class=&quot;form-control&quot;&gt;&lt;/textarea&gt;
      &lt;input type=&quot;submit&quot; value=&quot;답변등록&quot; class=&quot;btn btn-primary my-2&quot;&gt;
  &lt;/form&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre><h4 id="question_listhtml">question_list.html</h4>
<pre><code>&lt;html layout:decorate=&quot;~{layout}&quot;&gt;
&lt;div layout:fragment=&quot;content&quot; class=&quot;container my-3&quot;&gt;
  &lt;table class=&quot;table&quot;&gt;
      &lt;thead class=&quot;table-dark&quot;&gt;
      &lt;tr&gt;
          &lt;th&gt;번호&lt;/th&gt;
          &lt;th&gt;제목&lt;/th&gt;
          &lt;th&gt;작성일시&lt;/th&gt;
      &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
      &lt;tr th:each=&quot;question, loop : ${questionList}&quot;&gt;
          &lt;td th:text=&quot;${loop.count}&quot;&gt;&lt;/td&gt;
          &lt;td&gt;
              &lt;a th:href=&quot;@{|/question/detail/${question.id}|}&quot; th:text=&quot;${question.subject}&quot;&gt;&lt;/a&gt;
          &lt;/td&gt;
          &lt;td th:text=&quot;${#temporals.format(question.createDate, &#39;yyyy-MM-dd HH:mm&#39;)}&quot;&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;/tbody&gt;
  &lt;/table&gt;
&lt;/div&gt;
&lt;/html&gt;</code></pre></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250731 - SpringBootTest 재정리]]></title>
            <link>https://velog.io/@2025-pictoletter/20250731-SpringBootTest-%EC%9E%AC%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@2025-pictoletter/20250731-SpringBootTest-%EC%9E%AC%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 01 Aug 2025 05:18:10 GMT</pubDate>
            <description><![CDATA[<h3 id="1-springboottest-으로-시작">1. @SpringBootTest 으로 시작</h3>
<pre><code>@SpringBootTest
class SbbApplicationTests {

    @Autowired
    private QuestionRepository questionRepository; // 생성자 </code></pre><h3 id="2-testjpa">2. testJpa</h3>
<pre><code>    @Test
    void testJpa() {
        Question q1 = new Question(); // Question class 의 q1 인스턴스 생성
        q1.setSubject(&quot;sbb가 무엇인가요?&quot;);             // question entity내 신규 subject 값 지정
        q1.setContent(&quot;sbb에 대해서 알고 싶습니다.&quot;);  // question entity내 신규 content 값 지정
        q1.setCreateDate(LocalDateTime.now());       // question entity내 신규 createDate 값 지정
        this.questionRepository.save(q1);  //         insert into question entity

        Question q2 = new Question();
        q2.setSubject(&quot;스프링부트 모델 질문입니다.&quot;);
        q2.setContent(&quot;id는 자동으로 생성되나요?&quot;);
        q2.setCreateDate(LocalDateTime.now());
        this.questionRepository.save(q2);  // 두번째 질문 저장
        /*
            questionRepository는 JpaRepository를 상속받은 인터페이스로, 
            save() 메서드를 통해 Question 객체 q1을 데이터베이스에 저장합니다.

            JpaRepository는 Spring Data JPA에서 제공하는 인터페이스로, 
            기본적인 CRUD 메서드들을 이미 정의하고 있습니다.
        */    
    }
</code></pre><ul>
<li><p><strong>@SpringBootTest</strong>는 Spring Boot에서 통합 테스트를 할 때 사용하는 어노테이션.  테스트 클래스가 실제 애플리케이션처럼 동작하는 환경에서 실행
<strong>자주 함께 쓰는 어노테이션</strong></p>
<ul>
<li>@Transactional : 테스트 후 DB 롤백</li>
<li>@Test : JUnit 테스트 메서드</li>
<li>@Autowired : 테스트 대상 주입</li>
</ul>
</li>
<li><p><strong>@Autowired</strong> : pring 프레임워크에서 의존성(Dependency) 주입을 위해 사용하는 어노테이션. Spring이 관리하는 빈(Bean) 중에서 타입에 맞는 객체를 자동으로 찾아 등록.</p>
</li>
<li><p><strong>@Test :</strong> JUnit 프레임워크에서 테스트 메서드임을 표시하기 위해 사용되는 어노테이션. 선언된 다음 메서드는 테스트 실행 시 자동으로 호출되어 검증 로직 수행.</p>
</li>
</ul>
<h3 id="3-questionrepository-인터페이스">3. QuestionRepository 인터페이스</h3>
<pre><code>@Repository
public interface QuestionRepository extends JpaRepository&lt;Question, Integer&gt; {  // id 자료형을 선언
    Question findBySubject(String subject);
    Question findBySubjectAndContent(String subject, String content);
    List&lt;Question&gt; findBySubjectLike(String subject);
}</code></pre><ul>
<li><strong>@Repository</strong> 인터페이스가 Spring의 Repository 컴포넌트임을 선언. 생략해도 자동으로 인식되지만 명시적 등록권고.</li>
<li><strong>extends JpaRepository&lt;Question, Integer&gt;</strong> : Question 엔티티를 대상으로 하고, 기본 키(id)의 자료형은 Integer임을 명시.</li>
<li><strong>Question findBySubject(String subject)</strong> : 엔터티 내 subject 값이 정확히 일치하는 Question을 하나 반환.  </li>
<li>** Question findBySubjectAndContent(String subject, String content);** : subject와 content가 모두 일치하는 Question을 하나 반환</li>
<li><strong>List<Question> findBySubjectLike(String subject)</strong> : subject가 특정 패턴과 유사한 Question 목록을 반환합니다. 예: &quot;Spring%&quot; → &quot;Spring Boot&quot;, &quot;Spring Data&quot; 등</li>
</ul>
<h3 id="4-quesition-클래스">4. Quesition 클래스</h3>
<pre><code>      Question q1 = new Question();</code></pre><pre><code>@Entity // db
@Getter
@Setter
public class Question {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(length = 200)
    private String subject;

    @Column(columnDefinition = &quot;TEXT&quot;)
    private String content;

    private LocalDateTime createDate;

    // @OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE)
    @OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)
    private List&lt;Answer&gt; answerList;
}</code></pre><ul>
<li><strong>@Id</strong> : JPA에서 @Id는 엔티티의 기본 키(Primary Key) 필드를 지정하는 어노테이션입니다. 이 어노테이션이 붙은 필드는 데이터베이스 테이블의 PK 컬럼과 매핑.</li>
<li><strong>@GeneratedValue</strong>는 JPA에서 엔티티의 @Id 필드에 대한 키 생성 방식을 지정하는 어노테이션. <strong>strategy = GenerationType.IDENTITY</strong>를 사용하면 데이터베이스가 제공하는 자동 증가(Identity) 컬럼 기능을 통해 기본 키 값을 생</li>
<li><strong>@OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)</strong> <ul>
<li>@OneToMany는 RDB에서 1:다 FK 관계를 설정을 위한 선언임</li>
<li><strong>mappedBy = &quot;question&quot;</strong> 은 현재 entity.question 이 설정 대상, 다시 말해서 Answer 내에 question 컬럼이 존재하며, Answer.question은 Question.quesiont의 FK임을 명시. </li>
<li>하단에 위치한 private List<Answer> answerList; 선언에서 Answer 엔터티에 다수의 관계가 있음을 설명하고 있다. </li>
</ul>
</li>
</ul>
<h3 id="5-testjpa2">5. testJpa2()</h3>
<pre><code>    @Test
    void testJpa2() {
        List&lt;Question&gt; all = this.questionRepository.findAll();
        assertEquals(2, all.size());

        Question q = all.get(0);
        assertEquals(&quot;sbb가 무엇인가요?&quot;, q.getSubject());
    }</code></pre><ul>
<li><strong>@Test</strong> : Junit 테스트 선언</li>
<li>findAll()은 JpaRepository가 제공하는 기본 메서드. <strong>List<Question> all = this.questionRepository.findAll()</strong> 은 question entity에 모든 정보를 조회한다는 뜻</li>
<li>*<em>assertEquals(2, all.size()); *</em>조회된 Question 객체의 수가 2개인지 확인합니다. 아닌 경우 오류 발생.</li>
<li><strong>Question q = all.get(0);</strong> : Question 클래스 객체를 선언하여 db에서 가져온 객체 리스트에서 1 번째 값들을 부여</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250731 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250731-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250731-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Thu, 31 Jul 2025 09:49:36 GMT</pubDate>
            <description><![CDATA[<h3 id="static에-등록한-css-파일연결">static에 등록한 css 파일연결</h3>
<ul>
<li>staict 폴더에 등록한 경우<pre><code>&lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; th:href=&quot;@{/style.css}&quot;&gt;</code></pre></li>
<li>외부 링크를 이용한 경우<pre><code>&lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; integrity=&quot;sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr&quot; crossorigin=&quot;anonymous&quot;&gt;</code></pre></li>
</ul>
<h3 id="answer-등록">Answer 등록</h3>
<ul>
<li><p>AnswerController 클래스에서 POST Request를 수신하기 위해 RequestMapping, RequiredArgsConstructor, Controller annotation이 선언됨</p>
</li>
<li><p>자료 수신과 저장을 위한 questionService, answerService 객체 선언 </p>
<pre><code>@RequestMapping(&quot;/answer&quot;)
@RequiredArgsConstructor
@Controller
public class AnswerController {
  private final QuestionService questionService;
  private final AnswerService answerService;

  @PostMapping(&quot;/create/{id}&quot;)
  // @ResponseBody
  public String createAnswer(Model model, @PathVariable(&quot;id&quot;) Integer id, @RequestParam(value=&quot;content&quot;) String content) {
      Question question = this.questionService.getQuestion(id);
      this.answerService.create(question, content);
      return String.format(&quot;redirect:/question/detail/%s&quot;, id);

  }
}</code></pre></li>
<li><p>form action=&quot;/answer/create/3&quot; method=&quot;post&quot; 형식으로 정의된 client 로 부터 submit이 실행되면 createAnswer 메소드가 실행되면서 Model model 매개변수를 통해 컨트롤러에서 view 또는 service 전달할 데이터들을 객체화 함(스프링 MVC)</p>
</li>
<li><p>@PathVariable(&quot;id&quot;) Integer id 는 URL PATH로 정의 된 값(위 예시에 따르면 3)에 대한 정의</p>
</li>
<li><p>@RequestParam(value=&quot;content&quot;) String content 는 FORM DOM에서 선언된 POST.REQUEST에 포함된 객체 content를 처리하기 위해 매개변수를 정의 함. (textarea name=&quot;content&quot;)</p>
</li>
<li><p>Question question = this.questionService.getQuestion(id);  QuestionService 크래스에 선언된 public Question getQuestion(Integer id) 함수를 호출하여 question 인스턴스에 관련 값들을 db에서 가져옴</p>
</li>
<li><p>this.answerService.create(question, content); AnswerService 클래스 내에 선언된 create 메소드를 호출하여 그 결과를 db에 저장함 answerRepository.save(answer);</p>
</li>
<li><p>ToDolist.. should be continued... </p>
<pre><code>@RequiredArgsConstructor
@Service
public class AnswerService {
  private final AnswerRepository answerRepository;

  public void create(Question question, String content) {
      Answer answer = new Answer();
      answer.setContent(content);
      answer.setCreateDate(LocalDateTime.now());
      answer.setQuestion(question);
      this.answerRepository.save(answer);
  }
}</code></pre></li>
<li><h3 id="form--post">FORM : POST</h3>
<pre><code>&lt;form th:action=&quot;@{|/answer/create/${question.id}|}&quot; method=&quot;post&quot;&gt;
 &lt;textarea name=&quot;content&quot; id=&quot;content&quot; rows=&quot;15&quot;&gt;&lt;/textarea&gt;
 &lt;input type=&quot;submit&quot; value=&quot;답변등록&quot;&gt;
&lt;/form&gt;</code></pre></li>
<li><p>th:action은 Thymeleaf에서 form의 action 속성을 설정할 때 사용하는 속성. </p>
</li>
<li><p>일반적인 HTML에서는 action=&quot;/answer/create&quot;처럼 경로를 지정하지만 Thymeleaf에서는 동적으로 값을 넣어야 할 때 ${...} 문법을 사용.</p>
</li>
<li><p>@{...}는 URL을 생성하는 Thymeleaf 문법</p>
</li>
</ul>
<h3 id="url-perfix">URL Perfix</h3>
<pre><code>import org.springframework.web.bind.annotation.RequestMapping; // url prefix

@RequestMapping(&quot;/question&quot;) // url prefix
... 중량 ----
    //@GetMapping(&quot;/question/list&quot;)
    @GetMapping(&quot;/list&quot;)    // url perfix
    public String list(Model model) {
        List&lt;Question&gt; questionList = this.questionService.getList();
        model.addAttribute(&quot;questionList&quot;, questionList);
        return &quot;question_list&quot;;
    }

    //@GetMapping(&quot;/question/list&quot;)
    @GetMapping(&quot;/list&quot;)    // url perfix
    public String list(Model model) {
        List&lt;Question&gt; questionList = this.questionService.getList();
        model.addAttribute(&quot;questionList&quot;, questionList);
        return &quot;question_list&quot;;
    }    </code></pre><h3 id="questionservice">QuestionService</h3>
<h3 id="http-응답-코드">HTTP 응답 코드</h3>
<p>200 : 요청이 정상적으로 처리(SUCCESS)
300 : 요청이 REDIRECT 됨
400 : 클라이언트 요청에 문제가 있을 때
500 : 서버 사이트 문제 시
<strong>1xx (Informational):</strong>
Indicates that the server has received the request and is continuing the process. Examples include 100 Continue and 101 Switching Protocols. 
<strong>2xx (Successful):</strong>
Indicates that the request was successfully received, understood, and accepted. Examples include 200 OK, 201 Created, and 204 No Content.
<strong>3xx (Redirection):</strong>
Indicates that further action needs to be taken by the client to complete the request, usually involving a redirection to a different URL. Examples include 301 Moved Permanently and 302 Found.
<strong>4xx (Client Error):</strong>
Indicates that the client&#39;s request contains an error and cannot be fulfilled by the server. Examples include 400 Bad Request, 401 Unauthorized, 403 Forbidden, and 404 Not Found. 
<strong>5xx (Server Error):</strong>
Indicates that the server failed to fulfill a valid request due to an issue on the server&#39;s side. Examples include 500 Internal Server Error, 502 Bad Gateway, and 503 Service Unavailable.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250730 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250730-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250730-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Wed, 30 Jul 2025 09:49:42 GMT</pubDate>
            <description><![CDATA[<h4 id="templates을-적용하기-위한-controller-구현">Templates을 적용하기 위한 Controller 구현</h4>
<pre><code>@RequiredArgsConstructor
@Controller
public class QuestionController {
    private final QuestionRepository questionRepository; 

    @GetMapping(&quot;/question/list&quot;)
    public String list(Model model) { //org.springframework.ui.Model;
        List&lt;Question&gt; questionList = this.questionRepository.findAll();
        model.addAttribute(&quot;questionList&quot;, questionList);
        return &quot;question_list&quot;;
    }
}</code></pre><p><strong>@RequiredArgsConstructor :</strong> Lombok 라이브러리에서 제공하는 어노테이션.필수 인자를 받는 생성자를 자동으로 만들어주는 기능으로 하단의 private final QuestionRepository questionRepository; 설정에 관여하고 있다. 관련 변수/값을 생성하거나 가져오는 과정을 생략할 수 있다.
떠라서 클래스 내에 import lombok.RequiredArgsConstructor; 이 먼저 선언되어야 한다.
QuestionRepository class 파일에는 @Repository가 선언되어 있어야 하며 관련 변수 특성이 등록되어 있다.</p>
<p><strong>Model model :</strong>  Spring MVC에서 컨트롤러와 뷰 사이의 데이터를 전달하는 역할.</p>
<ul>
<li><p>Spring에서 제공하는 인터페이스.(import org.springframework.ui.Model;)</p>
</li>
<li><p>컨트롤러 → JSP(또는 템플릿 뷰) 로 데이터를 실어서 보내주는 바구니 같은 역할.</p>
</li>
<li><p>내부적으로 Map처럼 key-value 구조로 동작</p>
<pre><code>  List&lt;Question&gt; questionList = this.questionRepository.findAll();
  model.addAttribute(&quot;questionList&quot;, questionList);
  return &quot;question_list&quot;;</code></pre></li>
<li><p>클라이언트가 /question/list 요청</p>
</li>
<li><p>list() 메서드가 실행됨</p>
</li>
<li><p>questionRepository.findAll()로 데이터 조회</p>
</li>
<li><p>model.addAttribute()로 뷰에 데이터 전달</p>
</li>
<li><p>뷰 템플릿 question_list.html</p>
</li>
</ul>
<h4 id="templates-을-이용한-view-구현">Templates 을 이용한 view 구현</h4>
<pre><code>&lt;table class=&quot;table-1&quot; style=&quot;border:1px&quot; &gt;
    &lt;thead&gt;
    &lt;tr onclick=&quot;alert()&quot;&gt;
        &lt;th&gt;제목&lt;/th&gt;
        &lt;th&gt;내용&lt;/th&gt;
        &lt;th&gt;생성일시&lt;/th&gt;
    &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
    &lt;tr&gt;
        &lt;td&gt;
            [[${questionList[0].subject}]]
        &lt;/td&gt;
        &lt;td&gt;
            [[${questionList[0].content}]]
        &lt;/td&gt;
        &lt;td&gt;
            [[${questionList[0].createDate}]]
        &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td th:text=&quot;${questionList[0].subject}&quot;&gt;&lt;/td&gt;
        &lt;td th:text=&quot;${questionList[0].content}&quot;&gt;&lt;/td&gt;
        &lt;td th:text=&quot;${questionList[0].createDate}&quot;&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;table class=&quot;table-1&quot; style=&quot;border:1px&quot; &gt;
    &lt;thead&gt;
    &lt;tr onclick=&quot;alert()&quot;&gt;
        &lt;th&gt;ID&lt;/th&gt;
        &lt;th&gt;제목&lt;/th&gt;
        &lt;th&gt;내용&lt;/th&gt;
        &lt;th&gt;생성일시&lt;/th&gt;
    &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
    &lt;tr th:each=&quot;q : ${questionList}&quot;&gt;
        &lt;td th:text=&quot;${q.id}&quot;&gt;&lt;/td&gt;
        &lt;td th:text=&quot;${q.subject}&quot;&gt;&lt;/td&gt;
        &lt;td th:text=&quot;${q.content}&quot;&gt;&lt;/td&gt;
        &lt;td th:text=&quot;${q.createDate}&quot;&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;/tbody&gt;
&lt;/table&gt;</code></pre><h4 id="redirect-to-the-root-directory-with-springboot">REDIRECT TO THE ROOT DIRECTORY WITH SPRINGBOOT</h4>
<pre><code>    @GetMapping(&quot;/&quot;)
    public String root() {
        return &quot;redirect:/question/list&quot;;
    }</code></pre><p>@ GetMapping annotation을 이용하여 사용자가 Root directory 로 접근했을 때 [site url : localhost]/question/list 으로 포워딩하게 하고 @GetMapping(&quot;question/list&quot;)가 선언된 곳의 return 값을 실행하게 함. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250728 - JAP에서 데이터 로딩]]></title>
            <link>https://velog.io/@2025-pictoletter/20250728-JAP%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A1%9C%EB%94%A9</link>
            <guid>https://velog.io/@2025-pictoletter/20250728-JAP%EC%97%90%EC%84%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%A1%9C%EB%94%A9</guid>
            <pubDate>Mon, 28 Jul 2025 09:50:17 GMT</pubDate>
            <description><![CDATA[<p>JPA(Java Persisitence API 는 자바의 객체관계맵핑(ORM, Object-Relational Mapping) 기술표준을 정의하는 인터페이스 모음으로 DB에 관련된 CRUD를 메소드를 통해 객체지향적 관리를 하기 위해 고안된 것이다. 
_ * JAP(Java Application Platform)에서 DB 로딩 방식은 일반적으로 JDBC, JNDI(Java Naming and Directory Interface), DBCP(Database Connection Pool), 그리고 JPA를 통해 이루어진다._</p>
<p> 엔티티가 연관 관계를 맺은 다른 엔티티를 언제 조회할지 결정하는 전략이 바로 로딩(fetch) 방식입니다.</p>
<ul>
<li>EAGER: 대상 엔티티를 조회할 때 연관된 모든 엔티티를 즉시 함께 로드합니다.</li>
<li>LAZY: 실제로 연관 데이터를 사용할 때까지 로딩을 지연해 필요 시점에 조회합니다</li>
</ul>
<h4 id="jpa내-fetch-default">JPA내 fetch default</h4>
<p><img src="https://velog.velcdn.com/images/2025-pictoletter/post/d97d27ec-060c-4b3a-9289-ff54920dbf1e/image.png" alt="">
필요하다면 아래와 같이 명시적으로 변경할 수 있다.</p>
<pre><code>@OneToOne(fetch = FetchType.LAZY)
private Profile profile;

@ManyToOne(fetch = FetchType.EAGER)
private Team team;</code></pre><p>사용예시</p>
<pre><code>@Entity
public class Order {
  @Id @GeneratedValue
  private Long id;

  @OneToMany(mappedBy = &quot;order&quot;, fetch = FetchType.LAZY) // LAZY 또는 EAGER 지정
  private List&lt;OrderItem&gt; items = new ArrayList&lt;&gt;();
}

@Entity
public class OrderItem {
  @Id @GeneratedValue
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  private Order order;

  private String productName;
}</code></pre><pre><code>// 1) EAGER
List&lt;Order&gt; orders = em.createQuery(&quot;SELECT o FROM Order o&quot;, Order.class)
                       .getResultList();
</code></pre><p> Order → OrderItem 관계를 fetch = FetchType.EAGER로 설정했다고 상상 -- 예시를 다시 찾자</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20250728 - 학습일지]]></title>
            <link>https://velog.io/@2025-pictoletter/20250728-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</link>
            <guid>https://velog.io/@2025-pictoletter/20250728-%ED%95%99%EC%8A%B5%EC%9D%BC%EC%A7%80</guid>
            <pubDate>Mon, 28 Jul 2025 08:47:00 GMT</pubDate>
            <description><![CDATA[<h4 id="테스트-코드에서의-주의점">테스트 코드에서의 주의점</h4>
<p>@Transactional을 테스트 메서드에 직접 선언해야 하는 이유는 다음과 같습니다:</p>
<ul>
<li>테스트 클래스는 일반적으로 @SpringBootTest로 실행되며, 내부적으로 프록시 기반 AOP가 적용됩니다.</li>
<li>하지만 JUnit의 테스트 메서드는 프록시 대상이 아니기 때문에, 클래스에 @Transactional을 선언해도 메서드에 직접 선언하지 않으면 적용되지 않을 수 있습니다.</li>
<li>특히 @Transactional을 클래스에 선언하고 @Test 메서드에서 DB 접근을 할 경우, 트랜잭션이 제대로 적용되지 않아 LazyInitializationException이나 rollback이 되지 않는 문제가 발생할 수 있어요.</li>
<li>테스트에서 트랜잭션을 적용하고 싶다면 테스트 메서드에 직접 @Transactional을 붙이는 것이 안전</li>
</ul>
<p>또는 @OneToMany 선언된 곳을 아래와 같이 수정</p>
<pre><code>@OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE)
@OneToMany(mappedBy = &quot;question&quot;, cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)
</code></pre><h3 id="spring에서-말하는-proxy-mode는">Spring에서 말하는 Proxy Mode는</h3>
<p>AOP(Aspect-Oriented Programming)나 트랜잭션 처리 같은 기능을 적용하기 위해 프록시 객체를 생성하는 방식을 의미해요. 이 프록시는 실제 객체를 감싸서 메서드 호출을 가로채고, 필요한 부가 기능을 수행한 뒤 실제 메서드를 실행하게 됩니다. 
<img src="https://velog.velcdn.com/images/2025-pictoletter/post/4840d4b4-40f3-40f9-8797-d84cc19c7ded/image.png" alt=""></p>
<p><strong>Proxy Mode가 사용되는 대표적인 예</strong></p>
<ul>
<li>@Transactional: 트랜잭션을 자동으로 관리하기 위해 프록시를 생성</li>
<li>@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS): 스코프가 다른 빈을 주입할 때 프록시를 사용</li>
<li>@EnableAspectJAutoProxy(proxyTargetClass = true): AOP 적용 시 CGLIB 방식 강제</li>
</ul>
<p><strong>테스트 메서드에서의 프록시 한계</strong>
테스트 메서드는 일반적으로 프록시 객체로 감싸지지 않기 때문에, @Transactional 같은 어노테이션이 제대로 작동하지 않을 수 있어요. 그래서 테스트에서는 직접 메서드에 @Transactional을 붙여야 트랜잭션이 적용됩니다.</p>
<h3 id="🌱-spring이-questionrepository-타입의-빈bean을-찾아란">🌱 “Spring이 QuestionRepository 타입의 빈(bean)을 찾아...”란?</h3>
<p>스프링 프레임워크는 의존성 주입(Dependency Injection) 을 통해 필요한 객체들을 자동으로 주입해줘요. 이 과정에서 “빈(bean)”이라는 단어가 아주 중요한데요:</p>
<h4 id="빈bean이란">빈(bean)이란?</h4>
<ul>
<li><p>스프링 컨테이너(Spring Container) 가 관리하는 객체를 뜻합니다.</p>
</li>
<li><p>우리가 @Component, @Service, @Repository, @Controller 등의 어노테이션을 붙이면 해당 클래스는 빈으로 등록됩니다.</p>
</li>
<li><p>빈은 스프링이 생성하고 필요할 때 꺼내서 사용하는 “등록된 객체” 라고 볼 수 있어요.</p>
<h4 id="questionrepository는-어떻게-빈이-됐을까">QuestionRepository는 어떻게 빈이 됐을까</h4>
<pre><code>@Repository
public interface QuestionRepository extends JpaRepository&lt;Question, Integer&gt; { }</code></pre></li>
<li><p>@Repository 어노테이션 또는 JpaRepository를 확장했다는 사실 때문에 스프링 부트는 자동으로 이 인터페이스를 구현한 객체를 만들고 빈으로 등록해요. (@EnableJpaRepositories가 내장되어 있음)</p>
<h4 id="그래서-autowired는-뭘-하는가">그래서 @Autowired는 뭘 하는가</h4>
</li>
<li><p>이 코드가 실행되면 스프링은 컨테이너 안을 뒤져서 QuestionRepository라는 타입을 가진 빈이 있는지 확인합니다.</p>
</li>
<li><p>빈을 발견하면 해당 필드에 자동으로 주입해주는 거예요!
즉, 개발자가 new QuestionRepository()로 객체를 만들 필요 없이, 스프링이 알아서 등록된 인스턴스를 가져와 넣어준다는 뜻입니다.</p>
</li>
</ul>
<h3 id="questionanswer-repository">Question/Answer Repository</h3>
<pre><code>@Repository //- Tells Spring this interface is part of the persistence layer. jpa 규칙
/* JpaRepository&lt;Answer, Integer&gt;: Inherits CRUD functionality.
→ 기본적인 생성(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능 자동 제공.
- Answer: Entity class (엔티티 클래스)
- Integer: Primary key type (기본 키의 자료형)
* */
public interface AnswerRepository extends JpaRepository&lt;Answer, Integer&gt; {
}</code></pre><h4 id="autowired">@Autowired</h4>
<pre><code>@Autowired // 클래스 객체 생성 자동화
private QuestionRepository questionRepository;</code></pre><ul>
<li>Spring이 QuestionRepository 타입의 빈(bean)을 찾아 자동으로 주입해주는 어노테이션이에요.</li>
<li>즉, 직접 객체를 생성하지 않아도 Spring이 알아서 필요한 객체를 넣어줍니다.</li>
<li>생성자나 setter를 사용하지 않고도 멤버 변수에 바로 주입할 수 있어요.
→ 쉽게 말하면, &quot;Spring, 알아서 이 타입의 객체 좀 넣어줘!&quot; 하는 거예요. </li>
</ul>
<h3 id="리포지터리로-데이터베이스-관리하기-h2">리포지터리로 데이터베이스 관리하기 H2</h3>
<p>테스트 실행 → 실제 어플리케이션 실행, → H2 콘솔에서 확인하기 
<img src="https://velog.velcdn.com/images/2025-pictoletter/post/7addf9fe-afbd-4396-a175-9f9fca2122c2/image.png" alt="">
일련의 과정에서 로그 생성내역 확인 필요
<img src="https://velog.velcdn.com/images/2025-pictoletter/post/8e0a6ced-5223-48d0-a92c-c2f0cf00f415/image.png" alt="">
또한 jpd 설정에 update 옵션 적용여부 확인</p>
]]></description>
        </item>
    </channel>
</rss>