<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ee_ji0.log</title>
        <link>https://velog.io/</link>
        <description>개발과 성장의 여정을 기록합니다📝</description>
        <lastBuildDate>Thu, 09 Jan 2025 08:03:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ee_ji0.log</title>
            <url>https://velog.velcdn.com/images/ee_ji0/profile/36f739e0-44c3-46df-b583-4113a8acc09a/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ee_ji0.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ee_ji0" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[SteVe] OCPP 오픈소스 ]]></title>
            <link>https://velog.io/@ee_ji0/SteVe-OCPP-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4</link>
            <guid>https://velog.io/@ee_ji0/SteVe-OCPP-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4</guid>
            <pubDate>Thu, 09 Jan 2025 08:03:13 GMT</pubDate>
            <description><![CDATA[<p>오늘은 전기차 충전 네트워크의 똑똑한 관리자, <strong>SteVe</strong>라는 오픈 소스에 대해 알아보려고 합니다.</p>
<hr>
<h2 id="steve가-뭐야🤔">SteVe가 뭐야?🤔</h2>
<p><code>SteVe</code>는 독일에서 시작된 Java 기반의 OCPP(Open Charge Point Protocol) 서버 오픈 소스로, 전기차 충전기와 서버를 연결해주는 핵심 역할을 합니데이
쉽게 말하면, SteVe는 충전기와 통신하고, 데이터를 수집하고, 충전 과정을 관리하는 <strong>&quot;매니저&quot;</strong>입니다.</p>
<blockquote>
</blockquote>
<p>&quot;<code>OCPP</code>서버를 만들고 싶은데 어디서 시작해야 할지 모르겠어!😩&quot;
&quot;<code>SteVe</code>가 도와줄게요😉&quot;</p>
<hr>
<h2 id="steve의-주요-기능-🚀">SteVe의 주요 기능 🚀</h2>
<p>SteVe는 단순한 서버가 아닙니다. 다양한 기능으로 충전 네트워크를 효율적으로 관리할 수 있죠.</p>
 <ul>
  <li>
    <strong>OCPP 1.6 지원</strong><br />
    SteVe는 OCPP 1.6 표준을 완벽히 지원합니다. 덕분에 대부분의 현대 충전기와 호환이 가능하며, 안정적인 충전기-서버 통신 환경을 제공합니다.
    <ul>
      <li><strong>충전 세션 관리:</strong> 각 충전기의 세션 데이터를 효율적으로 관리할 수 있습니다.</li>
      <li><strong>원격 명령:</strong> 충전 시작/중지, 펌웨어 업데이트 등 다양한 원격 명령을 실행할 수 있습니다.</li>
    </ul>
  </li>

  <li>
    <strong>사용자 관리 기능</strong><br />
    관리자와 사용자 계정을 나누어, 충전 네트워크를 체계적으로 관리할 수 있는 기능을 제공합니다. 이를 통해 충전 기록과 세션 정보를 사용자 그룹별로 손쉽게 확인할 수 있습니다.
  </li>

  <li>
    <strong>웹 기반 UI</strong><br />
    충전기 상태, 세션 기록 등을 한눈에 파악할 수 있는 사용자 친화적인 웹 기반 대시보드를 제공합니다.
  </li>

  <li>
    <strong>오픈 소스의 자유로움</strong><br />
    SteVe의 코드는 모두 공개되어 있어, 필요에 따라 직접 수정하거나 새로운 기능을 추가할 수 있습니다. 커스터마이징이 중요한 환경에서 큰 장점이 됩니다.
  </li>
</ul>

<hr>
<h2 id="steve를-왜-써야-할까-🤷♀️">SteVe를 왜 써야 할까? 🤷‍♀️</h2>
<p>다른 상용 OCPP 서버 솔루션도 많은데, 왜 SteVe를 써야 할까요?<br>아래에서 SteVe의 매력을 살펴보세요!</p>
<hr>
<h2 id="💡-steve의-장점">💡 SteVe의 장점</h2>
<ul>
  <li>
    <strong>무료 & 오픈 소스</strong><br />
    SteVe는 100% 무료입니다. 비용 걱정 없이 프로토타입이나 테스트 환경을 구축할 수 있죠.
  </li>
  <li>
    <strong>확장 가능</strong><br />
    Java 기반으로 만들어졌기 때문에, 개발 경험이 있다면 원하는 대로 커스터마이징이 가능합니다.
  </li>
  <li>
    <strong>쉽게 설치 가능</strong><br />
    초기 설치가 비교적 간단해요. MySQL 데이터베이스와 함께 사용하면 곧바로 충전기를 연결할 수 있습니다.
  </li>
</ul>

<hr>
<h2 id="🛠️-steve-설치-가이드">🛠️ SteVe 설치 가이드</h2>
<p>SteVe를 직접 써보고 싶으신가요? 아래의 단계를 따라 설치해보세요!</p>
<h3 id="1-필요한-환경-준비하기">1. 필요한 환경 준비하기</h3>
<ul>
  <li>Java 11 이상</li>
  <li>MySQL 데이터베이스</li>
</ul>

<h3 id="2-steve-설치가이드">2. SteVe 설치가이드</h3>
<ol>
  <li>SteVe의 GitHub 페이지에서 코드를 가져옵니다.</li>
 <li>GitHub 저장소에서 스티브 소스 코드를 클론합니다. </li>
 <li>MySQL 데이터베이스를 생성하고, steve.properties 파일에서 데이터베이스 설정을 구성합니다. </li>
  <li>Maven을 사용하여 프로젝트를 빌드합니다</li>
<code>mvn clean install</code>


 <li>빌드된 .war 파일을 톰캣(Tomcat) 서버에 배포합니다. </li>
  <li>톰캣 서버를 시작하고, 웹 브라우저에서 스티브 웹 인터페이스에 접근합니다.</li>
</ol>





<h2 id="마무리-steve와-함께하는-충전의-미래-⚡">마무리: SteVe와 함께하는 충전의 미래 ⚡</h2>
<p>SteVe는 단순한 오픈 소스를 넘어, OCPP 서버 구축에 대한 훌륭한 가이드가 되어줍니다.
만약 전기차 충전 네트워크를 관리하거나, 새로운 기능을 실험해보고 싶다면 SteVe를 한 번 사용해보세요.</p>
<p>&quot;스티브와 함께 전기차 충전의 세계를 더 똑똑하고 유쾌하게 만들어봅시다!&quot; 🚗✨</p>
<p>👉 <a href="https://github.com/RWTH-i5-IDSG/steve">GitHub에서 SteVe 확인하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] StatelessWidget vs StatefulWidget]]></title>
            <link>https://velog.io/@ee_ji0/Flutter-StatelessWidget-vs-StatefulWidget</link>
            <guid>https://velog.io/@ee_ji0/Flutter-StatelessWidget-vs-StatefulWidget</guid>
            <pubDate>Tue, 07 Jan 2025 06:11:27 GMT</pubDate>
            <description><![CDATA[<p><code>Flutter</code>에서는 앱 화면을 구성하는 모든 것이 <code>Widget</code>입니다.</p>
<p><strong>이 위젯들은 크게 두 가지로 나눌 수 있습니다</strong>
<code>StatelessWidget</code>과 <code>StatefulWidget</code>
둘의 차이를 이해하면 더 나은 앱 개발이 가능해지니, 지금부터 예시와 함께 자세히 알아볼까요?</p>
<h2 id="1️⃣-statelesswidget">1️⃣ StatelessWidget</h2>
<p><code>StatelessWidget</code>은 말 그대로 <strong>&quot;상태가 없는 위젯&quot;</strong> 입니다.</p>
<blockquote>
<p>즉, 화면의 내용이 <code>고정</code>되어 있으며, 유저의 행동이나 데이터 변화에 따라 다시 그려질 필요가 없습니다.</p>
</blockquote>
<h3 id="💭-hello-flutter-라는-텍스트를-화면에-표시한다고-가정해보세요">💭 <strong>&quot;Hello, Flutter!&quot;</strong> 라는 텍스트를 화면에 표시한다고 가정해보세요.</h3>
<p>  이 텍스트는 사용자와의 상호작용과 상관없이 변하지 않는 고정된 내용입니다.</p>
<pre><code class="language-dart">
  import &#39;package:flutter/material.dart&#39;;

  class StatelesswidgetApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text(&#39;StatelessWidget 정적&#39;)),
          body: Center(
            child: Text(&#39;Hello, Flutter!&#39;), // 변하지 않는 텍스트
          ),
        ),
      );
    }
  }
</code></pre>
<h3 id="📱-실행-결과">📱 실행 결과</h3>
<p>  화면에** &quot;Hello, Flutter!&quot;** 라는 텍스트가 표시됩니다.
  유저가 버튼을 누르거나 화면을 스와이프해도 이 텍스트는 변하지 않습니다.</p>
<p>  이처럼 StatelessWidget은 고정된 UI를 표현할 때 사용됩니다.</p>
<h2 id="2️⃣-statefulwidget">2️⃣ StatefulWidget</h2>
<p>이제 <code>StatefulWidget</code>을 살펴봅시다.
 <code>StatefulWidget</code> 은 <strong>&quot;상태가 있는 위젯&quot;</strong> 으로,
사용자 상호작용이나 데이터 변화에 따라 화면의 내용이 동적으로 변경됩니다.</p>
<h3 id="💭-버튼을-누를-때마다-숫자가-증가하는-카운터-앱을-만들어봅시다">💭 버튼을 누를 때마다 숫자가 증가하는 카운터 앱을 만들어봅시다.</h3>
<pre><code class="language-dart">
import &#39;package:flutter/material.dart&#39;;

class StatefulwidgetApp extends StatefulWidget {
  @override
  _MyAppState createState() =&gt; _MyAppState();
}

class _MyAppState extends State&lt;StatefulwidgetApp&gt; {
  int _counter = 0; // 상태를 저장하는 변수

  void _incrementCounter() {
    setState(() {
      // 상태를 업데이트하고 화면 새로고침
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text(&#39;StatefulWidget 동적&#39;)),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(&#39;버튼를 눌러보세욤&#39;),
              Text(
                &#39;$_counter&#39;, // 상태에 따라 동적으로 변경되는 텍스트
                style: Theme.of(context).textTheme.headlineMedium,
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _incrementCounter, // 버튼 클릭 시 상태 업데이트
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

</code></pre>
<h3 id="📱-실행-결과-1">📱 실행 결과</h3>
<p>처음에는 화면 중앙에 0이라는 숫자가 표시됩니다.
유저가 화면 하단의 + 버튼을 누를 때마다 숫자가 1씩 증가합니다.
이처럼 <code>StatefulWidget</code>은 <strong>동적인 데이터와 UI</strong>를 처리할 때 사용됩니다.</p>
<h2 id="3️⃣-statelesswidget-vs-statefulwidget">3️⃣ StatelessWidget vs StatefulWidget</h2>
<table>
<thead>
<tr>
<th><strong>특징</strong></th>
<th><strong>StatelessWidget</strong></th>
<th><strong>StatefulWidget</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>상태 변화</strong></td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td><strong>화면 새로고침 필요 여부</strong></td>
<td>없음</td>
<td>필요</td>
</tr>
<tr>
<td><strong>사용 사례</strong></td>
<td>고정된 UI (예: 텍스트, 아이콘 등)</td>
<td>동적인 UI (예: 카운터, 애니메이션 등)</td>
</tr>
</tbody></table>
<p><code>StatelessWidget</code>은 <strong>&quot;액자 속 사진&quot;</strong>과 같습니다.
한 번 넣은 사진은 변하지 않죠.</p>
<p><code>StatefulWidget</code>은 <strong>&quot;디지털 액자&quot;</strong>와 같습니다.
버튼을 누르면 사진이 바뀌는 것처럼, 상태에 따라 화면이 변화합니다.</p>
<h2 id="결론-🎯">결론 🎯</h2>
<p><code>Flutter</code>에서 앱을 개발할 때 <strong>화면이 고정적</strong>인지, 아니면 <strong>동적</strong>으로 변하는지를 먼저 판단하세요.</p>
<p><strong>🖼️ 고정적이라면 → StatelessWidget
🔄 동적이라면 → StatefulWidget</strong></p>
<p>위 두 가지를 적재적소에 활용하면, 더 효율적이고 아름다운 앱을 만들 수 있습니다! 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Dart 문법]]></title>
            <link>https://velog.io/@ee_ji0/Flutter-Dart-%EB%AC%B8%EB%B2%95-jc07osqj</link>
            <guid>https://velog.io/@ee_ji0/Flutter-Dart-%EB%AC%B8%EB%B2%95-jc07osqj</guid>
            <pubDate>Mon, 06 Jan 2025 06:09:19 GMT</pubDate>
            <description><![CDATA[<p><code>Flutter</code> 코드를 작성하는데 필요한 <code>dart문법</code>을 연습해보자‼️</p>
<p><strong><a href="https://old-dartpad-3ce3f.web.app/?id=02d3c1600e40dd95d65af5ca6a3e4d16&amp;null_safety=true">🔗 [링크] DartPad : Dart 문법 연습하기  
</a></strong>
<img src="https://velog.velcdn.com/images/ee_ji0/post/2c159a52-fb41-4741-90d0-114df39d682a/image.png" alt=""></p>
<p><strong>🎯 기본내용</strong>
<code>main</code>은 Dart에서 처음 시작 시 호출하는 약속된 함수이다
<code>주석</code>은 <code>//</code>
<code>print()</code> 소괄호 안쪽에 값 입력 <code>console</code>에 출력
<code>;</code> Dart 에서 마지막은 <code>세미클론</code>으로 마무리~</p>
<pre><code class="language-dart">    main() {
    // Print 아래 내용이 콘솔에 출력됩니다.
    print(&quot;Hello Word~&quot;);
    }</code></pre>
<blockquote>
<p>1) 변수
2) 자료형 
3) 흐름 제어문
4) 함수(function) 
5) 클래스(Class)</p>
</blockquote>
<h2 id="1️⃣-변수">1️⃣ 변수</h2>
<pre><code class="language-dart">  main() {
    var variable = &quot;변수&quot;; 
    String id = &quot;ee_ji0&quot;; // 쌍따움표 
    String velogPath = &#39;https://velog.io/&#39;; // 단다음표

    //문자열 연산 
    print(velogPath + id); // https://velog.io/ee_ji0
    print(velogPath + &quot;@&quot; + id); // https://velog.io/@ee_ji0

    // 문자열 속에 변수값 할당
    print(&quot;velogPath id&quot;);   // velogPath id
    print(&quot;$velogPath@$id&quot;); // https://velog.io/ ee_ji0
    print(&quot;${velogPath + id}&quot;); // https://velog.io/ee_ji0

    // 내장함수 
    print(velogPath.split(&#39;//&#39;)); // [https:, velog.io/]

    }</code></pre>
<p>   ** 자료형 **
  <code>var</code> : 처음 담긴 값으로 타입이 지정됩니다.
  <code>String</code> : 문자만 담을 수 있습니다.
  <code>String?</code> : 문자 또는 비어있는(<code>null</code>) 상태일 수 있습니다.
  <code>final String</code> : 문자를 한 번 담은 뒤 재할당 불가능합니다.</p>
<p>   ** 변수명 만드는 규칙**
            1. <code>영문</code> / <code>_</code> / <code>$</code> / <code>숫자</code>만 사용
            2. <code>숫자</code>로 시작 불가능
            3. 카멜케이스(camelCase) 사용</p>
<p>  <img src="https://velog.velcdn.com/images/ee_ji0/post/b23061ef-e4c7-453d-a29d-a9a8e362f442/image.png" alt=""></p>
<h2 id="2️⃣-자료형">2️⃣ 자료형</h2>
<table>
<thead>
<tr>
<th>자료형</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>String</td>
<td>단따옴표와 쌍따옴표로 이루어진 문자열</td>
<td>&quot;철수&quot;/&#39;철수&#39;</td>
</tr>
<tr>
<td>int / double</td>
<td>int = 정수 / double = 실수</td>
<td>1, -1 / 1.5, -1.5</td>
</tr>
<tr>
<td>bool</td>
<td>참 거짓을 나타내는 자료형 / 비교 연산시 bool을 반환</td>
<td>true / false</td>
</tr>
<tr>
<td>List<T></td>
<td>데이터를 여러개 가진 배열</td>
<td>[1, 2, 3]</td>
</tr>
<tr>
<td>Map&lt;K, V&gt;</td>
<td>사전과 같이 {key : value} 형태 / key와 value에 모든 자료형이 올 수 있음</td>
<td>{&#39;name&#39;: &#39;철수&#39;,&#39;age&#39;: 20}</td>
</tr>
<tr>
<td>dynamic</td>
<td>모든 자료형을 담을 수 있음</td>
<td>dynamic name = &quot;hi&quot;; name = 1;</td>
</tr>
</tbody></table>
<h2 id="3️⃣-흐름제어문">3️⃣ 흐름제어문</h2>
<h3 id="1-조건문">1) 조건문</h3>
<pre><code class="language-dart">if (bool1) {
    // bool1이 true면 실행
} else {
    // bool1이 false면 실행
}</code></pre>
<pre><code class="language-dart">if (bool1) {
    // bool1이 true면 실행
} else if (bool2) {
    // bool1이 false이고, bool2가 true이면 실행
} else if (bool3) {
    // bool1과 bool2가 false이고, bool3가 true이면 실행
} else {
    // bool1, bool2, bool3가 모두 false이면 실행
}</code></pre>
<ul>
<li><p>** AND와 OR 연산자**</p>
<aside>
💡 `&&`는 AND 연산자라 불립니다

<p>💡 <code>||</code>는 OR 연산자라 불립니다.</p>
</aside>


</li>
</ul>
<h3 id="2-반복문">2) 반복문</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/3d89973d-6cda-422f-84d4-889656a468a0/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] Dart 문법]]></title>
            <link>https://velog.io/@ee_ji0/Flutter-Dart-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@ee_ji0/Flutter-Dart-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Thu, 02 Nov 2023 09:20:17 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯dartpad">🎯<a href="https://dartpad.dev/?id=02d3c1600e40dd95d65af5ca6a3e4d16&amp;null_safety=true">DartPad</a></h2>
<blockquote>
<p>: 온라인 상에서 Dart를 실행할 수 있는 웹사이트
▶️<a href="https://dartpad.dev/?id=02d3c1600e40dd95d65af5ca6a3e4d16&amp;null_safety=true">DartPad 접속</a>◀️</p>
</blockquote>
<pre><code class="language-dart">main() {
  // 여기서 부터 시작합니다.
  print(&quot;Hello Dart&quot;);

}</code></pre>
<ul>
<li><code>print()</code>: 소괄호 안쪽에 값을 넣으면 오른쪽 Console에 값이 출력<hr>

</li>
</ul>
<h1 id="💡변수">💡변수</h1>
<h2 id="1-변수-만들기">1. 변수 만들기</h2>
<h3 id="1-자료형">1) 자료형</h3>
<p><code>var</code> : 처음 담긴 값에 따라 타입을 지정(문자열, 정수)
<code>String</code>: 문자만
<code>String?</code>: 문자 또는 비어있는(<code>null</code>) 상태 
<code>final String</code>: 문자를 한 번 담은 뒤 재할당 불가능</p>
<h3 id="2-변수명">2) 변수명</h3>
<p><strong>Dart의 변수명 만드는 규칙</strong></p>
<ol>
<li><code>영문</code> / <code>_</code> / <code>$</code> / <code>숫자</code>만 사용</li>
<li><code>숫자</code>로 시작 불가능</li>
<li><code>카멜 표기법</code> 사용</li>
</ol>
<pre><code class="language-dart">main() {
  /// var : 처음 담은 값으로 자료형이 결정 됨
  var name = &#39;철수&#39;;
  print(name); // 철수
  print(name.runtimeType); // string (문자)

  var age = 20;
  print(age); // 20
  print(age.runtimeType); // int (정수)


  print(&quot;=&quot;*20);


  /// String : 문자만 넣을 수 있음
  String address = &#39;우리집&#39;;
  print(address); // 우리집

  // address = 1; // ⬅️ String 만 담을 수 있기 때문에 이 코드는 에러 발생

  address = &#39;모두의 집&#39;;
  print(address); // 모두의 집


  print(&quot;=&quot;*20);


  /// String? : 문자 또는 비어있을 수 있음
  String? email; // ⬅️ 아무것도 안넣었으므로 비어있음
  print(email); // null ⬅️ 비어있음을 의미

  email = &quot;a@a.com&quot;; // 문자열 할당
  print(email); // a@a.com

  email = null; // 다시 비우기
  print(email); // null


  print(&quot;=&quot;*20);


  /// final : 값을 재할당 할 수 없음
  final String phone = &quot;010-0000-0000&quot;;
  print(phone); // 010-0000-0000
  // phone = &quot;010-1111-1111&quot;; // final 때문에 이 코드는 실행 불가능
}</code></pre>
<h3 id="console화면">Console화면</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/4c58151c-ed74-443f-bee6-d98f962702a3/image.png" alt=""></p>
<hr>

<h1 id="💡자료형">💡자료형</h1>
<table>
<thead>
<tr>
<th align="center">자료형</th>
<th align="left">설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>String</code></td>
<td align="left">단따옴표와 쌍따옴표로 이루어진 문자열</td>
<td>&quot;철수&quot;<br>&#39;철수&#39;</td>
</tr>
<tr>
<td align="center"><code>int</code><br><code>double</code></td>
<td align="left">int = 정수<br>double = 실수</td>
<td>1, -1<br>1.5, -1.5</td>
</tr>
<tr>
<td align="center"><code>bool</code></td>
<td align="left">참 거짓을 나타내는 자료형<br>비교 연산시 bool을 반환</td>
<td>true<br>false</td>
</tr>
<tr>
<td align="center"><code>List&lt;T&gt;</code></td>
<td align="left">데이터를 여러개 가진 배열</td>
<td>[1, 2, 3]</td>
</tr>
<tr>
<td align="center"><code>Map&lt;K, V&gt;</code></td>
<td align="left">사전과 같이 {key : value} 형태<br>key와 value에 모든 자료형이 올 수 있음</td>
<td>{<br>  &#39;name&#39;: &#39;철수&#39;,<br>  &#39;age&#39;: 20<br>}</td>
</tr>
<tr>
<td align="center"><code>dynamic</code></td>
<td align="left">모든 자료형을 담을 수 있음</td>
<td>dynamic name = &quot;hi&quot;;<br>name = 1;</td>
</tr>
</tbody></table>
<h1 id="💡흐름-제어문">💡흐름 제어문</h1>
<h2 id="1-조건문">1) 조건문</h2>
<pre><code class="language-dart">if (bool1) {
    // bool1이 true면 실행
} else if (bool2) {
    // bool1이 false이고, bool2가 true이면 실행
} else if (bool3) {
    // bool1과 bool2가 false이고, bool3가 true이면 실행
} else {
    // bool1, bool2, bool3가 모두 false이면 실행
}</code></pre>
<h2 id="2-반복문for문">2) 반복문(for문)</h2>
<pre><code class="language-dart">for (int i = 0; i &lt; 5; i++) {
    print(&#39;hello ${i + 1}&#39;);
}```</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 위젯 모음.zip]]></title>
            <link>https://velog.io/@ee_ji0/Flutter-%EC%9C%84%EC%A0%AF-%EB%AA%A8%EC%9D%8C.zip</link>
            <guid>https://velog.io/@ee_ji0/Flutter-%EC%9C%84%EC%A0%AF-%EB%AA%A8%EC%9D%8C.zip</guid>
            <pubDate>Thu, 02 Nov 2023 00:57:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ee_ji0/post/0f908aaf-345c-4b2b-97db-e47c4778607b/image.png" alt=""></p>
<h1 id="flutter-이해하기">Flutter 이해하기</h1>
<blockquote>
<p>Flutter의 모든 위젯은 StatelessWidget 과 StatefultWidget으로 나눌 수 있다 </p>
</blockquote>
<p><strong>StatelessWidget</strong> : 상태 변화가 없어 화면을 새로고침 할 필요가 없는 위젯
<strong>StatefulWidget</strong>  : 상태 변화가 있어 화면을 새로고침 할 필요가 있는 위젯</p>
<h2 id="🚩statelesswidget-새로고침-없음">🚩StatelessWidget (새로고침 없음)</h2>
<ul>
<li><code>extends StatelessWidget</code> : StatelessWidget의 기능을 물려받는다</li>
<li><code>생성자</code> : 클래스 이름과 동일한 함수 </li>
<li><code>build 함수</code> : 화면에 보여줄 자식 위젯을 반환 
<a href="https://dartpad.dev/?id=847e8f2ade70953666b461e745d8686f&amp;null_safety=true"><strong>DartPad로 연습하기</strong></a><h2 id="🚩statefulwidget새로고침-있음">🚩StatefulWidget(새로고침 있음)</h2>
<a href="https://dartpad.dev/?id=dd2dbb0f5f660469e8ca32f2434b3275&amp;null_safety=true"><strong>DartPad로 연습하기</strong></a></li>
</ul>
<h1 id="👷♂️영역-배치-관련">👷‍♂️영역 배치 관련</h1>
<h2 id="💡scaffold--간편-배치">💡Scaffold : 간편 배치</h2>
<blockquote>
<ul>
<li>Scaffold 위젯 UI의 뼈대를 잡아주는 역할을 하는 위젯</li>
</ul>
</blockquote>
<ul>
<li>상단 appBar, 중앙 body, 하단bottomNavigetionBar, 우측하단 floatingActionButton</li>
</ul>
<pre><code class="language-dart">Scaffold(
    appBar: 다른 위젯, // 상단 바
    body: 다른 위젯, //화면 중앙에 가장 큰 면적
    bottomNavigationBar: 다른 위젯, //하단 바
    floatingActionButton: 다른 위젯, //우측 하단
),</code></pre>
<h2 id="💡column--세로-방향">💡Column : 세로 방향</h2>
<blockquote>
<p>세로 방향으로 여러 위젯을 나열할 때 사용</p>
</blockquote>
<pre><code class="language-dart">Column(
  children: [ // 자식 위젯들
    Text(&quot;위젯1&quot;),
    Text(&quot;위젯2&quot;),
  ],
),</code></pre>
<h2 id="💡padding">💡Padding</h2>
<h3 id="전방위-모두-동일-적용">전방위 모두 동일 적용</h3>
<pre><code class="language-dart">Paddion(
    padding: const EdgeInsets.all(8),
),</code></pre>
<h3 id="특정-방위만-적용">특정 방위만 적용</h3>
<pre><code class="language-dart">Paddion(
    padding: const EdgeInsets.only(
         left: 8,
          right: 8,
    ),
), </code></pre>
<h3 id="위아래-또는-좌우-적용">위아래 또는 좌우 적용</h3>
<pre><code class="language-dart">Paddion(
    padding: const EdgeInsets.symmetric(
        vertical: 8,
        horizontal: 8,
    ),
)</code></pre>
<h2 id="💡container">💡Container</h2>
<pre><code class="language-dart">Container(
  width: double.infinity, // 폭
  margin: EdgeInsets.only(top: 24),
  height: 200, // 높이
  color: Colors.amber, // 박스 색상
  alignment: Alignment.center, //chlid 정렬 
  child: Text(&quot;I Love Flutter!&quot;), // 자식 위젯
      style: TextStyle(
        color: Colors.white,
        fontSize: 18
    )
),</code></pre>
<h1 id="⌨️텍스트-관련">⌨️텍스트 관련</h1>
<h2 id="💡text--텍스트-창">💡Text : 텍스트 창</h2>
<blockquote>
<p>텍스트를 보여줄 때 Text위젯을 사용</p>
</blockquote>
<pre><code class="language-dart">Text(
  &#39;텍스트 내용&#39;,
  style: TextStyle(
    fontSize: 35, // 폰트 크기
    fontWeight: FontWeight.bold, // 폰트 두께
    color: Colors.amber, // 폰트 색상
  ),
),</code></pre>
<h2 id="💡textfield--텍스트-입력">💡TextField : 텍스트 입력</h2>
<pre><code class="language-dart">TextField(
    obscureText: true,
    decoration: InputDeCoration(
    labelText:&quot;비밀번호&quot;
    ),
),</code></pre>
<h1 id="🕹️버튼-관련">🕹️버튼 관련</h1>
<h2 id="💡elevatedbutton--기본-버튼">💡ElevatedButton : 기본 버튼</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/981a68b0-909f-4c8b-b436-9bf2ad0e7e85/image.png" alt=""> </p>
</blockquote>
<pre><code class="language-dart">// 위로 올라와 있는 듯한 버튼
ElevatedButton(
  onPressed: () {},// 클릭 이벤트 담당
  child: Text(&#39;Elevated Button&#39;), // 버튼 속 위젯
),</code></pre>
<h2 id="💡textbutton--텍스트-버튼">💡TextButton : 텍스트 버튼</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/50c61ed4-d5dc-4d85-be12-0c71e4d0193a/image.png" alt=""></p>
</blockquote>
<pre><code class="language-dart">// 텍스트 버튼
TextButton(
  onPressed: () {},
  child: Text(&#39;Text Button&#39;),
),</code></pre>
<h2 id="💡iconbutton--아이콘-버튼">💡IconButton : 아이콘 버튼</h2>
<blockquote>
<p><img src="blob:https://velog.io/36c16594-0037-4a00-ba66-068c7ca1c299" alt="업로드중.."></p>
</blockquote>
<pre><code class="language-dart">// 아이콘 버튼
IconButton(
  onPressed: () {},
  icon: Icon(Icons.add),
),</code></pre>
<h1 id="📷이미지-관련">📷이미지 관련</h1>
<h2 id="💡-image">💡 Image</h2>
<blockquote>
<p>Image.network(&quot;URL&quot;)</p>
</blockquote>
<pre><code class="language-dart">Padding(
       padding: const EdgeInsets.all(32),
       child: Image.network(&quot;URL&quot;,
       width: 81,
),</code></pre>
<h1 id="🖱️scroll-관련">🖱️Scroll 관련</h1>
<h2 id="💡singlechildscrollview">💡SingleChildScrollView</h2>
<blockquote>
<p>범위 감싸기</p>
</blockquote>
<pre><code class="language-dart">  body: SingleChildScrollView(
   child: 다른 위젯,
  )</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 프로젝트 생성]]></title>
            <link>https://velog.io/@ee_ji0/Flutter-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@ee_ji0/Flutter-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Wed, 01 Nov 2023 14:12:26 GMT</pubDate>
            <description><![CDATA[<p><a href="https://velog.io/@ee_ji0/Flutter-%ED%94%8C%EB%9F%AC%ED%84%B0%EB%9E%80-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%84%A4%EC%B9%98">지난 포스팅</a>에서 Flutter가 무엇인지 왜 사용하는지 알아보았다🤔</p>
<blockquote>
<p>위젯트리을 사용하고 여러 개발 환경을 다룰 필요 없이 
flutter 하나 만으로 안드로이드, iOS를 다룰 수 있다는 점이 흥미로웠다🍭</p>
</blockquote>
<p><del>그래,, Flutter 너 멋진 아이구나,, 
근데 왜 설치하기,, 왜이리 힘드냐,,?🙄</del></p>
<p>여러 고난과 역경을 겪고 결국 해냈다..🪄
Flutter 설치 과정에 대해서 다시 정리해서 포스팅하는 것이 해두어야겠다</p>
<p>(더 이상의 삽질은 용납 못한다🫶)</p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/93270b3b-27e9-417b-8c37-28d2cf0d0331/image.png" alt=""></p>
<p>프로그램도 설치했으니 오늘은 프로젝트 파일을 생성해 보려고한다!</p>
<blockquote>
<p><strong>🪄프로젝트 준비</strong></p>
</blockquote>
<p>1) Flutter 프로젝트 생성하기 
2) VSCode Dart 세팅 
3) Emulator 실행하기 
4) 실행하기 </p>
<h2 id="💡flutter-프로젝트-생성하기">💡Flutter 프로젝트 생성하기</h2>
<p>1) Visual Stutdio Code(VSCode) 실행하기 
2) 상단에 <code>view</code> ➡️<code>command Palette</code> 버튼 클릭하기 </p>
<blockquote>
<p>window 단축키: <code>ctrl + Shift + p</code>
    macOS 단축키 : <code>cmd + Shift + p</code></p>
</blockquote>
<p>3) 명령어 검색 팝업창에 <code>flutter</code>입력 후 <code>Flutter: New Project</code>선택하기
4) <code>Application</code>을 선택하기
5) 프로젝트 시작할 폴더를 선택하기 (경로 지정하기 <del>한글은 지양</del>)
6) 프로젝트 이름 입력 후 확인!</p>
<blockquote>
<h3 id="🤔폴더-구조-살펴-보기">🤔폴더 구조 살펴 보기</h3>
<p>나의 경우에는 <code>hello_flutter</code>라는 폴더를 생성하였다🫶
이쯤에서 굼금해 지는 폴더 구조 한번 살펴 보자 
<img src="https://velog.velcdn.com/images/ee_ji0/post/5b661f4f-c1ba-4966-ab7c-9bf388d20fd3/image.png" alt="">
<code>lib</code> : 주로 코드를 짜는 폴더
<code>pubspec.yaml</code> : 라이브러리 및 설정을 하는 폴더 
<code>android</code> : Android 프로젝트 폴더
<code>ios</code> : iOS 프로젝트 폴더
<code>web</code> : Web 프로젝트 폴더 (웹도 만들수 있다!)
<code>macos</code>: MacOS 프로젝트 폴더
<code>linux</code> : Linux 프로젝트 폴더
<code>windows</code> : Window 프로젝트 폴더</p>
</blockquote>
<p>7) <code>main.dart</code>에 아래 코드 복사해서 기존 코드 모두 지우고 붙여 넣기 </p>
<pre><code class="language-js">import &#39;package:flutter/material.dart&#39;;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(),
      ),
    );
  }
}</code></pre>
<p>8) 불필요한 내용 화면에 표시하지 않기<code>analysis_options.yaml</code>파일을 열고, 아래 코드 복사해서 24번째 라인 뒤에 붙여 넣고 저장하기</p>
<pre><code class="language-js">    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false</code></pre>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/d132089a-a1ce-46f5-8781-1e6b96632072/image.png" alt=""></p>
<h2 id="💡vscode-dart-세팅">💡VSCode Dart 세팅</h2>
<p>1) <code>View</code> → <code>Command Palette</code> 실행하기
     &gt; window 단축키: <code>ctrl + Shift + p</code>
    macOS 단축키 : <code>cmd + Shift + p</code>
2) <code>dart recommend</code>라고 검색한 뒤 <code>Dart: Use Recommended Settings</code>를 선택하기  (자동 줄 정렬 기능과 같이 편의 기능 설정이 적용)</p>
<h2 id="💡emulator-실행하기">💡Emulator 실행하기</h2>
<blockquote>
</blockquote>
<h3 id="🤔에뮬레이터emulator란">🤔에뮬레이터(Emulator)란?</h3>
<p>실제 기기를 연결하지 않고 개발 항 수 있도록 컴퓨터 상의 가상의 휴대폰을 의미한다.</p>
<p>1) <code>View</code> → <code>Command Palatte</code>실행하기
2) <code>launch</code>를 입력한 뒤 <code>Flutter: Launch Emulator</code>를 선택하기.
3) <code>android</code> 에뮬레이터를 선택하기</p>
<h2 id="💡실행하기">💡실행하기</h2>
<p>1) <code>ⅴ</code> 버튼을 누르고 Run Without Debugging 버튼을 클릭 
<img src="https://velog.velcdn.com/images/ee_ji0/post/8f8196de-0adb-476b-ad3d-08acdc0da8ea/image.png" alt="">
2) 실행하는 동안 시간이 조금 소요 되니 여유롭게 기다리기!</p>
<h2 id="🥳실행화면">🥳실행화면</h2>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/6079d7c1-a5b2-488f-ad70-22908e7d1de9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SQLD] 데이터 모델링의 이해(1)]]></title>
            <link>https://velog.io/@ee_ji0/SQLD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EB%A7%81%EC%9D%98-%EC%9D%B4%ED%95%B41</link>
            <guid>https://velog.io/@ee_ji0/SQLD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EB%A7%81%EC%9D%98-%EC%9D%B4%ED%95%B41</guid>
            <pubDate>Thu, 26 Oct 2023 09:53:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ee_ji0/post/d91bbfcb-ce25-4ba9-995e-ca3ddb6cd22b/image.png" alt=""></p>
<h2 id="데이터-모델링의-특징">데이터 모델링의 특징</h2>
<p><code>추상화</code>  : 현실세계를 간략하게 표현해야한다. 
<code>단순화</code>  : 누구나 쉽게 이해할 수있도록 표현해야한다.
<code>명확성</code>  : 명확하게 의미가 해석되어야 하고 한가지 의미를 가져야한다.</p>
<h2 id="데이터-모델링의-3단계-개-논-물">데이터 모델링의 3단계 <del>(개-&gt;논-&gt;물)</del></h2>
<blockquote>
<p>📍데이터 모델링 3단계 <br> <code>개념적 모델링</code> , <code>논리적 모델링</code>, <code>물리적 모델링</code>
 📍데이터 모델링 관점
<code>데이터</code>, <code>프로세스</code>, <code>데이터와 프로세스</code></p>
</blockquote>
<h3 id="개념적-모델링">개념적 모델링</h3>
<ul>
<li><strong>전사적 관점</strong>에서 기업의 데이터를 모델링한다.</li>
<li><strong>추상화 수준이 가장 높은 모델링</strong>이다.<h3 id="논리적-모델링">논리적 모델링</h3>
</li>
<li>정규화를 통해서 <strong>재사용</strong>을 높인다. <h3 id="물리적-모델링">물리적 모델링</h3>
</li>
<li>** 성능, 보완, 가용성 등을 고려하여 데이터 베이스를 구축**한다. </li>
</ul>
 <br>
 <br>
## ERD작성 절차 
> 1) 엔터티를 도출하고 그린다.
2) 엔터티를 배치한다.
3) 엔터티 간의 관계를 설정한다. 
4) 관계를 서술한다.
5) 관계 참여도를 표현한다. 
6) 관계의 필수 여부를 표현한다. 


<h2 id="3층-스키마">3층 스키마</h2>
<p>사용자, 설계자, 개발자가 데이터 베이스를 보는 관점에 까라 데이터 베이스를 기술하고 이들 간의 관계를 정의한 것
<strong>데이터베이스의 독립성을 확보하기 위한 방법</strong>이다</p>
<h3 id="3층-스키마의-구조-데이더의-독립성-요소">3층 스키마의 구조 (데이더의 독립성 요소)</h3>
<p>1) <code>외부스키마</code> (외부화면, 사용자, 인터페이스)</p>
<ul>
<li>개개 사용자가 보는 개인적 DB 스키마 </li>
<li>응용 프로그램이 접근하는 데이터베이스를 정의</li>
</ul>
<p>2) <code>개념 스키마</code> (전체가 통합된 스키마)</p>
<ul>
<li>모든 사용자 관점을 통합한 전체 DB </li>
<li>통합 데이터 베이스의 구조 </li>
</ul>
<p>3) <code>내부 스키마</code> (실제 저장된 DB)</p>
<ul>
<li>물리적 장치(물리적 저장 구조)에서 데이터가 실제적 저장된 형식</li>
</ul>
<h3 id="데이터의-독립성">데이터의 독립성</h3>
<blockquote>
<p>논리적 복립성 : 개념스키마가 변경 되더라도 외부 스키마가 영향을 받지 않는 것이다.
물리적 독립성 : 내부 스키마가 변경되더라도 개념 스키마가 영향을 받지 않는 것이다. </p>
</blockquote>
<h2 id="엔터티-entity">엔터티 (Entity)</h2>
<p>업무에서 관리해애하는 데이터의 집합을 의미하며, 저장되고 관리되어야 하는 데이타이다. <del>객체?와 같은 의미인듯</del></p>
<h3 id="특징">특징</h3>
<ul>
<li>유일한 식별자가 있어야 한다.</li>
<li>2개 이상의 인스턴스가 있어야 한다.</li>
<li>반드시 속성을 가지고 있다.</li>
<li>다른 엔터티와 최소한 한개 이상의 관계가 있어야 한다.</li>
<li>업무에서 관리 되어야 하는 집합이다. </li>
</ul>
<h3 id="종류">종류</h3>
<p>유형과 무형에 따른 엔터티의 종류로는<br><code>유형엔터티</code>, <code>개념엔터티</code>, <code>사건엔터티</code>
발생 시점에 따른 엔터티의 종류 
<code>기본엔터티</code>, <code>중심엔터티</code>, <code>행위엔터티</code></p>
<br>

<h2 id="속성attribute">속성(Attribute)</h2>
<p>업무에소 필요한 정보인 엔터가 가지는 항목 
속성은 더 이상 분류되지 않은 단위로, 업무에 필요한 데이터를 저장할 수 있다. </p>
<blockquote>
<h3 id="속성의-종류">속성의 종류</h3>
<p>단일 속성 : 하나의 의미로 구성된 것
복합 속성 : 여러 개의 의미가 있는 것
다중값 속성 : 여러개의 값을 가질 수 있는 것으로 (엔터티로 구분O)</p>
</blockquote>
<h2 id="관계-relationship">관계 (Relationship)</h2>
<p>엔터티 간의 관계성을 의미하며 존재관계와 행위 관계로 구분 </p>
<blockquote>
<h3 id="관계의-종류">관계의 종류</h3>
<p>존재관계 : 엔터티 간의 상태를 의미 (화사 - 사원)
행위 관계 : 엔터티 간의 어떤 행위가 있는 것 (회사 - 주문내역)</p>
</blockquote>
<h3 id="관계차수">관계차수</h3>
<p>| 는 필수적 관계
O 선택적 관계
<img src="https://velog.velcdn.com/images/ee_ji0/post/7cfe5e1a-9d75-4857-aa97-1bb227b2b5c9/image.png" alt=""></p>
<h2 id="엔터티-식별자">엔터티 식별자</h2>
<h3 id="주식별자-기본키-primary-key">주식별자 (기본키, primary key)</h3>
<p><code>최소성</code>,<code>대표성</code>, <code>유일성</code>,<code>불변성</code></p>
<blockquote>
</blockquote>
<h3 id="식별자의-종류">식별자의 종류</h3>
<p>대표성 여부에 따른 식별자의 종류</p>
<ul> 
  <li>주식별자</li>
- 유일성과 최소성을 만족하면서 엔터티를 대표하는 식별자이다<br>
- 다른 엔터티와 참조 관계를 연결될 수 있다.
보조
  <li>보조식별자</li>
  - 유일성과 최소성은 만족하지만 대표성은 만족하지 못하는 식별자

</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 플러터란? / 플러터 설치 ]]></title>
            <link>https://velog.io/@ee_ji0/Flutter-%ED%94%8C%EB%9F%AC%ED%84%B0%EB%9E%80-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@ee_ji0/Flutter-%ED%94%8C%EB%9F%AC%ED%84%B0%EB%9E%80-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Mon, 23 Oct 2023 04:30:38 GMT</pubDate>
            <description><![CDATA[<p>시작은 웹 개발이었는데 어쩌다..앱 개발도 공부하게 되어 버렸다...
놀랍다.. 끝 없는 코딩세계💻📱</p>
<h2 id="🤔-플러터-flutter-란">🤔 플러터 [Flutter] 란?</h2>
<p>플러터는 <strong>cross- platform프레임워크</strong>이다. 
<strong>cross-platform</strong> 라는 말은, <strong>iOS</strong>와 <strong>Android</strong> 두개의 플랫폼에 사용할 앱을 <strong>하나의 코드베이스</strong>로 구축할 수 있음을 의미한다. 이런 특징은 규모적으로 스위프트, 코틀린 각각을 다루기 어려운 스타트업 개발에 있어서 큰 장점이고, 구글에서 많은 지원을 하고 있기 때문에 앞으로의 활용 범위도 넓어질 것이라고 한다👍</p>
<p><del>앱 뿐만 아니라 웹 페이지까지 플러터로 한 번에 구축할 수 있다고 했는데..!
웹은 아직 다가가지 않는 것이 정신 건강에 이롭다고 한다❕👀</del></p>
<p>따라서 플러터는 기본적으로 <strong>앱 개발을 위한 프레임워크</strong>!
고도의 모듈화된 <strong>위젯</strong>들을 이용해 <strong>widget tree</strong>를 쌓으며 <strong>User Interface</strong>를 코드로 구현하는 환경을 제공한다. 하나하나 구현 하는 것보다 <strong>쉽게 앱 개발이 가능</strong>하다는 장점이있다. 
<br></p>
<h2 id="💭왜-flutter를-사용하는가">💭왜 Flutter를 사용하는가?</h2>
<p>앱 개발 방법에 따라 <code>네이티브 앱</code>, <code>클로스 플랫폼 앱</code>으로 나누어 진다.</p>
<h3 id="📱네이티브-앱native-app">📱네이티브 앱(Native App)</h3>
<p><strong>Android</strong>는 </p>
<ul>
<li>프로그래밍 언어 : java / Kotlin </li>
<li>개발 툴 : Android Studio</li>
</ul>
<p><strong>iOS</strong>는</p>
<ul>
<li>프로그래밍 언어 : Obiective-C / Swift</li>
<li>개발 툴 : XCode</li>
</ul>
<blockquote>
<p>Android 는 Google에서 제공하는 Android SDK를 이용해 개발하는 방면 
iOS는 Apple에서 제공하는 iOS(Software Development Kit)를 이용하여 개발 한다. 또한 macOS에서만 개발이 가능하다😮 </p>
</blockquote>
<p>따라서 <strong>네이티브 앱</strong>은, 하나의 Android 와 iOS를 각각의 개발 방법에 따라 개별로 두 번 만드는 단점이 있다!
그러나 성능이 빠르고 최신 기능을 빠르게 적용할 수 있다는 장법도 존재❕</p>
<h3 id="📱크로스-플랫폼-앱cross-platform-app">📱크로스 플랫폼 앱(Cross Platform App)</h3>
<blockquote>
<p>💡 하나의 프로그래밍 언어와 소스코드로 Android와 iOS를 모두 개발하는 방법
💡네이티브 앱의 성능을 뛰어 넘을 수는 없지만! 
하나의 언어로 두 플랫폼에서 모두 실행이 된다는 엄청만 장점이 있다~~</p>
</blockquote>
<p><strong>React Native</strong></p>
<ul>
<li>프로그래밍 언어 : javaScript </li>
<li>페이스북에서 출시한 오픈 소스 모바일 애플리케이션 프레임 워크</li>
</ul>
<p><strong>Flutter</strong></p>
<ul>
<li>프로그래밍 언어 : Dart</li>
<li>구글에서 출시한 오픈 소스 모바일 애플리케이션 프레임 워크</li>
</ul>
<br>

<h2 id="💭flutter-이해하기">💭Flutter 이해하기</h2>
<h3 id="📍위젯-레고와-같이-조립이-가능-하다">📍위젯 (레고와 같이 조립이 가능 하다)</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/2e683ad8-095f-47df-a951-39bff9d7241b/image.png" alt=""></p>
<blockquote>
<p>💡Flutter는 모든 것이 위젯(Widget)으로 만들어져 있다❕
<strong>위젯(Widget)</strong> 은 레고 블럭과 같이 앱을 만든데 사용되는 작은 모듈*
<img src="https://velog.velcdn.com/images/ee_ji0/post/c22b8bdb-e390-4f6a-9cbc-575217fec8c5/image.png" alt="">
<strong>위젯트리(Widget Tree)</strong> 레고들록 하나하나가 위젯들이고 블록이 겹쳐서 만들어진것이 하나의 앱! </p>
</blockquote>
<p>메테리얼 위젯(Material Widget) - Android 기본화면 구성요소 재현 위젯
쿠퍼티노 위젯(Cupertino Widget) - iOS 기본화면 구성요소 재현 위젯 
커스텀 위젯(Custom Widget) - 특정 플랫폼에 종속되지 않고 고유의 디자인
<br></p>
<h2 id="🤔-플러터-설치하기">🤔 플러터 설치하기</h2>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/5b1e81bb-ee0c-4eca-80c5-0ba80b6f3ef9/image.gif" alt=""></p>
<p><del>진짜,,,플러터까지는 설치 쉬웠다,, Android Studio 설치가 빡세서 놀랬다..</del></p>
<h3 id="🪄설치">🪄설치</h3>
<blockquote>
</blockquote>
<ol>
<li><a href="https://docs.flutter.dev/get-started/install">플러터홈페이지 </a>에서 플러터를 <strong>다운로드</strong>한다. <br><br> </li>
<li>플러터를 실행하기 위해서는 플러터뿐이니라 Android Studio, Xcode, Chrome등 ... 추가적으로 필요한 게 많은데 flutter doctor를 이용해 확인할 수 있다.
터미널 실행 &gt; 플러터 디렉터리가 위치한 경로로 이동 <code>flutter doctor</code>입력 &gt; 추가 사항 알려줌<br></li>
<li>X표시된 부분에서 문제가 있음 열심히 구글링하기!</li>
</ol>
<h4 id="😮🫶reference">😮🫶Reference</h4>
<p><a href="https://brunch.co.kr/@mystoryg/114">Flutter개발환경 구축</a></p>
<p><a href="https://aws.amazon.com/ko/what-is/flutter/">https://aws.amazon.com/ko/what-is/flutter/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 배포]]></title>
            <link>https://velog.io/@ee_ji0/%EC%9B%B9-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@ee_ji0/%EC%9B%B9-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Wed, 27 Sep 2023 02:06:02 GMT</pubDate>
            <description><![CDATA[<h2 id="1-tomcat-8593-설치">1. tomcat 8.5.93 설치</h2>
<blockquote>
<p>빌드 : 컴파일된 코드를 실제 실행할 수 있는 상태로 만드는 일 
배포 : 빌드가 완성된 실행 가능한 파일을 
사용자가 접근할 수 있는 환경에 배 시키는 일 </p>
</blockquote>
<p><strong><em>jar(Java ARchive)</em></strong> : 
<strong><em>war(Web application ARchive)</em></strong> :</p>
<p><a href="https://tomcat.apache.org/">https://tomcat.apache.org/</a></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/8256cc59-bdf4-48fd-9f1d-24d6ca594c9d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/b7f5fcd7-4910-4d99-a2c6-d0834e2d2cbb/image.png" alt="">
서버로 파일 옮기고 압축 풀기</p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/f624ac36-0868-431d-839c-17347e818fe0/image.png" alt=""></p>
<p>server.xml 파일 vos 코드로 열기 
22번째행 / 69번째행 port번호 수정 
<img src="https://velog.velcdn.com/images/ee_ji0/post/95ea817b-8f86-41c9-b895-cc3164cff225/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/733703b1-c4c4-4b9b-af01-58b0f9366956/image.png" alt=""></p>
<p>cmd켜기 (관리자 권한)
bin 폴더까지 전체 복사
나는 tools에 파일이 있기 때문에 
cd C:\tools\server\apache-tomcat-8.5.93\bin</p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/4413bebd-bb0b-456a-a241-e5c422308ef2/image.png" alt=""></p>
<p>cd 파일경로 
startup.bat
입력하면 아래와 같은 한글이 깨진  cmd창 뜬다</p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/6f7f0fbb-4f0c-4801-b164-856b0943d536/image.png" alt=""></p>
<h2 id="2-maven-설치해보자">2. Maven 설치해보자</h2>
<p><a href="https://maven.apache.org/">https://maven.apache.org/</a></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/fde255f3-8f40-4170-898b-e79b07e76adc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/1105f224-643b-4752-9205-6ca81efa2a9e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/1dd8c620-1bf8-441d-a3b3-08872e3eb87e/image.png" alt="">
repository 파일 생성
conf &gt; settings.xml<br><img src="https://velog.velcdn.com/images/ee_ji0/post/6a27c945-37af-4348-a190-c2b9e8c5004d/image.png" alt="">
<img src="https://velog.velcdn.com/images/ee_ji0/post/355e2f63-1856-4904-adee-ba7277c50a15/image.png" alt="">
53번째 행 밖으로 빼고 
reposetory 경로 입력
<localRepository>C:\tools\maven\apache-maven-3.9.4\reposetory</localRepository>
<img src="https://velog.velcdn.com/images/ee_ji0/post/a8ad7b7c-6d22-4250-8a55-f2bfa5534c36/image.png" alt=""></p>
<p>STS 켜고 
8_application
window-preferences-Maven-userSettings
<img src="https://velog.velcdn.com/images/ee_ji0/post/19daf1eb-9dd3-41c0-81b1-de41a310b092/image.png" alt=""></p>
<p>스프링 프로젝트 설정 다시하기
Spring MVC project
페키지 명 : DeployExam 
클래스 명 : com.kh.deployExam</p>
<p>프로젝트 우클릭 &gt; run as&gt; </p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/de977166-3f2b-4309-bff0-7d360b12eaad/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/27272911-dd5e-47cf-9fc3-065cc7bb6bcf/image.png" alt=""></p>
<h2 id="3-배포">3. 배포</h2>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/8e48b03e-7f07-4e2d-8c93-a5324fe40f66/image.png" alt=""></p>
<pre><code>&lt;plugin&gt;
&lt;groupId&gt;org.apache.tomcat.maven&lt;/groupId&gt;
&lt;artifactId&gt;tomcat7-maven-plugin&lt;/artifactId&gt;
&lt;version&gt;2.2&lt;/version&gt;
&lt;configuration&gt;
&lt;url&gt;http://127.0.0.1:8899/manager/text&lt;/url&gt;
&lt;username&gt;admin&lt;/username&gt;
&lt;password&gt;admin&lt;/password&gt;
&lt;path&gt;/DeployExam&lt;/path&gt;
&lt;/configuration&gt;
&lt;/plugin&gt;
</code></pre><p>추가 pom.xml
<plugins/> 태그에 추가ㅎㅎㅎ</p>
<p> <img src="https://velog.velcdn.com/images/ee_ji0/post/49604b19-f22e-4bed-8114-f845f783b9c3/image.png" alt=""></p>
<p> tomcat-user.xml
<img src="https://velog.velcdn.com/images/ee_ji0/post/2197c922-cedf-421c-a058-573111fc89dc/image.png" alt=""></p>
<pre><code class="language-&lt;role">&lt;role rolename=&quot;manager-gui&quot;/&gt;
&lt;role rolename=&quot;manager-jmx&quot;/&gt;
&lt;role rolename=&quot;manager-status&quot;/&gt;
&lt;user username=&quot;admin&quot; password=&quot;admin&quot; roles=&quot;managergui,manager-script,manager-status,manager-jmx&quot;/&gt;
</code></pre>
<p>Maven build
run confingurations -&gt; tomcat7:deploy -&gt; apply -&gt; run </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React ] Context api]]></title>
            <link>https://velog.io/@ee_ji0/React-Context-api</link>
            <guid>https://velog.io/@ee_ji0/React-Context-api</guid>
            <pubDate>Fri, 08 Sep 2023 02:08:32 GMT</pubDate>
            <description><![CDATA[<p>Context APi에 대해 알아보자❗</p>
<h1 id="context-api란❓">Context API란❓</h1>
<p><code>Props driling</code>은 React 컴포넌트 구조에서 자식 컴포넌트로 데이터를 전달하기 위해 부모 컴포넌트를 거치는 것을 의미 이는 컴포넌트의 구조가 깊어지면서 <code>코드가 복잡</code>해지고 <code>유지보수가 어려워지는 문제를 유발</code>한다. </p>
<blockquote>
<p>Context API는 props driling 방지 문제점을 해결하기 위한 방법이다</p>
</blockquote>
<h2 id="props-driling">Props driling</h2>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/f6c6aeb8-627b-4cbf-80c6-b6f93ef90cf0/image.png" alt=""></p>
<h2 id="context를-사용하기-위해서는❓">ConText를 사용하기 위해서는❓</h2>
<p><code>createContext()</code>함수를 사용하여 <code>Context객체</code>를 생성해야한다. 
객체 생성 후 Context.<code>Provider</code> 컴포넌트를 이용해 <code>자식컴포넌트</code>에게 <code>데이터를 전달</code>할 수 있다.</p>
<h3 id="provider">Provider</h3>
<blockquote>
<p>value prop을 통해 데이터를 전달한다.</p>
</blockquote>
<h2 id="아래-예제를-보고-context-api를-알아보자">아래 예제를 보고 Context API를 알아보자</h2>
<h2 id="usercontextjs">UserContext.js</h2>
<pre><code class="language-js">import React, { createContext } from &#39;react&#39;;

// createContext(초기값): Constext 객체생성
const UserContext = createContext();

export default UserContext;</code></pre>
<h2 id="r06_context_apijs">R06_context_api.js</h2>
<pre><code class="language-js">import React, { useState , useContext } from &#39;react&#39;;
import UserContext from &#39;../contexts/UserConext&#39;;

const User = () =&gt;{

    //useContext(Context명) : 지정된 Context를 사용
    // -&gt; 부모 컴포넌트에서 제공한 값을 꺼내 쓸 수 있다
    const {user, temp} = useContext(UserContext); //user, temp
    // UserContext에서 user를 꺼내서 user변수에 저장
    // UserContext에서 temp를 꺼내서 temp변수에 저장

    console.log(user);
    console.log(temp);

    return(
        &lt;ul&gt;
            &lt;li&gt;{user.name}&lt;/li&gt;
            &lt;li&gt;{user.email}&lt;/li&gt;
        &lt;/ul&gt;
    )
}

const Profile =() =&gt; {

    const [user, setUser] = useState(null);

    const print = () =&gt; {

        setUser({name: &#39;김미영&#39;, email: &#39;my-kim@kh.or.kr&#39;})
    }



const temp = &#39;임시변수&#39;;

    return(

        /*  UserContext가 감싸고 있는 자식 컴포넌트에게  
            Context API를 이용해서 user 제공
        */ 

        &lt;UserContext.Provider value={ {user , temp} }&gt;
            &lt;div&gt;
                {/* 삼항연산자를 이용한 컴포넌트 렌더링 제어(조건부 렌더링) */}
                { user != null? (
                    &lt;&gt;
                        &lt;User/&gt;
                        &lt;button onClick={ ()=&gt;{ setUser(null) }}&gt; 개인 정보 숨기기&lt;/button&gt;
                    &lt;/&gt;
                ) : (
                    &lt;button onClick={print}&gt; 개인 정보 출력&lt;/button&gt;
                )}

            &lt;/div&gt;
        &lt;/UserContext.Provider&gt;

    );
}

export default Profile;</code></pre>
<h2 id="💗화면">💗화면</h2>
<h3 id="첫-화면">첫 화면</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/642fcdf9-b17a-469d-90be-bedc7326ccd3/image.png" alt=""></p>
<h3 id="개인정보-출력-버튼을-누르면">개인정보 출력 버튼을 누르면?</h3>
<p>onClick print()가 수행되고 김미영 팀장의 정보가 나타난다.
<img src="https://velog.velcdn.com/images/ee_ji0/post/645c6ea0-eb24-444a-ae25-711a2c1af8ca/image.png" alt=""></p>
<h3 id="개인정보-숨기기-버튼을-누루면">개인정보 숨기기 버튼을 누루면?</h3>
<p>onClick 에서 익명함수가 실행되고 setUser를 통해 user는 null이 되어 초기화면으로 돌아간다.
<img src="https://velog.velcdn.com/images/ee_ji0/post/642fcdf9-b17a-469d-90be-bedc7326ccd3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] StateLiftingUp(상태끌어올리기)]]></title>
            <link>https://velog.io/@ee_ji0/React-StateLiftingUp%EC%83%81%ED%83%9C%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@ee_ji0/React-StateLiftingUp%EC%83%81%ED%83%9C%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Thu, 07 Sep 2023 03:08:57 GMT</pubDate>
            <description><![CDATA[<h1 id="stateliftingup">StateLiftingUp</h1>
<blockquote>
<p>자식 컴포넌트에서 발생한 이벤트를 부모 컴포넌드에서 처리하도록 하는 상태</p>
</blockquote>
<p>원래는 <code>부모 컴포넌트</code>가 <code>자식 컴포넌트</code>의 상태를 직접 변경할 수 없다❌!
그러나 <code>props</code>와 <code>useState</code>의 성질를 이용해서 위와 같은 문제를 해결할 수 있다!</p>
<hr>
상태 끌어올리기를 해보기 전 알아야하는 개념 
```props```, ```state```, ```useState```

<h4 id="props">props</h4>
<blockquote>
<p>부모컴포넌트가 자식 컴포넌드에게 데이터 전달 시 사용하는 객체</p>
</blockquote>
<pre><code>**props 사용법**
    1. 자식 컴포넌트에 전달하려는 값(date) 속성을 정의한다.
    2. props를 사용해 정의된 값과 속성을 전달한다. 
    3. 전달받은 props를 렌더링 한다. </code></pre><h4 id="state">State</h4>
<blockquote>
<p>컴포넌트의 상태를 나타내고 동적인 데이터를 다룰 때 사용한다. </p>
</blockquote>
<h3 id="userstate">userState</h3>
<blockquote>
<p>컴포넌트의 상태를 관리해줌 
state의 변화가 감지되면 변화가 감지된 부분만❗컴포넌트 리렌더링를 한다</p>
</blockquote>
<h2 id="stateliftingup-예제">StateLiftingUp 예제</h2>
<h3 id="r04_state3js">R04_state3.js</h3>
<blockquote>
<p>📍상태끌어올리기 : 부모컴포넌트의 &quot;상태를 변경하는 함수&quot;(useState사용) 그 자체를 자식 컴포넌트로 전달하고, 이함수를 하위 컴포넌트가 실행한다.</p>
</blockquote>
<p>const [변수명, 값을 변경하는 함수(setter같은 역할)] = useState(&#39;초기값&#39;);</p>
<p>값을 변경하는 함수를 담을 변수를 생성해서 props로 보냄!
자식이 변경되면 부모도 변경되는 상태가된다.</p>
<pre><code class="language-js">import React,{useState} from &quot;react&quot;;

const Id = ({handler}) =&gt; {

    // props로 전달한 값 중에서 key가 handler인 요소인 value 반환
    // console.log(handler);

    return(
        &lt;&gt;
            &lt;div className=&#39;wrapper&#39;&gt;
                &lt;label htmlFor=&#39;id&#39;&gt;ID : &lt;/label&gt;
                &lt;input type=&quot;text&quot; id=&#39;id&#39; onChange={handler}/&gt;
            &lt;/div&gt;
        &lt;/&gt;
    );

};


const Pw = ({handler}) =&gt; { 
    return(
        &lt;&gt;
            &lt;div className=&#39;wrapper&#39;&gt;
                &lt;label htmlFor=&#39;pw&#39;&gt;PW : &lt;/label&gt;
                &lt;input type=&#39;password&#39; id=&#39;pw&#39;onChange={handler}/&gt;
            &lt;/div&gt;
        &lt;/&gt;
    );

};

// 상태 끌어올리기
const StateLiftingUp = () =&gt; {

    const [id, setId] = useState(&#39;&#39;);
    const [pw, setPw] = useState(&#39;&#39;);

    // 변수에 저장
    const idHandler = (e) =&gt; { // id 값을 변경하는 함수

        setId(e.target.value);
    };

    // 변수에 저장
    const pwHandler = (e) =&gt; { // pw 값을 변경하는 함수
        setPw(e.target.value);
    };

    console.log(&quot;id : &quot;+ id);
    console.log(&quot;pw : &quot;+ pw);

    return(
        &lt;&gt;
            &lt;Id handler={idHandler}/&gt;
            &lt;Pw handler={pwHandler}/&gt;

            &lt;div className=&#39;wrapper&#39;&gt;

                {/* disabled */}
                {/* id와 pw중 하나라도 입력되지 않을 경우  버튼 안 눌리게 하기*/}
                &lt;button disabled={id.length === 0||pw.length === 0}&gt;Login&lt;/button&gt;
            &lt;/div&gt;
        &lt;/&gt;
    );
};

export default StateLiftingUp;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] State 상태& Props 속성]]></title>
            <link>https://velog.io/@ee_ji0/React-State-%EC%83%81%ED%83%9C-Props-%EC%86%8D%EC%84%B1</link>
            <guid>https://velog.io/@ee_ji0/React-State-%EC%83%81%ED%83%9C-Props-%EC%86%8D%EC%84%B1</guid>
            <pubDate>Thu, 07 Sep 2023 00:52:20 GMT</pubDate>
            <description><![CDATA[<h2 id="props-속성">Props 속성</h2>
<blockquote>
<p> 부모컴포넌트가 자식 컴포넌드에게 데이터 전달 시 사용하는 객체
props는 자식이 부모에게 전달 할 수는 없다. </p>
</blockquote>
<p><code>React 컴포넌트</code>에게 데이터를 전달하는 방법 중 하나입니다. 
<code>Props</code>는 <code>부모 컴포넌트</code>로부터 <code>자식 컴포넌트</code>로 전달되며, 컴포넌트 내부에서 변경할 수 없는 <code>읽기 전용 데이터</code>이다. 
<code>Props</code>를 사용하면 컴포넌트 간의 <code>데이터 전달이 간단</code>하고 <code>유지보수가 쉬워진다</code>.
    또한, 컴포넌트의 <code>재사용성</code>을 높일 수 있습니다. </p>
<h2 id="props-drilling--상태-내려꽂기">Props Drilling : 상태 내려꽂기</h2>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/cb52eeee-d661-4ef7-9fc5-4558c0c986ae/image.png" alt=""></p>
<blockquote>
<p>Props를 통해 데이터를 전달 할 때, 하위 컴포넌트에서 필요하지 않은 Props를 게속 전달하는 것</p>
</blockquote>
<p>코드의 가독성을 떨어뜨리고, 유지보수를 어렵게 만들수 있다.
따라서 props drilling을 최소화하기 위해서는, 필요한 Props만을 전달하고, 필요하지 않은 Props는 하위 컴포넌트에서 직접 접근하는 것이 좋다. </p>
<p>이후 HttpAPI를 배울예정....
<del>(React Context, Redux 상태 관련 라이브러리)</del></p>
<hr>

<h3 id="props-예제">Props 예제</h3>
<h4 id="appjs">App.js</h4>
<pre><code class="language-js">import &#39;./App.css&#39;;

import PropsEx from &#39;./components/R01_props.js&#39;;
  return (
    &lt;&gt;
      &lt;h1&gt;Hello React!!&lt;/h1&gt;

      &lt;div&gt;리엑트 배운다~~&lt;/div&gt;

      &lt;PropsEx name={&#39;홍길동&#39;}/&gt;
      &lt;PropsEx name={&#39;김길동&#39;}/&gt;
      &lt;PropsEx name={&#39;이길동&#39;}/&gt;

    &lt;/&gt;
  );
}
export default App;
</code></pre>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/14bcd023-d750-4d74-a0b3-11744dc02921/image.png" alt=""></p>
<h4 id="r01_propsjs">R01_Props.js</h4>
<pre><code class="language-js">import React from &#39;react&#39;;

//props : 부모 컴포넌트가 자식 컴포넌트에게 
//      데이터 전달 시 사용하는 객체
//*** props는 자식 -&gt; 부모 데이터 전달 불가능 ***/

const ChildComponent = (props) =&gt; {
    return(
        &lt;&gt;
            &lt;ul&gt;
                &lt;li&gt;이름: {props.name}&lt;/li&gt;
                &lt;li&gt;나이: {props.age}&lt;/li&gt;
            &lt;/ul&gt;
        &lt;/&gt;
    );
}

const MenuPrin =(props) =&gt; {
    return(
        &lt;h4&gt;김밥 : {props.김밥} , 떡볶이 : {props.떡볶이}&lt;/h4&gt;
    );
}

const PropsEx = (props) =&gt; {
    //props 매개변수 : 부모로 부터 전달 받은 값이 담긴 객체

    // K:V 형태로 담겨 있음
    console.log(props);
    console.log(props.name);

    const menu = { &#39;김밥&#39; : 3000, &#39;떡볶이&#39; : 4000 };

    return(
        &lt;&gt;
            &lt;h1&gt;{props.name}&lt;/h1&gt;
            &lt;ChildComponent name={props.name} age={props.name === &#39;홍길동&#39; ? 20 :25}/&gt;
            &lt;MenuPrin {...menu}/&gt;
            {/* &lt;MenuPrin 김밥={3000}, 떡볶이={4000}/&gt; */}
        &lt;/&gt;
    );
}
export default PropsEx;</code></pre>
<h3 id="props-결과화면">props 결과화면</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/61438a6c-c2e5-4514-8a28-8f9256453095/image.png" alt=""></p>
<hr>
<hr>

<h2 id="state-상태">State 상태</h2>
<blockquote>
<p>컴포넌트의 상태를 나타내고 동적인 데이터를 다룰 때 사용한다. </p>
</blockquote>
<p><code>const [상태명, 상태변경함수명] = useState(초기값);</code>
컴포넌트 내부에서 관리되는 <code>상태 값</code>을 의미한다.
컴포넌트가 <code>생성</code>되고, <code>갱신될때마다 변경</code>될 수 있는 값이며,
이 값이 변경 될 때마다 화면이 다시 렌더링된다.
<code>state</code>는 <code>useState</code> Hook 을사용하여 컴포넌트 내부에서 관리할 수 있으며, setState 함수를 통해 값을 업데이트할 수 있습니다. </p>
<h3 id="state-예제-코드">state 예제 코드</h3>
<h4 id="r02_state1js">R02_state1.js</h4>
<pre><code class="language-js">import React, {useState} from &#39;react&#39;;

//컴포넌트 이름은 대문자로 작성!

// 리액트는 컴포넌트의 상태가 변할 때 마다 리렌더링을 수행 함
const InputTest = () =&gt; {

    const [inputValue, setInputValue] = useState(&quot;초기값&quot;)
    //        변수           함수

    // InputValue는 값을 저장하는 변수
    // setInputValue는 InputValue에 값을 대입하는 setter의 역할

    const changeInputValue = (e) =&gt; {
        console.log(e.target.value);
        setInputValue(e.target.value);
    }

    return(

        // 첫 렌더링 : vlaue = &quot;초기값&quot;
        // -&gt; input 의 값을 변경 (컴포넌트의 상태 변화 -&gt; 리렌더링 진행)
        //      1) onChage(값이 변했을 때)
        //          + onChangeInputValue 함수가 실행되면서
        //          inputValue에 e.target.value(변화된 값 대입)

        //      2) 컴포넌트의 상태 변화 -&gt; 리렌더링 진행

        // 리렌더링 -&gt; value = 변경된 inputValue의 값


        &lt;input type=&#39;text&#39; value={inputValue}
        onChange={changeInputValue}
        // onChange={(e)=&gt;{setInputValue(e.target.value)}}
        /&gt;
    );
}
export default InputTest;</code></pre>
<h4 id="r03_state3js">R03_state3.js</h4>
<pre><code class="language-js">import React, {useState} from &#39;react&#39;

const State2= (props) =&gt;{

    //props : 부모로 부터 값을 전달 받은 객체
    // props.init
    //const [conut , setConut] = useState(0);
    const [conut , setConut] = useState(props.init);


    // useState :컴포넌트의 상태를 관리할 때 사용하는 Hook
    // const [변수, 값을 변경하는 함수 (setter)] = useState(초기값);

    return(

        &lt;div&gt;
            &lt;h3&gt;{conut}&lt;/h3&gt;
            &lt;button onClick={()=&gt;setConut(conut +1)}&gt;클릭하면 1증가&lt;/button&gt;
        &lt;/div&gt;
    );

}

export default State2;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] Component란?]]></title>
            <link>https://velog.io/@ee_ji0/React-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ee_ji0/React-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 Sep 2023 03:48:59 GMT</pubDate>
            <description><![CDATA[<h1 id="react란">React란?</h1>
<p><code>리액트(React)</code>는 <code>페이스북</code>에서 제공해주는 프론트엔드 <code>라이브러리</code>이다.
<code>React</code>는 <code>컴포넌트(Component)</code> 기반으로 되어 있어 <code>conponent</code>에 데이터를 내려주면 개발자가 설계한 대로 UI가 만들어져 사용자에게 보여진다. 
리액트을 사용하지 않아도 html과 css로 충분히 화면을 만들수 있다!</p>
<p>그러나 html과 css로만 만들어진 웹페이지는 동적인 데이터를 UI에 뿌려주기에는 적합합지 ❌
리액트를 이용하면 사용자와 상호작용할 수 있는 UI를 손 쉽게 만들어 줄 수 있다!</p>
<p>❕❕따라서 동적인 화면을 만들기 위함❕❕</p>
<br>
<hr>
<br>


<h2 id="component란">Component란?</h2>
<p>리액트는 컴포넌트 기반의 라이브러리이다
 <code>component란?</code> 
특정 코드 뭉치를 다른 부분으로 인식하거나 재사용하기 위해 사용하는 코드를 블록단위를 말한다.
쉽게 말해, UI를 하나의 <code>큰 덩어리</code>로 생각한다면 <code>컴포넌트</code>는 그 덩어리를 이루는 <code>아주 작은 요소들</code>이라고 생각하면 된다! </p>
<p>따라서 작은 컴포넌트들은 다른 화면에서도 사용될 수 있는 재사용성을 가지고 있기 때문에 똑같은 코드를 반복적으로 입력할 필요가 없어 비교적 효율적이다. 
컴포넌트의 <code>종류</code>는 <code>클래스형</code>과 <code>함수형</code>으로 나누어 진다!</p>
<h3 id="component의-종류클래스형-함수형">Component의 종류(클래스형, 함수형)</h3>
<p><code>클래스형  컴포넌트</code> : React.Componet 클래스를 상속 받아 구현
<code>함수형 컴포넌트</code> : 함수로 구현</p>
<p>컴포넌트는 다른 컴포넌트를 포함할 수 있으며, <code>부모-자식 관계</code>를 가진다
부모 컴포넌트는 자식 컴포넌트에게 <code>props</code>라는 속성을 전달 할 수 있다.
자식 컴포넌트는 props를 통해 전달받은 값을 사용하여 UI를 렌더링 한다.</p>
<h4 id="fragment">fragment</h4>
<p>리액트 컴포넌트는 딱 하나의 요소만 반환할 수 있다
여러 요소를 반환하고 싶은 때는 부모 요소로 묶어준다.</p>
<p>이때 사용하는 것이 <code>fragment(&lt;&gt;&lt;/&gt;)</code> 반환 요소를 감쌀 때 사용하고 해석이 되지 않는다❗</p>
<br>
<hr>
<br>

<h3 id="클래스형-컴포넌트-활용하기exam1js">클래스형 컴포넌트 활용하기(Exam1.js)</h3>
<blockquote>
<p>💗_<strong>클래스형 컴포넌트 만들기</strong>_💗</p>
</blockquote>
<ol>
<li>Component 상속 만들기 </li>
<li>render() 함수 작성하기(필수)</li>
<li>만든 class를 export default 지정하기 (내보내기)
 -&gt; 다른 곳에서 import받을 수 있음</li>
</ol>
<p><mark style="background-color:pink">화면에 버튼을 누르면 1씩 Count가 증가되는 화면을 만들 것이다!</mark></p>
<p><del>import React, { Component } from &#39;react&#39;;
상속 받으려고 react 패키지에서 가지고 오는거 겠지..?</del>??</p>
<pre><code class="language-js">import React, { Component } from &#39;react&#39;;
// node-moduls 폴더에 있는 React 패키지를 가져옴 


class Exam1 extends Component{

    constructor(props){
        super(props);
        this.state = { count : 0 };
    }

    handleClick = () =&gt; {
        this.setState({ count : this.state.count + 1 });
    }

    // 화면 렌더링 시 
    // render() 함수에서 반환된 값이 화면에 출력됨
    render(){
        return(
            &lt;&gt;
                &lt;h2&gt;클래스형 컴포넌트&lt;/h2&gt;
                &lt;h1&gt;Count : {this.state.count}&lt;/h1&gt;
                &lt;button onClick = {this.handleClick}&gt;Increment&lt;/button&gt;
            &lt;/&gt;
        );
    }
}

export default Exam1;
</code></pre>
<p>이 때 까지 html, js로 <code>이벤트</code>을 줬던 방법과 다르다❕
js에서는 아래와 같이 코드를 작성했었음</p>
<pre><code class="language-js">document.getElementById(버튼).addEventListener(&quot;click&quot;, ()=&gt;{
        const count = document.getElementById(카운트);
        count.innerText = Number(count,innerText)+1;
}); 
</code></pre>
<p>➡️ <code>React</code>는 미리 <code>사용할 함수를 변수</code>로 지정해 두고 해당 함수를 사용해서 화면을 만들어 준다!
또한 분리하여 화면을 만들어 주기 때문에 <code>재사용성</code> <code>유지보수</code>👍👍👍👍
이전 코드 스타일과 완전히 다름❕🥲🫧</p>
<br>
<hr>
<br>



<h3 id="함수형-컴포넌트-활용하기exam2js">함수형 컴포넌트 활용하기(Exam2.js)</h3>
<blockquote>
<p>💗_<strong>함수형 컴포넌트 만들기</strong>_💗</p>
</blockquote>
<ol>
<li>함수생성하기</li>
<li>return 구문에 출력하고자 하는 html 코드 작성하기 </li>
<li>만든 함수를 export default 지정하기 (내보내기)
 -&gt; 다른 곳에서 import받을 수 있음</li>
</ol>
<p><mark style="background-color:pink">화면에 버튼을 누르면 1씩 Count가 감소하는 화면을 만들 것이다!</mark></p>
<pre><code class="language-js">import React, { useState } from &#39;react&#39;;

function Exam2(){

    const [count, setCount] = useState(100);
    // count 라는 변수에 초기 값 100대입
    // count 값(상태)을 변경 할 때 setCount 함수를 이용

    const handleClick = () =&gt; {
        setCount( () =&gt; { return count -1 })
    }

    return(
        &lt;&gt;
            &lt;h2&gt;함수형 컴포넌트&lt;/h2&gt;
            &lt;h1&gt; Conut : { count }&lt;/h1&gt;
            &lt;button onClick={handleClick}&gt;Decrement&lt;/button&gt;
        &lt;/&gt;
    );
}
export default Exam2
</code></pre>
<br>
<hr>
<br>

<h3 id="appjs로-만든-컴포넌트-추합하기❕❕❕❕">App.js로 만든 컴포넌트 추합하기❕❕❕❕</h3>
<p>약간 이 친구가 mian.jsp같은 느낌,,,,이 든다. 안녕,,,</p>
<p>import시 동등한 위치로 가고 싶다면 <code>./</code> 사용해 주어야한다❗</p>
<pre><code class="language-js">import &#39;./App.css&#39;;

// components 폴더의Exam1.js 를 가져와서 사용
// 사용 할때 이름을 Ex1으로 지정
import Ex1 from &#39;./components/Exam1&#39;;
import Ex2 from &#39;./components/Exam2&#39;;

function App() {
  // 리엑트의 컴포넌트는 딱 하나의 요소만을 반환할 수 있다
  // -&gt; 여러 요소를 반환하고 싶을 때는 부모 요소로 묶어 준다! 
  return (
    /*fragment(&lt;&gt;&lt;/&gt;) : 반화요소를 감쌀 때 사용, 해석 X*/
    &lt;&gt;
      {/* jsx 주석 */}
      &lt;h1&gt;Hello React!!&lt;/h1&gt;

      &lt;div&gt;리엑트 배운다~~&lt;/div&gt;

      &lt;Ex1/&gt;

      &lt;Ex2/&gt;
    &lt;/&gt;
  );
}

export default App;
</code></pre>
<br>
<hr>
<br>

<h3 id="출력화면">출력화면</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/00a6c2be-0809-4a57-a260-10ba1eae9678/image.png" alt="">
⬇️버튼을 하나씩 눌러 보자❗
<img src="https://velog.velcdn.com/images/ee_ji0/post/a4344041-8781-4def-8397-5efe240044d2/image.png" alt=""></p>
<h2 id="리액트-처음-다루어-보는데어색-그-자체💗">리액트 처음 다루어 보는데,,어색 그 자체,,💗</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 설치]]></title>
            <link>https://velog.io/@ee_ji0/React-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ee_ji0/React-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 Sep 2023 01:58:09 GMT</pubDate>
            <description><![CDATA[<h1 id="react설치하기">React설치하기</h1>
<ol>
<li><p><a href="https://nodejs.org/ko">node.js</a> &gt; 18.17.1 LTS 다운로드 &gt; 설치<br><img src="https://velog.velcdn.com/images/ee_ji0/post/b6124688-0b30-4e5a-b8e2-93d587cb3f32/image.png" alt=""></p>
</li>
<li><p>웹도우 검색창 &gt; PowerShell창 <code>관리자 권한</code>으로 열기 
<img src="https://velog.velcdn.com/images/ee_ji0/post/22ef65d8-b661-45b1-8aa1-d35b28c66018/image.png" alt=""></p>
</li>
<li><p>PowerShell 창 <code>node -v</code> 입력 후 설치 및 버전 확인하기
<img src="https://velog.velcdn.com/images/ee_ji0/post/bfea0956-8801-4377-b825-a55be11893d5/image.png" alt=""></p>
</li>
<li><p><code>get -help Set-ExecutionPolicy</code> 명령어 실행 후 <code>Y</code>입력</p>
</li>
<li><p><code>Set-ExecutionPolicy RemoteSigned</code> 명령어 실행후 <code>Y</code>입력</p>
</li>
<li><p>원하는 파일 위치에 폴더 생성하기 </p>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/8e782bb8-8a2c-4025-9a73-663517b55305/image.png" alt=""></p>
<ol start="7">
<li><p>그 위치로 VS Code 에서 해당 폴더 여기 
 ctrl + shift + ` 으로 터미널 실행. 혹은 상단 메뉴바의 Terminal  &gt;  new Terminal</p>
</li>
<li><p>터미널에 <code>npm install –global yarn</code> 으로 전역으로 yarn 설치하기</p>
<p> ++만약 여기서 실행 되지 않고 오류가 발생 한다면!</p>
<blockquote>
<p>[yarn 설치 안 될 때]
 윈도우 검색창 - 시스템 환경변수 편집 - 고급 - 환경변수 - Path - 새로 만들기 - nodejs 폴더 경로 추가 - VS Code 재실행</p>
</blockquote>
</li>
<li><p><code>yarn global add creat-react-app</code> 으로 전역에서 creat-react-app 을 사용하게끔 명령
어 실행</p>
</li>
<li><p><code>npx create-react-app my-app</code> 으로 my-app이라는 이름의 리액트 앱을 생성
[리엑트 앱 생성 에러 발생시]
<code>npx clear-npx-cache</code>명령어로 캐시 클리어 후 진행</p>
</li>
<li><p>리액트앱 생성 후 cd my-app 명령어로 내 리액 프로잭트로 경로 이동</p>
</li>
<li><p><code>npm start</code> or <code>yarn start</code>명령어로 프로 젝트 실행시키기 </p>
<pre><code>http://localhost:3000/ 접속 ( 자동실행 된다 )</code></pre><p><img src="https://velog.velcdn.com/images/ee_ji0/post/7fdc4a2d-6447-449b-ac7a-55391276cffc/image.png" alt=""></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 서버에 저장된 사진 삭제(scheduling)]]></title>
            <link>https://velog.io/@ee_ji0/Spring-%EC%84%9C%EB%B2%84%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%9C-%EC%82%AC%EC%A7%84-%EC%82%AD%EC%A0%9C</link>
            <guid>https://velog.io/@ee_ji0/Spring-%EC%84%9C%EB%B2%84%EC%97%90-%EC%A0%80%EC%9E%A5%EB%90%9C-%EC%82%AC%EC%A7%84-%EC%82%AD%EC%A0%9C</guid>
            <pubDate>Fri, 01 Sep 2023 02:55:04 GMT</pubDate>
            <description><![CDATA[<h1 id="🫧-서버에-저장된-사진-확인해보기">🫧 서버에 저장된 사진 확인해보기</h1>
<h3 id="mark-stylebackground-colorpink문제점mark-프로젝트-서버에-일정-시간마다-파일-업뎃하기"><mark style="background-color:pink">!문제점!</mark> 프로젝트 서버에 일정 시간마다 파일 업뎃하기!</h3>
<p>현재 진행 중인 서버의 images 파일에는 board 파일과 member파일이 있고 위의 폴더에는 사진이 게시글 사진과 프로필 사진이 각각 저장되어 있다.
그러나, 게시글 목록과 프로필이미지에서 사진을 수정 삭제 할 경우에 DB에는 변경된 사항들이 UPDATE와 DELETE을 통해 변경이 되지만 서버에는 변경 되지 않고 그대로 남아 있는 모습을 보인다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/0c6a486f-de9d-4bbc-9a9a-a85028d0e811/image.png" alt="">
<strong>202308145419_31115.png , 202308145419_95983.png</strong>
이 두 파일은 DB에 없는 사진 파일이다!
그런데 서버에는 존재하고 board파일에 쌓이는 중..^^
재거해 보자!</p>
</blockquote>
<h2 id="이때-사용할-수-있는-기능">이때 사용할 수 있는 기능</h2>
<blockquote>
<p>🚩** @Schedulrd **
Spring에서 제공하는 스케줄러로 시간에 따른 특정 작업의 순서를 저장하는 방법이다!</p>
</blockquote>
<h3 id="📍-scheduling-설정하기">📍 @Scheduling 설정하기</h3>
<p>@Scheduling 사용하기 위해서는 아래와 같이 <code>설정하는 과정</code> 이 필요하다</p>
<ul>
<li>1) servlet-context.xml -&gt; Namespaces 탭 -&gt; task 체크 후 저장</li>
<li>2) servlet-context.xml -&gt; Source 탭 -&gt; <a href="task:annotation-driven/">task:annotation-driven/</a> 추가</li>
</ul>
<h3 id="📍-scheduled-속성">📍 @Scheduled 속성</h3>
<ul>
<li><p>fixedDelay : 이전 작업이 끝난 시점으로 부터 고정된 시간(ms)을 설정.
@Scheduled(fixedRate = 10000) // 이전 작업이 시작된 후 10초 뒤에 실행</p>
<ul>
<li><p>fixedRate : 이전 작업이 수행되기 시작한 시점으로 부터 고정된 시간(ms)을 설정.
@Scheduled(fixedDelay  = 10000) // 이전 작업이 끝난 후 10초 뒤에 실행</p>
</li>
<li><p>cron 속성 : UNIX계열 잡 스케쥴러 표현식으로 작성 - cron=&quot;초 분 시 일 월 요일 [년도]&quot; - 요일 : 1(SUN) ~ 7(SAT) 
ex) 2019년 9월 16일 월요일 10시 30분 20초 cron=&quot;20 30 10 16 9 2 &quot; // 연도 생략 가능</p>
</li>
<li><p>특수문자 * : 모든 수. 
- : 두 수 사이의 값. ex) 10-15 -&gt; 10이상 15이하 
, : 특정 값 지정. ex) 3,4,7 -&gt; 3,4,7 지정 
/ : 값의 증가. ex) 0/5 -&gt; 0부터 시작하여 5마다 
? : 특별한 값이 없음. (월, 요일만 해당) 
L : 마지막. (월, 요일만 해당)</p>
</li>
<li><p>@Scheduled(cron=&quot;0 * * * * *&quot;) // 매 분마다 실행</p>
</li>
</ul>
</li>
</ul>
<h2 id="🫧-코드-작성해보자">🫧 코드 작성해보자!</h2>
<h3 id="💗imagedeleteschedulingjava">💗ImageDeleteScheduling.java</h3>
<pre><code class="language-java">package edu.kh.project.common.scheduling;

import java.io.File;
import java.util.Arrays;
import java.util.List;

import javax.servlet.ServletContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import edu.kh.project.board.model.service.BoardService;

// 스프링이 일정 시간 마다 해당 객체를 이용해서 코드를 수행
// == 스프링이 해당 클래스를 만들어서 관리를 해야한다
// == Bean으로 등록

@Component // @Controller, @Service, @Repository의 부모 어노테이션
            // Bean 등록을 하겠다고 명시하는 어노테이션
public class ImageDeleteScheduling {

    @Autowired
    private ServletContext servletContext;

    @Autowired
    private BoardService service;

    //@Scheduled(fixedDelay = 10000) // ms단위
    // 일(5초) -&gt; 10초 대기 -&gt; 일(5초) -&gt; 10초 대기

    //@Scheduled(fixedRate = 10000) 
    // 일(5초)
    // 대기(10초)

    //cron =&quot;초 분 시 일 월 요일 [년도]&quot;
    //@Scheduled(cron =&quot;0,30 * * * * *&quot;)
    @Scheduled(cron = &quot;0 0 * * * *&quot;) // 매 정시 (*시 0분 0초)
    public void test() {
        //System.out.println(&quot;스케줄러가 일정시간마자 자동으로 출력&quot;);

        System.out.println(&quot;----------게시판 DB, 서버가 불일치하는 파일 제거------------&quot;);

        // 서버에 저장된 파일 목록을 조회해서
        // DB에 저장된 파일 목록과 비교하여
        // 매칭되지 않는 서버 파일 제거


        // 1) 서버에 저장된 파일 목록 조회
        // -&gt; application객체를 이용해서 
        //         /resources/images/board의 실제 서버 경로를 얻어옴
        String filePath= servletContext.getRealPath(&quot;/resources/images/board&quot;);
        // C:\workspace\6_Framework\boardProject\src\main\webapp\resources\images\board

        // 2)filePath네 저장된 모든 파일 경로 읽어오기
        File path = new File(filePath);
        File[] imageArr = path.listFiles();

        // 배열 -&gt; List로 변환
        List&lt;File&gt; serverImageList = Arrays.asList(imageArr);


        /* // 확인(임시)
        for(File f :serverImageList) {
            System.out.println(f.toString());
        }*/

        // 3) DB 파일 목록 비교
        List&lt;String&gt; dbImageList = service.selectImageList();


        /*// 확인(임시)
        for(String s :dbImageList) {
            System.out.println(s);
        }*/

        // 4) 서버에 파일 목록이 있을 경우에 비교 시작
        if(!serverImageList.isEmpty()) {

            // 5) 서버 파일 목록 순차접근
            for(File server : serverImageList) {

                // 6) 서버에서 존재하는 파일이 
                //  DB(dbImageList)에 없다면 삭제

                //String[] temp =  server.toString().split(&quot;\\&quot;);
                //String s = temp[temp.length-1];

                //System.out.println(s);
                // C:\workspace\6_Framework\boardProject\src\main\webapp\resources\images\board\20230821141913_00001.png

                // getName() - spilt을 쓰지 않아도 
                // System.out.println(server.getName());

                //List.indexOf(객체) = 객체가 List에 있으면 인덱스 반환
                //                      없으면 -1반환

                if(dbImageList.indexOf(server.getName()) == -1){
                    //db파일 목록             서버 파일 이름

                    System.out.println(server.getName() + &quot;삭제&quot;);
                    server.delete(); //File.delete() : 파일 삭제
                }
            } // for문 종료
             System.out.println(&quot;-----------이미지 파일 삭제 스케줄러 종료----------------&quot;);

        }
    }

}
</code></pre>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/d7c4cb9a-9c29-430f-945c-a12ed9ed718e/image.png" alt="">
일정시간이 지나면 알아서 사진 파일 업뎃하고 삭제한다! 
<img src="https://velog.velcdn.com/images/ee_ji0/post/4f988fde-9a10-4694-901a-5afab33e9218/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring ] 게시글 검색]]></title>
            <link>https://velog.io/@ee_ji0/Spring-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EA%B2%80%EC%83%89</link>
            <guid>https://velog.io/@ee_ji0/Spring-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EA%B2%80%EC%83%89</guid>
            <pubDate>Thu, 31 Aug 2023 05:00:37 GMT</pubDate>
            <description><![CDATA[<h4 id="mark-style-background-colorpink게시글-목록-조회에서-검색어-입력시-관련-게시글이-검색되도록-해보자-mark"><mark style= background-color:pink>게시글 목록 조회에서 검색어 입력시 관련 게시글이 검색되도록 해보자 </mark></h4>
<p>이전에 구현한 게시글 목록 조회에 참고해서 추가 기능을 넣어 볼 예정이다 .</p>
<h3 id="🫧-boardlistjsp">🫧 boardList.jsp</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/c98c6f60-33d6-4bf1-9f00-5aa0892b5cb0/image.png" alt=""></p>
<pre><code class="language-jsp">         &lt;%-- 검색을 진행한 경우 파라미터 (key,query)를
            쿼리스트링 태로 저장한 변수  --%&gt;
        &lt;c:if test=&quot;${!empty param.key}&quot; &gt;
            &lt;c:set var=&quot;search&quot; value=&quot;&amp;key=${param.key}&amp;query=${param.query}&quot;/&gt;
        &lt;/c:if&gt;


         &lt;div class=&quot;pagination-area&quot;&gt;


                &lt;ul class=&quot;pagination&quot;&gt;

                    &lt;!-- 첫 페이지로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=1${search}&quot;&gt;&amp;lt;&amp;lt;&lt;/a&gt;&lt;/li&gt;

                    &lt;!-- 이전 목록 마지막 번호로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${pagination.prevPage}${search}&quot;&gt;&amp;lt;&lt;/a&gt;&lt;/li&gt;


                    &lt;!-- 특정 페이지로 이동 --&gt;
                    &lt;c:forEach var=&quot;i&quot; begin=&quot;${pagination.startPage}&quot;
                                end=&quot;${pagination.endPage}&quot; step=&quot;1&quot;&gt;
                        &lt;c:choose&gt;
                           &lt;c:when test=&quot;${i== pagination.currentPage}&quot;&gt;
                                &lt;!-- 현재 보고있는 페이지 --&gt;
                                &lt;li&gt;&lt;a class=&quot;current&quot;&gt;${i}&lt;/a&gt;&lt;/li&gt;
                           &lt;/c:when&gt;

                           &lt;c:otherwise&gt;
                                &lt;!-- 현재 페이지를 제외한 나머지 --&gt;
                                &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${i}${search}&quot;&gt;${i}&lt;/a&gt;&lt;/li&gt;
                           &lt;/c:otherwise&gt;
                        &lt;/c:choose&gt;
                    &lt;/c:forEach&gt;

                    &lt;!-- 다음 목록 시작 번호로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${pagination.nextPage}${search}&quot;&gt;&amp;gt;&lt;/a&gt;&lt;/li&gt;

                    &lt;!-- 끝 페이지로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${pagination.maxPage}${search}&quot;&gt;&amp;gt;&amp;gt;&lt;/a&gt;&lt;/li&gt;

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




            &lt;!-- 검색창 --&gt;
            &lt;form action=&quot;${boardCode}&quot; method=&quot;get&quot; id=&quot;boardSearch&quot;&gt;

                &lt;select name=&quot;key&quot; id=&quot;searchKey&quot;&gt;
                    &lt;option value=&quot;t&quot;&gt;제목&lt;/option&gt;
                    &lt;option value=&quot;c&quot;&gt;내용&lt;/option&gt;
                    &lt;option value=&quot;tc&quot;&gt;제목+내용&lt;/tion&gt;
                    &lt;option value=&quot;w&quot;&gt;작성자&lt;/option&gt;
                &lt;/select&gt;

                &lt;input type=&quot;text&quot; name=&quot;query&quot;  id=&quot;searchQuery&quot; placeholder=&quot;검색어를 입력해주세요.&quot;&gt;

                &lt;button&gt;검색&lt;/button&gt;
            &lt;/form&gt;</code></pre>
<h3 id="🫧-boardlistjava">🫧 boardList.java</h3>
<pre><code class="language-js">// 검색창 이전의 기록 남겨 놓기 
const boardSearch = document.querySelector(&quot;#boardSearch&quot;);
const searchKey = document.querySelector(&quot;#searchKey&quot;);
const searchQuery = document.querySelector(&quot;#searchQuery&quot;);
const options = document.querySelectorAll(&quot;#searchKey &gt; option&quot;);

(()=&gt;{
    const params = new URL(location.href).searchParams;
    const key = params.get(&quot;key&quot;); //t,c,tc,w중  하나
    const query = params.get(&quot;query&quot;); // 검색어

    if(key != null){ // 검색을 했을 때
        searchQuery.value=query; // 검색어를 화면에 출력 

        // option태그를 하나씩 순차접근해서 value가 key랑 같으면
        // selected속성 추가
        for(let op of options){
            if(op.value == key){
                op.selected = true;
            }

        }
    }
})();

//검색어 없이 제출된 경우 
boardSearch.addEventListener(&quot;submit&quot;, e =&gt;{
    if(searchQuery.value.trim().length == 0){
        e.preventDefault();

        location.pathname//해당게시판 1페이지로 이동 

        //location.pathname : 쿼리스트링을 제외한 실제 주소
    }
})</code></pre>
<h3 id="🫧-boardcontrollerjava">🫧 boardController.java</h3>
<p>if문으로 게시글 검색이 있을 경우와 없을 경우를 만들어 주었다 </p>
<pre><code class="language-java">    //게시글 목록 조회
    @GetMapping(&quot;/{boardCode:[0-9]+}&quot;) // boardCode는 1자리 이상 숫자
    public String selectBoardList(@PathVariable(&quot;boardCode&quot;) int boardCode
            , @RequestParam(value=&quot;cp&quot;, required = false, defaultValue = &quot;1&quot;) int cp
            , Model model
            , @RequestParam Map&lt;String, Object&gt; paramMap //파라미터가 전부다 담겨 있음 
            ) {

        // boardCode 확인
        //System.out.println(&quot;boardCode: &quot;+ boardCode);

        if( paramMap.get(&quot;key&quot;) == null ) {//검색어가 없을 때 

            // 게시글을 목록 조회하는 service호출 
            Map&lt;String, Object&gt; map = service.selectBoardList(boardCode, cp);

            //조회 결과를 request scope에 세팅 후 forward
            model.addAttribute(&quot;map&quot;, map);

        }else { //검색어가 있다 (검색 O)

            paramMap.put(&quot;boardCode&quot;, boardCode);

            Map&lt;String , Object&gt; map = service.selectBoardList(paramMap, cp);

            model.addAttribute(&quot;map&quot;, map);
        }
        return &quot;board/boardList&quot;;
    }</code></pre>
<h3 id="🫧-boardservicejava">🫧 boardService.java</h3>
<pre><code class="language-java">    /** 검색어를 통한 게시글 목록 조회(검색)
     * @param paramMap
     * @param cp
     * @return boardList
     */
    Map&lt;String, Object&gt; selectBoardList(Map&lt;String, Object&gt; paramMap, int cp);</code></pre>
<h3 id="🫧-boardserviceimpljava">🫧 boardServiceImpl.java</h3>
<p>매개변수의 자료형이 달라서 overlodding되어 메소드 사용할 수 있다!</p>
<pre><code class="language-java">    // 게시글 목록 조회 (검색)
    @Override
    public Map&lt;String, Object&gt; selectBoardList(Map&lt;String, Object&gt; paramMap, int cp) {

        // 1. 특정 게시판에 삭제되지 않고 검색 조건이 일치하는 게시글 수 조회
        int listCount = dao.getListCount(paramMap);

        // 2. 1번 조회 결과 + cp를 이용해서 Pagination 객체 생성
        // -&gt; 내부에 필드가 모두 계산 되어서 초기화 됨
        Pagination pagination = new Pagination(cp, listCount);


        // 3. 특정게시판에서
        // 현재 페이지에 해당하는 게시글 목록 조회
        // + 단, 검색 조건이 일치하는 글만 
        List&lt;Board&gt; boardList = dao.selectBoardList(pagination, paramMap);

        // 4. paginatio, boardList를 Map에 담아서 반환
        Map&lt;String, Object&gt; map = new HashMap&lt;String, Object&gt;();

        map.put(&quot;pagination&quot;, pagination);
        map.put(&quot;boardList&quot;, boardList);

        return map;
    }</code></pre>
<h3 id="🫧-boarddaojava">🫧 boardDAO.java</h3>
<pre><code class="language-java">    /** 게시글 수 조회(검색)
     * @param paramMap
     * @return listCount
     */
    public int getListCount(Map&lt;String, Object&gt; paramMap) {
        return sqlSession.selectOne(&quot;boardMapper.getListCount_search&quot;, paramMap);
    }


    /** 게시글 목록 조회(검색)
     * @param pagination
     * @param paramMap
     * @return boardList
     */
    public List&lt;Board&gt; selectBoardList(Pagination pagination, Map&lt;String, Object&gt; paramMap) {

        // 1) offset 계산
        int offset 
        = (pagination.getCurrentPage() -1) * pagination.getLimit();

        // 2) Row Bounds객체 생성
        RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());

        // 3) selectList(&quot;namespace.id&quot;, 파라미터, Rowbounds)호출
        return sqlSession.selectList(&quot;boardMapper.selectBoardList_search&quot;, paramMap, rowBounds);
    }</code></pre>
<h3 id="🫧-board-mapperxml">🫧 board-mapper.xml</h3>
<p>동적 SQL사용을 사용하여 조회해 보았다(if, choose,CDATA)</p>
<pre><code class="language-xml">    &lt;!-- 특정게시판에 삭제되지 않고 검색 조건에 일치하는 게시글 수 조회 --&gt;
    &lt;select id=&quot;getListCount_search&quot;  resultType=&quot;_int&quot;&gt;
        SELECT COUNT(*)
        FROM BOARD

        &lt;!-- 작성자 검색일 경우 --&gt;
        &lt;if test=&#39;key==&quot;w&quot;&#39;&gt;
            JOIN MEMBER USING(MEMBER_NO)
        &lt;/if&gt;

        WHERE BOARD_DEL_FL = &#39;N&#39;
        AND BOARD_CODE = #{boardCode}


        &lt;choose&gt;
            &lt;when test=&#39;key== &quot;t&quot;&#39;&gt;
                &lt;!-- 제목 --&gt;
                AND BOARD_TITLE LIKE &#39;%${query}%&#39;
            &lt;/when&gt;
            &lt;when test=&#39;key==&quot;c&quot;&#39;&gt;
                &lt;!-- 내용 --&gt;
                AND BOARD_CONTENT LIKE &#39;%${query}%&#39;
            &lt;/when&gt;
            &lt;when test=&#39;key==&quot;tc&quot;&#39;&gt;
                &lt;!-- 제목 + 내용 --&gt;
                AND (BOARD_TITLE LIKE &#39;%${query}%&#39; OR BOARD_CONTENT LIKE &#39;%${query}%&#39;)
            &lt;/when&gt;
            &lt;when test=&#39;key==&quot;w&quot;&#39;&gt;
                &lt;!-- 작성자(닉네임) --&gt;
                AND MEMBER_NICKNAME LIKE &#39;%#{query}%&#39;            
            &lt;/when&gt;
        &lt;/choose&gt;        
    &lt;/select&gt;

    &lt;!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급--&gt;
    &lt;!-- 게시글 목록 조회 --&gt;
    &lt;select id=&quot;selectBoardList_search&quot; resultMap=&quot;board_rm&quot;&gt;
        SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT, 

         &lt;![CDATA[
            CASE  
               WHEN SYSDATE - B_CREATE_DATE &lt; 1/24/60
               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || &#39;초 전&#39;
               WHEN SYSDATE - B_CREATE_DATE &lt; 1/24
               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || &#39;분 전&#39;
               WHEN SYSDATE - B_CREATE_DATE &lt; 1
               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || &#39;시간 전&#39;
               ELSE TO_CHAR(B_CREATE_DATE, &#39;YYYY-MM-DD&#39;)
            END B_CREATE_DATE,
         ]]&gt;

         (SELECT COUNT(*) FROM &quot;COMMENT&quot; C
          WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT,

         (SELECT COUNT(*) FROM BOARD_LIKE L
          WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,

         (SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
         WHERE I.BOARD_NO = B.BOARD_NO
         AND IMG_ORDER = 0) THUMBNAIL
      FROM &quot;BOARD&quot; B
      JOIN &quot;MEMBER&quot; USING(MEMBER_NO)
      WHERE BOARD_DEL_FL = &#39;N&#39;
      AND BOARD_CODE = #{boardCode}

      &lt;choose&gt;
            &lt;when test=&#39;key== &quot;t&quot;&#39;&gt;
                &lt;!-- 제목 --&gt;
                AND BOARD_TITLE LIKE &#39;%${query}%&#39;
            &lt;/when&gt;
            &lt;when test=&#39;key==&quot;c&quot;&#39;&gt;
                &lt;!-- 내용 --&gt;
                AND BOARD_CONTENT LIKE &#39;%${query}%&#39;
            &lt;/when&gt;
            &lt;when test=&#39;key==&quot;tc&quot;&#39;&gt;
                &lt;!-- 제목 + 내용 --&gt;
                AND (BOARD_TITLE LIKE &#39;%${query}%&#39; OR BOARD_CONTENT LIKE &#39;%${query}%&#39;)
            &lt;/when&gt;
            &lt;when test=&#39;key==&quot;w&quot;&#39;&gt;
                &lt;!-- 작성자(닉네임) --&gt;
                AND MEMBER_NICKNAME LIKE &#39;%#{query}%&#39;            
            &lt;/when&gt;
        &lt;/choose&gt;

      ORDER BY BOARD_NO DESC
    &lt;/select&gt;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 게시판 댓글]]></title>
            <link>https://velog.io/@ee_ji0/Spring-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%8C%93%EA%B8%80</link>
            <guid>https://velog.io/@ee_ji0/Spring-%EA%B2%8C%EC%8B%9C%ED%8C%90-%EB%8C%93%EA%B8%80</guid>
            <pubDate>Wed, 30 Aug 2023 04:59:06 GMT</pubDate>
            <description><![CDATA[<h4 id="부모-댓글-자식-댓글-구현">부모 댓글 자식 댓글 구현</h4>
<p>게시판에 아래와 같이 댓글과 그 댓글 의 답글을 다는 코드를 구현해 보려고 한다. 
결과 화면은 아래와 같다</p>
<h2 id="🫧db-데이터-입력">🫧DB 데이터 입력</h2>
<p>기능 구현에 필요한 더미 데이터를 입력해 보자</p>
<blockquote>
<p>현재 게시글 번호가 &#39;1506&#39;인 게시글에는 댓글이 없다
댓글을 넣어주자</p>
</blockquote>
<pre><code class="language-sql">SELECT COMMENT_NO, COMMENT_CONTENT,
    TO_CHAR(C_CREATE_DATE, &#39;YYYY&quot;년&quot; MM&quot;월&quot; DD&quot;일&quot; HH24&quot;시&quot; MI&quot;분&quot; SS&quot;초&quot;&#39;)C_CREATE_DATE,
    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM &quot;COMMENT&quot;
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = 1506
ORDER BY COMMENT_NO;</code></pre>
<p>부모 댓글 2개 INSERT하기 </p>
<pre><code class="language-SQL">INSERT INTO &quot;COMMENT&quot;
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;부모 댓글1&#39;, DEFAULT, DEFAULT, 1506, 1, NULL);
INSERT INTO &quot;COMMENT&quot;                                     -- 
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;부모 댓글2&#39;, DEFAULT, DEFAULT, 1506, 1, NULL);</code></pre>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/b0a15823-720e-40d5-b519-f5f7d4ef3f36/image.png" alt=""></p>
<blockquote>
<p>하위 자식 댓글  INSERT하기 </p>
</blockquote>
<pre><code class="language-SQL">INSERT INTO &quot;COMMENT&quot;                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;자식 댓글1-1&#39;, DEFAULT, DEFAULT, 1506, 1, 1022);
INSERT INTO &quot;COMMENT&quot;                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;자식 댓글1-2&#39;, DEFAULT, DEFAULT, 1506, 1, 1022);
INSERT INTO &quot;COMMENT&quot;                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;자식 댓글1-3&#39;, DEFAULT, DEFAULT, 1506, 1, 1022);
---------------------------
INSERT INTO &quot;COMMENT&quot;                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;자식 댓글2-1&#39;, DEFAULT, DEFAULT, 1506, 1, 1023);
INSERT INTO &quot;COMMENT&quot;                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;자식 댓글2-2&#39;, DEFAULT, DEFAULT, 1506, 1, 1023);
INSERT INTO &quot;COMMENT&quot;                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, &#39;자식 댓글2-3&#39;, DEFAULT, DEFAULT, 1506, 1, 1023);</code></pre>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/5570e99f-bc0a-44a3-8d82-7f823aa88f1e/image.png" alt=""></p>
<hr>

<h2 id="🫧db입력이-끝났으면-조회을-해보자">🫧DB입력이 끝났으면 조회을 해보자!</h2>
<blockquote>
<p>*<em>계층형 쿼리 조회하기 *</em>
계층형 쿼리(START WITH, CONNECT BY, ORDER SIBLINGS BY)
    - 상위 타입과 하위 타입간의 관계를 계층식으로 표현 할 수 있게 하는 질의어(SELECT)
<br>
    - START WITH : 상위 타입(최상위 부모)으로 사용될 행을 지정 (서브쿼리로 지정 가능)
<br>
    - CONNECT BY 
    -&gt; 상위 타입과 하위 타입 사이의 관계를 규정
    -&gt; PRIOR(이전의) 연산자와 같이 사용하여
         현재 행 이전에 상위 타입 또는 하위 타입이 있을지 규정
<br>
    1) 부모 -&gt; 자식 계층 구조
         CONNECT BY PRIOR 자식 컬럼 = 부모 컬럼
     2) 자식 -&gt; 부모 계층 구조
         CONNECT BY PRIOR 부모 컬럼 = 자식 컬럼
<br>
    - ORDER SIBLINGS BY : 계층 구조 정렬
<br>
    ** 계층형 쿼리가 적용 SELECT 해석 순서 **
<br>
    5 : SELECT
    1 : FROM (+JOIN)
    4 : WHERE
    2 : START WITH
    3 : CONNECT BY
    6 : ORDER SIBLINGS BY
<br><br>    - WHERE절의 계층형 쿼리 보다 순서가 늦기 때문에 
       먼저 조건을 반영하고 싶은 경우 FROM절 서브쿼리(인라인뷰)을 이용</p>
</blockquote>
<pre><code class="language-sql">     SELECT LEVEL, C.* FROM 
      (SELECT COMMENT_NO, COMMENT_CONTENT,
          TO_CHAR(C_CREATE_DATE, &#39;YYYY&quot;년&quot; MM&quot;월&quot; DD&quot;일&quot; HH24&quot;시&quot; MI&quot;분&quot; SS&quot;초&quot;&#39;)C_CREATE_DATE,
          BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
      FROM &quot;COMMENT&quot;
      JOIN MEMBER USING(MEMBER_NO)
      WHERE BOARD_NO = 1507) C
      WHERE COMMENT_DEL_FL=&#39;N&#39;
      START WITH PARENT_NO IS NULL
      CONNECT BY PRIOR COMMENT_NO= PARENT_NO 
      ORDER SIBLINGS BY COMMENT_NO
;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 댓/답글 기능 구현 ]]></title>
            <link>https://velog.io/@ee_ji0/Spring-%EB%8C%93%EB%8B%B5%EA%B8%80-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ee_ji0/Spring-%EB%8C%93%EB%8B%B5%EA%B8%80-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Wed, 30 Aug 2023 04:57:10 GMT</pubDate>
            <description><![CDATA[<h4 id="💗너무-어려운-댓글-기능-구현화이또🫶">💗너무 어려운 댓글 기능 구현..화이또,,,🫶</h4>
<h2 id="🫧vs-code">🫧VS Code</h2>
<h3 id="💗commentjsp">💗comment.jsp</h3>
<pre><code class="language-jsp">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot;  %&gt;

&lt;div id=&quot;commentArea&quot;&gt;
    &lt;!-- 댓글 목록 --&gt;
    &lt;div class=&quot;comment-list-area&quot;&gt;

        &lt;ul id=&quot;commentList&quot;&gt;

            &lt;c:forEach items=&quot;${board.commentList}&quot; var=&quot;comment&quot;&gt;
                &lt;!-- 부모/자식 댓글 --&gt;
                &lt;li class=&quot;comment-row &lt;c:if test=&#39;${comment.parentNo !=0 }&#39;&gt;child-comment&lt;/c:if&gt;&quot;&gt;
                    &lt;p class=&quot;comment-writer&quot;&gt;

                        &lt;!-- 프로필 이미지 --&gt;
                        &lt;c:if test=&quot;${empty comment.profileImage}&quot; &gt;
                            &lt;%-- 없을 경우 --%&gt;
                            &lt;img src=&quot;/resources/images/user.png&quot;&gt;
                        &lt;/c:if&gt;
                        &lt;c:if test=&quot;${!empty comment.profileImage}&quot; &gt;
                            &lt;%-- 있을 경우 --%&gt;
                            &lt;img src=&quot;${comment.profileImage}&quot;&gt;
                        &lt;/c:if&gt;

                        &lt;!-- 닉네임 --&gt;
                        &lt;span&gt;${comment.memberNickname}&lt;/span&gt;

                        &lt;!-- 작성일 --&gt;
                        &lt;span class=&quot;comment-date&quot;&gt;&lt;/span&gt;
                    &lt;/p&gt;

                    &lt;!-- 댓글 내용 --&gt;
                    &lt;p class=&quot;comment-content&quot;&gt;${comment.commentContent}&lt;/p&gt;




                    &lt;!-- 버튼 영역 --&gt;
                    &lt;div class=&quot;comment-btn-area&quot;&gt;
                        &lt;button onclick=&quot;showInsertComment(${comment.commentNo},this)&quot;&gt;답글&lt;/button&gt;   

                        &lt;!-- 로그인 회원과 댓글 작성자가 같은 경우 --&gt;  
                        &lt;c:if test=&quot;${loginMember.memberNo == comment.memberNo}&quot; &gt;
                            &lt;button onclick=&quot;showUpdateComment(${comment.commentNo},this)&quot;&gt;수정&lt;/button&gt;     
                            &lt;button onclick=&quot;deleteComment(${comment.commentNo})&quot;&gt;삭제&lt;/button&gt;
                        &lt;/c:if&gt;
                    &lt;/div&gt;
                &lt;/li&gt;
            &lt;/c:forEach&gt;


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

    &lt;!-- 댓글 작성 부분 --&gt;
    &lt;div class=&quot;comment-write-area&quot;&gt;
        &lt;textarea id=&quot;commentContent&quot;&gt;&lt;/textarea&gt;
        &lt;button id=&quot;addComment&quot;&gt;
            댓글&lt;br&gt;
            등록
        &lt;/button&gt;

    &lt;/div&gt;

&lt;/div&gt;</code></pre>
<h3 id="💗commentjs">💗comment.js</h3>
<pre><code class="language-js">// 댓글 목록 조회
function selectCommentList(){

    // REST(REpresentational State Transfer) API
    // - 자원을 이름으로 구분(Repersentational)하여
    //   자원의 상태(State)을 주고 받는 것(Transfer)

    // -&gt; 주소를 명시하고 
    // http Method(GET, POST, DELETE)를 이용해
    // 지정된 자원에 대한 CRUD 진행

    // Create : 생성 (POST)
    // Read   : 조회 (GET)
    // Update : 수정 (PUT,PETCH)
    // Delete : 삭제 (DELETE)

    // 기본적으로 form 태그 GET/POST만 지원


    fetch(&quot;/comment?boardNo=&quot;+ boardNo) //GET방식은 주소에 파라미터를 담아서 전달
    .then(response =&gt; response.json()) // 응답 객체 -&gt; 파싱
    .then(cList =&gt; {  // cList :댓글 목록 (객체 배열)
        console.log(cList);

        // 화면에 출력되어 있는 댓글 목록 삭제
        const commentList = document.getElementById(&quot;commentList&quot;); // ul태그
        commentList.innerHTML = &quot;&quot;;

        // cList에 저장된 요소를 하나씩 접근
        for(let comment of cList){

            // 행
            const commentRow = document.createElement(&quot;li&quot;);
            commentRow.classList.add(&quot;comment-row&quot;);

            // 답글일 경우 child-comment 클래스 추가
            if(comment.parentNo != 0)  commentRow.classList.add(&quot;child-comment&quot;);


            // 작성자
            const commentWriter = document.createElement(&quot;p&quot;);
            commentWriter.classList.add(&quot;comment-writer&quot;);

            // 프로필 이미지
            const profileImage = document.createElement(&quot;img&quot;);

            if( comment.profileImage != null ){ // 프로필 이미지가 있을 경우
                profileImage.setAttribute(&quot;src&quot;, comment.profileImage);
            }else{ // 없을 경우 == 기본이미지
                profileImage.setAttribute(&quot;src&quot;, &quot;/resources/images/user.png&quot;);
            }

            // 작성자 닉네임
            const memberNickname = document.createElement(&quot;span&quot;);
            memberNickname.innerText = comment.memberNickname;

            // 작성일
            const commentDate = document.createElement(&quot;span&quot;);
            commentDate.classList.add(&quot;comment-date&quot;);
            commentDate.innerText =  &quot;(&quot; + comment.commentCreateDate + &quot;)&quot;;

            // 작성자 영역(p)에 프로필,닉네임,작성일 마지막 자식으로(append) 추가
            commentWriter.append(profileImage , memberNickname , commentDate);



            // 댓글 내용
            const commentContent = document.createElement(&quot;p&quot;);
            commentContent.classList.add(&quot;comment-content&quot;);
            commentContent.innerHTML = comment.commentContent;

            // 행에 작성자, 내용 추가
            commentRow.append(commentWriter, commentContent);


            // 로그인이 되어있는 경우 답글 버튼 추가
            if(loginMemberNo != &quot;&quot;){
                // 버튼 영역
                const commentBtnArea = document.createElement(&quot;div&quot;);
                commentBtnArea.classList.add(&quot;comment-btn-area&quot;);

                // 답글 버튼
                const childCommentBtn = document.createElement(&quot;button&quot;);
                childCommentBtn.setAttribute(&quot;onclick&quot;, &quot;showInsertComment(&quot;+comment.commentNo+&quot;, this)&quot;);
                childCommentBtn.innerText = &quot;답글&quot;;

                // 버튼 영역에 답글 버튼 추가
                commentBtnArea.append(childCommentBtn);

                // 로그인한 회원번호와 댓글 작성자의 회원번호가 같을 때만 버튼 추가
                if( loginMemberNo == comment.memberNo   ){

                    // 수정 버튼
                    const updateBtn = document.createElement(&quot;button&quot;);
                    updateBtn.innerText = &quot;수정&quot;;

                    // 수정 버튼에 onclick 이벤트 속성 추가
                    updateBtn.setAttribute(&quot;onclick&quot;, &quot;showUpdateComment(&quot;+comment.commentNo+&quot;, this)&quot;);                        


                    // 삭제 버튼
                    const deleteBtn = document.createElement(&quot;button&quot;);
                    deleteBtn.innerText = &quot;삭제&quot;;
                    // 삭제 버튼에 onclick 이벤트 속성 추가
                    deleteBtn.setAttribute(&quot;onclick&quot;, &quot;deleteComment(&quot;+comment.commentNo+&quot;)&quot;);                       


                    // 버튼 영역 마지막 자식으로 수정/삭제 버튼 추가
                    commentBtnArea.append(updateBtn, deleteBtn);

                } // if 끝


                // 행에 버튼영역 추가
                commentRow.append(commentBtnArea); 
            }


            // 댓글 목록(ul)에 행(li)추가
            commentList.append(commentRow);
        }


    })
    .catch(err =&gt; console.log(err));

}


//-------------------------------------------------------------------------------------------------


// 댓글 등록
const addComment = document.getElementById(&quot;addComment&quot;);
const commentContent = document.getElementById(&quot;commentContent&quot;);

addComment.addEventListener(&quot;click&quot;, e =&gt; { // 댓글 등록 버튼이 클릭이 되었을 때

    // 1) 로그인이 되어있나? -&gt; 전역변수 memberNo 이용
    if(loginMemberNo == &quot;&quot;){ // 로그인 X
        alert(&quot;로그인 후 이용해주세요.&quot;);
        return;
    }

    // 2) 댓글 내용이 작성되어있나?
    if(commentContent.value.trim().length == 0){ // 미작성인 경우
        alert(&quot;댓글을 작성한 후 버튼을 클릭해주세요.&quot;);

        commentContent.value = &quot;&quot;; // 띄어쓰기, 개행문자 제거
        commentContent.focus();
        return;
    }

    // 3) AJAX를 이용해서 댓글 내용 DB에 저장(INSERT)

    const data={ &quot;commentContent&quot;  : commentContent.value
                , &quot;memberNo&quot;       : loginMemberNo
                , &quot;boardNo&quot;        : boardNo}; //JS객체

    fetch(&quot;/comment&quot;, {
         method: &quot;POST&quot;
        ,headers:{&quot;Content-Type&quot;: &quot;application/json&quot;}
        ,body : JSON.stringify(data) //JS객체 -&gt; JSON으로 파싱
    })
    .then(resp =&gt; resp.text())
    .then(result =&gt; {
        if(result &gt; 0){ // 등록 성공
            alert(&quot;댓글이 등록되었습니다.&quot;);

            commentContent.value = &quot;&quot;; // 작성했던 댓글 삭제

            selectCommentList(); // 비동기 댓글 목록 조회 함수 호출
            // -&gt; 새로운 댓글이 추가되어짐

        } else { // 실패
            alert(&quot;댓글 등록에 실패했습니다...&quot;);
        }
    })
    .catch(err =&gt; console.log(err));
});


// -----------------------------------------------------------------------------------
// 댓글 삭제
function deleteComment(commentNo){

    if( confirm(&quot;정말로 삭제 하시겠습니까?&quot;) ){

        fetch(&quot;/comment&quot;,{
            method: &quot;DELETE&quot;
            ,headers: {&quot;Content-Type&quot;: &quot;application/json&quot;}
            ,body : commentNo //값을 하나만 전달시 JSON 필요없음

        })
        .then(resp =&gt; resp.text())
        .then(result =&gt; {
            if(result &gt; 0){
                alert(&quot;삭제되었습니다&quot;);
                selectCommentList(); // 목록을 다시 조회해서 삭제된 글을 제거
            }else{
                alert(&quot;삭제 실패&quot;);
            }
        })
        .catch(err =&gt; console.log(err));

    }
}




// ------------------------------------------------------------------------------------------
// 댓글 수정 화면 전환 

let beforeCommentRow; // 수정 전 원래 행의 상태를 저장할 변수


function showUpdateComment(commentNo, btn){
                     // 댓글번호, 이벤트발생요소(수정버튼)

    // ** 댓글 수정이 한 개만 열릴 수 있도록 만들기 **
    // 댓글 수정을 위한 textarea를 모두 얻어옴 -&gt; 수정이 활성화 되어 있을 경우 1개, 없으면 0개
    const temp = document.getElementsByClassName(&quot;update-textarea&quot;);  

    if(temp.length &gt; 0){ // 수정이 한 개 이상 열려 있는 경우

        if(confirm(&quot;다른 댓글이 수정 중입니다. 현재 댓글을 수정 하시겠습니까?&quot;)){ // 확인

            temp[0].parentElement.innerHTML = beforeCommentRow;
            // comment-row                       // 백업한 댓글
            // 백업 내용으로 덮어 씌워 지면서 textarea 사라짐

        }else{ // 취소
            return;
        }
    }


    // 1. 댓글 수정이 클릭된 행을 선택
    const commentRow = btn.parentElement.parentElement; // 수정 버튼의 부모의 부모

    // 2. 행 내용 삭제 전 현재 상태를 저장(백업) (문자열)
    //    (전역변수 이용)
    beforeCommentRow = commentRow.innerHTML;


    // 3. 댓글에 작성되어 있던 내용만 얻어오기 -&gt; 새롭게 생성된 textarea 추가될 예정

    let beforeContent = commentRow.children[1].innerHTML;

    // 이것도 가능!
    //let beforeContent = btn.parentElement.previousElementSibling.innerHTML;


    // 4. 댓글 행 내부 내용을 모두 삭제
    commentRow.innerHTML = &quot;&quot;;

    // 5. textarea 요소 생성 + 클래스 추가  +  **내용 추가**
    const textarea = document.createElement(&quot;textarea&quot;);
    textarea.classList.add(&quot;update-textarea&quot;);

    // ******************************************
    // XSS 방지 처리 해제
    beforeContent =  beforeContent.replaceAll(&quot;&amp;amp;&quot;, &quot;&amp;&quot;);
    beforeContent =  beforeContent.replaceAll(&quot;&amp;lt;&quot;, &quot;&lt;&quot;);
    beforeContent =  beforeContent.replaceAll(&quot;&amp;gt;&quot;, &quot;&gt;&quot;);
    beforeContent =  beforeContent.replaceAll(&quot;&amp;quot;&quot;, &quot;\&quot;&quot;);

    // ******************************************
    textarea.value = beforeContent; // 내용 추가

    // 6. commentRow에 생성된 textarea 추가
    commentRow.append(textarea);


    // 7. 버튼 영역 + 수정/취소 버튼 생성
    const commentBtnArea = document.createElement(&quot;div&quot;);
    commentBtnArea.classList.add(&quot;comment-btn-area&quot;);


    const updateBtn = document.createElement(&quot;button&quot;);
    updateBtn.innerText = &quot;수정&quot;;
    updateBtn.setAttribute(&quot;onclick&quot;, &quot;updateComment(&quot;+commentNo+&quot;, this)&quot;);


    const cancelBtn = document.createElement(&quot;button&quot;);
    cancelBtn.innerText = &quot;취소&quot;;
    cancelBtn.setAttribute(&quot;onclick&quot;, &quot;updateCancel(this)&quot;);


    // 8. 버튼영역에 버튼 추가 후 
    //    commentRow(행)에 버튼영역 추가
    commentBtnArea.append(updateBtn, cancelBtn);
    commentRow.append(commentBtnArea);

}


// -----------------------------------------------------------------------------------
// 댓글 수정 취소
function updateCancel(btn){
    // 매개변수 btn : 클릭된 취소 버튼
    // 전역변수 beforeCommentRow : 수정 전 원래 행(댓글)을 저장한 변수

    if(confirm(&quot;댓글 수정을 취소하시겠습니까?&quot;)){
        btn.parentElement.parentElement.innerHTML = beforeCommentRow;
    }
}

// -----------------------------------------------------------------------------------
// 댓글 수정(AJAX)
function updateComment(commentNo, btn){

    // 새로 작성된 댓글 내용 얻어오기
    const commentContent = btn.parentElement.previousElementSibling.value;

    const data= {
        &quot;commentNo&quot;: commentNo,
        &quot;commentContent&quot;: commentContent
    }

    fetch(&quot;/comment&quot;,{
        method: &quot;PUT&quot;,
        headers : {&quot;Content-Type&quot; : &quot;application/json&quot;}
        ,body: JSON.stringify(data)
    })
    .then(resp =&gt; resp.text())
    .then(result =&gt; {
        if(result &gt; 0){
            alert(&quot;댓글이 수정되었습니다.&quot;);
            selectCommentList();
        }else{
            alert(&quot;댓글 수정 실패&quot;);
        }
    })
    .catch(err =&gt; console.log(err));

}

// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------

// 답글 작성 화면 추가 
// -&gt; 답글 작성 화면은 전체 화면에 1개만 존재 해야한다!

function showInsertComment(parentNo, btn){
                        // 부모 댓글 번호, 클릭한 답글 버튼


    // ** 답글 작성 textarea가 한 개만 열릴 수 있도록 만들기 **
    const temp = document.getElementsByClassName(&quot;commentInsertContent&quot;);

    if(temp.length &gt; 0){ // 답글 작성 textara가 이미 화면에 존재하는 경우

        if(confirm(&quot;다른 답글을 작성 중입니다. 현재 댓글에 답글을 작성 하시겠습니까?&quot;)){
            temp[0].nextElementSibling.remove(); // 버튼 영역부터 삭제
            temp[0].remove(); // textara 삭제 (기준점은 마지막에 삭제해야 된다!)

        } else{
            return; // 함수를 종료시켜 답글이 생성되지 않게함.
        }
    }

    // 답글을 작성할 textarea 요소 생성
    const textarea = document.createElement(&quot;textarea&quot;);
    textarea.classList.add(&quot;commentInsertContent&quot;);

    // 답글 버튼의 부모의 뒤쪽에 textarea 추가
    // after(요소) : 뒤쪽에 추가
    btn.parentElement.after(textarea);


    // 답글 버튼 영역 + 등록/취소 버튼 생성 및 추가
    const commentBtnArea = document.createElement(&quot;div&quot;);
    commentBtnArea.classList.add(&quot;comment-btn-area&quot;);


    const insertBtn = document.createElement(&quot;button&quot;);
    insertBtn.innerText = &quot;등록&quot;;
    insertBtn.setAttribute(&quot;onclick&quot;, &quot;insertChildComment(&quot;+parentNo+&quot;, this)&quot;);


    const cancelBtn = document.createElement(&quot;button&quot;);
    cancelBtn.innerText = &quot;취소&quot;;
    cancelBtn.setAttribute(&quot;onclick&quot;, &quot;insertCancel(this)&quot;);

    // 답글 버튼 영역의 자식으로 등록/취소 버튼 추가
    commentBtnArea.append(insertBtn, cancelBtn);

    // 답글 버튼 영역을 화면에 추가된 textarea 뒤쪽에 추가
    textarea.after(commentBtnArea);

}


// 답글 취소
function insertCancel(btn){
                    // 취소
    btn.parentElement.previousElementSibling.remove(); // 취소의 부모의 이전 요소(textarea) 제거
    btn.parentElement.remove(); // 취소의 부모 요소(comment-btn-area) 제거
}


// 답글 등록
function insertChildComment(parentNo, btn){
                        // 부모 댓글 번호, 답글 등록 버튼

    // 누가?                loginMemberNo(로그인한 회원의 memberNo )(전역변수)
    // 어떤 내용?           textarea에 작성된 내용
    // 몇번 게시글?         현재 게시글 boardNo (전역변수)
    // 부모 댓글은 누구?    parentNo (매개변수)

    // 답글 내용
    const commentContent = btn.parentElement.previousElementSibling.value;

    // 답글 내용이 작성되지 않은 경우
    if(commentContent.trim().length == 0){
        alert(&quot;답글 작성 후 등록 버튼을 클릭해주세요.&quot;);
        btn.parentElement.previousElementSibling.value = &quot;&quot;;
        btn.parentElement.previousElementSibling.focus();
        return;
    }

    const data = {&quot;commentContent&quot;: commentContent,
                &quot;memberNo&quot;: loginMemberNo,
                &quot;boardNo&quot;: boardNo,
                &quot;parentNo&quot;: parentNo 
    }

    fetch(&quot;/comment&quot;,{
            method : &quot;POST&quot;
            , headers : {&quot;Content-Type&quot; : &quot;application/json&quot;}
            ,  body : JSON.stringify(data)
    })
    .then(resp =&gt; resp.text())
    .then(result =&gt; {
        if(result &gt; 0){ // 등록 성공
            alert(&quot;답글이 등록되었습니다.&quot;);
            selectCommentList(); // 비동기 댓글 목록 조회 함수 호출

        } else { // 실패
            alert(&quot;답글 등록에 실패했습니다...&quot;);
        }
    })
    .catch(err =&gt; console.log(err));

}</code></pre>
<h2 id="🫧spring">🫧Spring</h2>
<h3 id="💗commentcontrollerjava">💗CommentController.java</h3>
<pre><code class="language-java">package edu.kh.project.board.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import edu.kh.project.board.model.dto.Comment;
import edu.kh.project.board.model.service.CommentService;

//@Controller + @ResponseBody
@RestController  // 요청/응답 처리(단, 모든 요청 응답이 비동기)
                // -&gt; REST API 구축하기 위한 Controller
public class CommentController {

    @Autowired
    private CommentService service;

    // 댓글 목록 조회
    @GetMapping(value = &quot;/comment&quot; , produces = &quot;application/json; charset=UTF-8&quot;)
    @ResponseBody
    public List&lt;Comment&gt; select( @RequestParam(&quot;boardNo&quot;) int boardNo ) {
        return service.select(boardNo); // httpMessageconverter List -&gt; JSON 변환
    }

    // 댓글 삽입
    @PostMapping(&quot;/comment&quot;)
    public int insert(@RequestBody Comment comment) {
        // 요청데이터(JSON)을
        // httpMessageConverter가 해석해서 JAVA객체(comment)에 대입
        return service.insert(comment);
    }

    //댓글 삭제 
    @DeleteMapping(&quot;/comment&quot;)
    public int delete( @RequestBody int commentNo ){
                        // ajax요청시 body에 담겨 있는 하나의 데이터는 
                        // 매개변수 int commentNo담기게 된다
        return service.delete(commentNo);
    }

    // 댓글 수정
    @PutMapping(&quot;/comment&quot;)
    public int update( @RequestBody Comment comment) {
        return service.update(comment);

    }
}
</code></pre>
<h3 id="💗commentservicejava-인터페이스">💗CommentService.java 인터페이스</h3>
<pre><code class="language-java">package edu.kh.project.board.model.service;

import java.util.List;

import edu.kh.project.board.model.dto.Comment;

public interface CommentService {

    /** 댓글 목록 조회
     * @param boardNo
     * @return cList
     */
    List&lt;Comment&gt; select(int boardNo);

    /** 댓글 삽입
     * @param comment
     * @return result
     */
    int insert(Comment comment);

    /** 댓글 삭제
     * @param commentNo
     * @return result
     */
    int delete(int commentNo);

    /** 댓글 수정
     * @param comment
     * @return result
     */
    int update(Comment comment);

}</code></pre>
<h3 id="💗commentserviceimpljava">💗CommentServiceImpl.java</h3>
<pre><code class="language-java">package edu.kh.project.board.model.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import edu.kh.project.board.common.utility.Util;
import edu.kh.project.board.model.dao.CommentDAO;
import edu.kh.project.board.model.dto.Comment;

@Service
public class CommentServiceImpl implements CommentService{

    @Autowired
    private CommentDAO dao;

    // 댓글 목록 조회
    @Override
    public List&lt;Comment&gt; select(int boardNo) {
        return dao.select(boardNo);
    }

    // 댓글 삽입
    @Transactional(rollbackFor = Exception.class)
    @Override
    public int insert(Comment comment) {
        // XSS 방지 처리 
        comment.setCommentContent(Util.XSSHandling(comment.getCommentContent()));
        return dao.insert(comment);
    }

    // 댓글 삭제
    @Transactional(rollbackFor = Exception.class)
    @Override
    public int delete(int commentNo) {
        return dao.delete(commentNo);
    }

    //댓글 수정
    @Transactional(rollbackFor = Exception.class)
    @Override
    public int update(Comment comment) {

        // XSS 방지 처리 
        comment.setCommentContent(Util.XSSHandling(comment.getCommentContent()));
        return dao.update(comment);
    }
}</code></pre>
<h3 id="💗commentdaojava">💗CommentDAO.java</h3>
<pre><code class="language-java">package edu.kh.project.board.model.dao;

import java.util.List;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.project.board.model.dto.Comment;

@Repository // DB 관련 + bean 등록(IOC)
public class CommentDAO {

    @Autowired
     private SqlSessionTemplate sqlsession;

    /** 댓글 목록 조회
     * @param boardNo
     * @return cList
     */
    public List&lt;Comment&gt; select(int boardNo) {
                                // board-mapper.xml에 작성된 select 이용
        return sqlsession.selectList(&quot;boardMapper.selectCommentList&quot;, boardNo);
    }

    /** 댓글 삽입
     * @param comment
     * @return result
     */
    public int insert(Comment comment) {
        return sqlsession.insert(&quot;commentMapper.insert&quot;,comment);
    }

    /** 댓글 삭제
     * @param commentNo
     * @return result
     */
    public int delete(int commentNo) {
        return sqlsession.update(&quot;commentMapper.delete&quot;,commentNo);
    }

    /**댓글 수정
     * @param comment
     * @return result
     */
    public int update(Comment comment) {
        return sqlsession.update(&quot;commentMapper.update&quot;,comment);
    }
}
</code></pre>
<h3 id="💗mybatis-configxml">💗mybatis-config.xml</h3>
<pre><code class="language-xml">    &lt;!-- mapper 파일(SQL 작성되는파일) 위치 등록 부분 --&gt;
    &lt;mappers&gt;
        &lt;mapper resource=&quot;/mappers/comment-mapper.xml&quot; /&gt;
    &lt;/mappers&gt;</code></pre>
<h3 id="💗comment-mapperxml">💗comment-mapper.xml</h3>
<pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE mapper PUBLIC &quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; &quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot; &gt;
&lt;mapper namespace=&quot;commentMapper&quot;&gt;


  &lt;insert id=&quot;insert&quot;&gt;
      INSERT INTO &quot;COMMENT&quot;
      VALUES(SEQ_COMMENT_NO.NEXTVAL,
            #{commentContent},
            DEFAULT, DEFAULT, 
            #{boardNo}, #{memberNo}, 

            &lt;!-- 동적 SQL : if문 --&gt;

            &lt;!-- 부모 댓글 --&gt;
            &lt;if test=&quot;parentNo == 0&quot;&gt;NULL&lt;/if&gt;

            &lt;!-- 자식 댓글 --&gt;
            &lt;if test=&quot;parentNo != 0&quot;&gt;#{parentNo}&lt;/if&gt;
            )
   &lt;/insert&gt;

   &lt;!-- 댓글 삭제 --&gt;
   &lt;update id=&quot;delete&quot;&gt;
           UPDATE &quot;COMMENT&quot; SET
           COMMENT_DEL_FL =&#39;Y&#39;
           WHERE COMMENT_NO = #{commentNo}
   &lt;/update&gt;

   &lt;!-- 댓글 수정 --&gt;
   &lt;update id=&quot;update&quot;&gt;
       UPDATE &quot;COMMENT&quot; SET
           COMMENT_CONTENT=#{commentContent}
        WHERE COMMENT_NO=#{commentNo}
   &lt;/update&gt;
&lt;/mapper&gt;
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 게시글 작성(이미지 삽입)]]></title>
            <link>https://velog.io/@ee_ji0/Spring-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EC%9E%91%EC%84%B1%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%82%BD%EC%9E%85</link>
            <guid>https://velog.io/@ee_ji0/Spring-%EA%B2%8C%EC%8B%9C%EA%B8%80-%EC%9E%91%EC%84%B1%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%82%BD%EC%9E%85</guid>
            <pubDate>Mon, 28 Aug 2023 06:35:52 GMT</pubDate>
            <description><![CDATA[<h1 id="게시글-작성을-해보자">게시글 작성을 해보자</h1>
<blockquote>
<p><strong>게시글 작성 구현 전 알아야 할 것!</strong></p>
</blockquote>
<ul>
<li>목록 조회 : /board/1?cp=1 (cp : current page(현재페이지))</li>
<li>상세 조회 : /board/1/1500?cp=1<br></li>
<li><strong>*컨트롤러 따로 생성 *</strong></li>
<li>삽입 : /board2/1/inssert?code=1(code ==BOARD_CODE , 게시판 종류)</li>
<li>수정 : /board2/1/update?code=1&amp;no=1500 (no == BOARD_NO , 게시글 번호)</li>
<li>삭제 : /board2/1/delete?code=1&amp;no=1500</li>
</ul>
<h2 id="boardcontroller">board.controller</h2>
<h3 id="boardcontroller2">BoardController2</h3>
<pre><code class="language-java">package edu.kh.project.board.controller;

import java.io.IOException;
import java.util.List;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.fasterxml.jackson.annotation.JacksonInject.Value;

import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.service.BoardService2;
import edu.kh.project.member.model.dto.Member;

@Controller
@RequestMapping(&quot;/board2&quot;)
@SessionAttributes({&quot;loginMember&quot;})
public class BoardController2 {

    @Autowired
    private BoardService2 service;

    // 게시글 작성 화면 전환
    @GetMapping(&quot;/{boardCode:[0-9]+}/insert&quot;)
    public String boardInsert(@PathVariable(&quot;boardCode&quot;) int boardCode) {

        //@PathVariable :주소 값 가져오기 + requset scope 에 값올리기
        return &quot;board/boardWrite&quot;;
    }


    // 게시글 작성 
    @PostMapping(&quot;/{boardCode:[0-9]+}/insert&quot;)
    public String boardInsert( 
            @PathVariable(&quot;boardCode&quot;) int boardCode
            , Board board /*커멘드 객체(필드에 파라미터 담겨져 있음)*/
            , @RequestParam(value=&quot;images&quot;, required = false) List&lt;MultipartFile&gt; images
            , @SessionAttribute(&quot;loginMember&quot;) Member loginMember
            , RedirectAttributes ra
            , HttpSession session) throws IllegalStateException, IOException{


        // 파라미터 : 제목, 내용, 파일(0~5개)
        // 파일저장 경로 : httpSession
        // 세션 : 로그인한 회원 번호 
        //리다이렉트 시 데이터 전달 : RedirectAuttributrs
        // 작성 성공 시 이동할 게시판 코드 : @PathVariable(&quot;boardCode&quot;)

        /* List&lt;MultipartFile&gt;
         * -업로드된 이미지가 없어도 List에 요소 MultipartFile  객체가 추가됨
         * 
         * -단, 업로드된 이미지가 없는 MultipartFile 객체는
         * 파일크기(size)가 0 또는 파일명(getOriginalFileName())이 &quot;&quot;
         * */

        // 1. 로그인한 회원 번호를 얻어와 board에 세팅
        board.setMemberNo(loginMember.getMemberNo());

        // 2. boardCode도 board에 세팅
        board.setBoardCode(boardCode);

        // 3. 업로드된 이밎 서버에 실제로 저장되는 경로
        //    + 웹에서 요청 시 이미지를 볼 수 있는 경로
        String webPath =&quot;/resources/images/board/&quot;;
        String filePath = session.getServletContext().getRealPath(webPath);

        // 게시글 삽입을 하는 서비스 호출 후 삽입된 게시글의 번호 반환 받기
        int boardNo = service.boardInsert(board, images, webPath, filePath);

        // 게시글 삽입 셩공 시 
        // -&gt; 방금 삽입한 게시글의 상세 조회 페이지로 리다이렉트
        // -&gt; /board/{boardCode}/{boardNo}

        String message = null;
        String path =&quot;redirect:&quot;;

        if(boardNo &gt; 0) { //성공 시
            message=&quot;게시글이 등록 되었습니다.&quot;;
            path +=&quot;/board/&quot;+boardCode +&quot;/&quot;+boardNo;
        }else {
            message=&quot;게시글 등록 실패ㅠㅠ&quot;;
            path += &quot;insert&quot;;
        }

        ra.addFlashAttribute(&quot;message&quot;, message);
        return path;
    }
}</code></pre>
<h2 id="boardmodel">board.model</h2>
<h3 id="serviceboardservice2-인터페이스">service.BoardService2 인터페이스</h3>
<pre><code class="language-java">/** 게시글 삽입
     * @param board
     * @param images
     * @param webPath
     * @param filePath
     * @return boardNo
     */
    int boardInsert(Board board, List&lt;MultipartFile&gt; images, String webPath, String filePath)throws IllegalStateException, IOException;</code></pre>
<h3 id="servicebaordserviceimpl2-클래스">service.BaordServiceImpl2 클래스</h3>
<pre><code class="language-java">package edu.kh.project.board.model.service;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import edu.kh.project.board.common.utility.Util;
import edu.kh.project.board.model.dao.BoardDAO2;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;
import edu.kh.project.board.model.exception.FileUploadException;

@Service
public class BoardServiceImpl2 implements BoardService2{

    @Autowired
    private BoardDAO2 dao;


    // 게시글 삽입 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int boardInsert(Board board, List&lt;MultipartFile&gt; images, String webPath, String filePath) throws IllegalStateException, IOException {

        // 0. XSS 방지 처리
        board.setBoardContent(Util.XSSHandling(board.getBoardContent()));
        board.setBoardTitle(Util.XSSHandling(board.getBoardTitle()));


        // 1. BOARD 테이블에 INSERT하기(제목, 내용, 작성자, 게시판 코드)
        // -&gt; boardNo (시퀀스로 생성한 번호) 반환 받기
        int boardNo = dao.boardInsert(board);

        // 2. 게시글 삽입 성공시 
        // 업로드 된 이미지가 있다면 BOARD_IMG 테이블에 삽입하는 DAO 호출

        if(boardNo &gt; 0) { // 게시글 성공 시

            // List&lt;MultipartFile&gt; images
            // -&gt; 업로드된 파일이 담긴 객체 MultipartFile이 5개 존재
            // -&gt; 단, 업로드된 파일이 없어도 MultipartFile 개체는 존재

            // 실제로 업로드된 파일을 기록할 List
            List&lt;BoardImage&gt; uploadList = new ArrayList&lt;BoardImage&gt;();

            // images에 담겨 잇는 파일 중에서 실제로 업로드된 파일 만 분류
            for(int i= 0; i &lt;images.size(); i++) {

                //i 번째 요소에 업로드한 파일이 있다면
                if(images.get(i).getSize() &gt; 0) {

                    BoardImage img = new BoardImage();

                    //img에 파일 정보를 담아서 uploadList에 추가 
                    img.setImagePath(webPath); //웹 접근 경로
                    img.setBoardNo(boardNo); //게시글 번호 
                    img.setImageOrder(i); //이미지 순서

                    //파일 원본명
                    String fileName = images.get(i).getOriginalFilename();

                    img.setImageOriginal(fileName); //원본명
                    img.setImageReName(Util.fileRename(fileName)); // 변경명

                    uploadList.add(img);
                }
            } // 분류하는 for문 종료

            // 분류 작업 후 uploadList가 비어 있지 않은 경우
            // == 업로드한 파일이 있다
            if(!uploadList.isEmpty()) {

                // BOARD_IMG테이블에 INSERT 하는 DAO호출
                int result = dao.insertImageList(uploadList);
                // result == 삽입된 행의 개수 == uploadList.size()

                // 삽입된 행의 개수와 uploadList의 개수가 같다면
                // == 전체 insert 성공
                if(result == uploadList.size()) {

                    // 서버에 파일 저장(transferTo())

                    // images            : 실제 파일이 담긴 객체 리스트
                    //                        (업로드 안된 인덱스 빈칸)

                    // uploadList         : 업로 된 파일의 정보 리스트
                    //            (원본명, 변경명, 순서, 경로, 게시글 번호)

                    // 순서 == images 업로드된 인덱스 번호

                    for(int i =0; i &lt;uploadList.size(); i++) {

                        int index = uploadList.get(i).getImageOrder();


                        // 파일로 변환 
                        String rename = uploadList.get(i).getImageReName();

                        images.get(index).transferTo(new File(filePath+ rename));
                    }

                }else { //일부 또는 전체 insert 실패

                    // **웹서비스 실행 중 1개라도 실패하면 전체 실패**
                    // -&gt; rollback필요

                    // @Transactional(rollbackFor = Exception.class)
                    // -&gt; 예외가 발생 해야지만 롤백

                    // [결론]
                    // 예외 강제 발생 시켜서 rollback  해야한다
                    // -&gt; 사용자 정의 예외 생성

                    throw new FileUploadException(); //예외 강제 발생
                }
            }
        }

        return boardNo;
    }
}</code></pre>
<h3 id="daoboarddao2">dao.BoardDAO2</h3>
<pre><code class="language-java">package edu.kh.project.board.model.dao;

import java.util.List;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.BoardImage;

@Repository
public class BoardDAO2 {

    @Autowired
    private SqlSessionTemplate sqlsession;

    /** 게시글 삽입
     * @param board
     * @return 
     */
    public int boardInsert(Board board) {
        int result = sqlsession.insert(&quot;boardMapper.boardInsert&quot;, board);

        // -&gt;sql 수행 후 매개변수 board 객체에는 boardNo 존재O

        // 삽입 성공시 
        if(result &gt; 0) result =  board.getBoardNo();

        return result; // 삽입 성공 시 boardNo, 실패 시 0 반환
    }


    /** 이미지 리스트(여러개)삽입
     * @param uploadList
     * @return result
     */
    public int insertImageList(List&lt;BoardImage&gt; uploadList) {
        return sqlsession.insert(&quot;boardMapper.insertImageList&quot;, uploadList);
    }
}
</code></pre>
<h3 id="board-mapperxml">board-mapper.xml</h3>
<blockquote>
<p>게시글 삽입(INSERT) 시 미리 boardNo 결과를 Select 구문으로 얻어와서 INSERT구문에 넣어 삽입 결과 1 or 0 얻어오기!
<br>
<mark>useGeneratedKeys 속성</mark> : DB 내부적으로 생성한 key(시퀀스)를 전달된 파라미터의 필드로 대입 가능 여부를 지정(trur/false)
<mark><strong>동적 SQL</strong></mark>
프로그램 수행 중 SQL를 변경하는 기능 (마이바티스의 가장 강력한 기능)
<mark> selectKey태그 </mark> 
INSERT/UPDATE 시 사용할 키(시퀀스)를 조회해서 파라미터의 지정된 필드에 대입
<mark>oder속성</mark>
메인 SQL이 수행되기 전/후 selectKey가 수행되도록 지정한다
전: BEFORE / 후: AFTER
<mark> foreach</mark> 
<mark> 이미지 리스트 (여러개 삽입)</mark> </p>
</blockquote>
<pre><code class="language-xml">    &lt;!-- 게시글 삽입 --&gt;
    &lt;!-- 
        useGeneratedKeys 속성 : DB 내부적으로 생성한 key(시퀀스)를
                              전달된 파라미터의 필드로 대입 가능 여부를 지정

        **동적 SQL**
        - 프로그램 수행 중 SQL를 변경하는 기능 (마이바티스의 가장 강력한 기능)

        &lt;selectKey&gt; 태그 : INSERT/UPDATE 시 사용할 키(시퀀스)를 
                          조회해서 파라미터의 지정된 필드에 대입    

        oder속성 : 메인 SQL이 수행되기 전/후 selectKey가 수행되도록 지정한다
                전: BEFORE
                후: AFTER


        keyProperty 속성 : selectKey 조회 결과를 저장할 파라미터의 필드
     --&gt;
    &lt;insert id=&quot;boardInsert&quot; parameterType=&quot;Board&quot; useGeneratedKeys=&quot;true&quot;&gt;

        &lt;selectKey order=&quot;BEFORE&quot; resultType=&quot;_int&quot; keyProperty=&quot;boardNo&quot;&gt;
            SELECT SEQ_BOARD_NO.NEXTVAL FROM DUAL
        &lt;/selectKey&gt;

        INSERT INTO BOARD 
          VALUES( #{boardNo},
                  #{boardTitle},
                  #{boardContent},
                  DEFAULT, DEFAULT, DEFAULT, DEFAULT,
                  #{memberNo}, 
                  #{boardCode} )
    &lt;/insert&gt;

    &lt;!-- 동적 SQL 중 &lt;foreach&gt; 
      - 특정 SLQ 구문을 반복할 때 사용
      - 반복되는 사이에 구분자(separator)를 추가할 수 있음.

      collection : 반복할 객체의 타입 작성(list, set, map...)
      item : collection에서 순차적으로 꺼낸 하나의 요소를 저장하는 변수
      index : 현재 반복 접근중인 인덱스 (0,1,2,3,4 ..)

      open : 반복 전에 출력할 sql
      close : 반복 종료 후에 출력한 sql

      separator : 반복 사이사이 구분자
    --&gt;



    &lt;!-- 이미지 리스트(여러개 삽입) --&gt;
    &lt;insert id=&quot;insertImageList&quot; parameterType=&quot;list&quot;&gt;
        INSERT INTO BOARD_IMG
        SELECT SEQ_IMG_NO.NEXTVAL, A.*
        FROM(

            &lt;foreach collection=&quot;list&quot; item=&quot;img&quot; separator=&quot; UNION ALL &quot;&gt;
                SELECT  #{img.imagePath} IMG_PATH,
                         #{img.imageReName} IMG_RENAME,
                         #{img.imageOriginal} IMG_ORIGINAL,
                        #{img.imageOrder} IMG_ORDER,
                        #{img.boardNo} BOARD_NO
                FROM DUAL;
            &lt;/foreach&gt;

            ) A
    &lt;/insert&gt;</code></pre>
<h2 id="vs-code">VS CODE</h2>
<h3 id="boardlistjsp">boardList.jsp</h3>
<pre><code class="language-jsp">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot;  %&gt;

&lt;%-- map에 저장된 값을 각각 변수에 저장 --%&gt;
&lt;c:set var=&quot;pagination&quot; value=&quot;${map.pagination}&quot;/&gt;
&lt;c:set var=&quot;boardList&quot; value=&quot;${map.boardList}&quot;/&gt;

&lt;%-- &lt;c:set var=&quot;boardName&quot; value=&quot;${boardTypeList[boardCode-1].BOARD_NAME}&quot;/&gt; --%&gt;
&lt;c:forEach items=&quot;${boardTypeList}&quot; var=&quot;boardType&quot;&gt;
    &lt;c:if test=&quot;${boardType.BOARD_CODE == boardCode}&quot; &gt;
        &lt;c:set var=&quot;boardName&quot; value=&quot;${boardType.BOARD_NAME}&quot;/&gt;
    &lt;/c:if&gt;
&lt;/c:forEach&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;&lt;/title&gt;

    &lt;link rel=&quot;stylesheet&quot; href=&quot;/resources/css/board/boardList-style.css&quot;&gt;

&lt;/head&gt;
&lt;body&gt;
    &lt;main&gt;
        &lt;jsp:include page=&quot;/WEB-INF/views/common/header.jsp&quot;/&gt;


        &lt;section class=&quot;board-list&quot;&gt;

            &lt;h1 class=&quot;board-name&quot;&gt;${boardName}&lt;/h1&gt;


            &lt;div class=&quot;list-wrapper&quot;&gt;
                &lt;table class=&quot;list-table&quot;&gt;

                    &lt;thead&gt;
                        &lt;tr&gt;
                            &lt;th&gt;글번호&lt;/th&gt;
                            &lt;th&gt;제목&lt;/th&gt;
                            &lt;th&gt;작성자&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;c:choose&gt;
                           &lt;c:when test=&quot;${empty boardList}&quot;&gt;
                                &lt;%-- 조회된 게시글 목록 비어있거나 null인 경우 --%&gt;

                                &lt;!-- 게시글 목록 조회 결과가 비어있다면 --&gt;
                                &lt;tr&gt;
                                    &lt;th colspan=&quot;6&quot;&gt;게시글이 존재하지 않습니다.&lt;/th&gt;
                                &lt;/tr&gt;
                           &lt;/c:when&gt;

                           &lt;c:otherwise&gt;

                            &lt;c:forEach items=&quot;${boardList}&quot; var=&quot;board&quot;&gt;
                                &lt;!-- 게시글 목록 조회 결과가 있다면--&gt;
                                &lt;tr&gt;
                                    &lt;td&gt;${board.boardNo}&lt;/td&gt;
                                    &lt;td&gt; 
                                        &lt;%-- 썸네일이 있을 경우 --%&gt;
                                        &lt;c:if test=&quot;${!empty board.thumbnail}&quot; &gt;
                                            &lt;img class=&quot;list-thumbnail&quot; src=&quot;${board.thumbnail}&quot;&gt;
                                        &lt;/c:if&gt;



                                        &lt;%-- ${boardCode} : @PathVariable로 request scope에 추가된 값 --%&gt;
                                        &lt;a href=&quot;/board/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}&quot;&gt;${board.boardTitle}&lt;/a&gt;   
                                        [${board.commentCount}]                       
                                    &lt;/td&gt;
                                    &lt;td&gt;${board.memberNickname}&lt;/td&gt;
                                    &lt;td&gt;${board.boardCreateDate}&lt;/td&gt;
                                    &lt;td&gt;${board.readCount}&lt;/td&gt;
                                    &lt;td&gt;${board.likeCount}&lt;/td&gt;
                                &lt;/tr&gt;
                            &lt;/c:forEach&gt;
                           &lt;/c:otherwise&gt;
                        &lt;/c:choose&gt;




                    &lt;/tbody&gt;
                &lt;/table&gt;
            &lt;/div&gt;


            &lt;div class=&quot;btn-area&quot;&gt;

            &lt;!-- 로그인 상태일 경우 글쓰기 버튼 노출 --&gt;
                &lt;c:if test=&quot;${!empty loginMember}&quot; &gt;
                    &lt;button id=&quot;insertBtn&quot;&gt;글쓰기&lt;/button&gt;                     
                &lt;/c:if&gt;

            &lt;/div&gt;


            &lt;div class=&quot;pagination-area&quot;&gt;


                &lt;ul class=&quot;pagination&quot;&gt;

                    &lt;!-- 첫 페이지로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=1&quot;&gt;&amp;lt;&amp;lt;&lt;/a&gt;&lt;/li&gt;

                    &lt;!-- 이전 목록 마지막 번호로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${pagination.prevPage}&quot;&gt;&amp;lt;&lt;/a&gt;&lt;/li&gt;


                    &lt;!-- 특정 페이지로 이동 --&gt;
                    &lt;c:forEach var=&quot;i&quot; begin=&quot;${pagination.startPage}&quot;
                                end=&quot;${pagination.endPage}&quot; step=&quot;1&quot;&gt;
                        &lt;c:choose&gt;
                           &lt;c:when test=&quot;${i== pagination.currentPage}&quot;&gt;
                                &lt;!-- 현재 보고있는 페이지 --&gt;
                                &lt;li&gt;&lt;a class=&quot;current&quot;&gt;${i}&lt;/a&gt;&lt;/li&gt;
                           &lt;/c:when&gt;

                           &lt;c:otherwise&gt;
                                &lt;!-- 현재 페이지를 제외한 나머지 --&gt;
                                &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${i}&quot;&gt;${i}&lt;/a&gt;&lt;/li&gt;
                           &lt;/c:otherwise&gt;
                        &lt;/c:choose&gt;
                    &lt;/c:forEach&gt;

                    &lt;!-- 다음 목록 시작 번호로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${pagination.nextPage}&quot;&gt;&amp;gt;&lt;/a&gt;&lt;/li&gt;

                    &lt;!-- 끝 페이지로 이동 --&gt;
                    &lt;li&gt;&lt;a href=&quot;/board/${boardCode}?cp=${pagination.maxPage}&quot;&gt;&amp;gt;&amp;gt;&lt;/a&gt;&lt;/li&gt;

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


         &lt;!-- 검색창 --&gt;
            &lt;form action=&quot;#&quot; method=&quot;get&quot; id=&quot;boardSearch&quot;&gt;

                &lt;select name=&quot;key&quot; id=&quot;searchKey&quot;&gt;
                    &lt;option value=&quot;t&quot;&gt;제목&lt;/option&gt;
                    &lt;option value=&quot;c&quot;&gt;내용&lt;/option&gt;
                    &lt;option value=&quot;tc&quot;&gt;제목+내용&lt;/tion&gt;
                    &lt;option value=&quot;w&quot;&gt;작성자&lt;/option&gt;
                &lt;/select&gt;

                &lt;input type=&quot;text&quot; name=&quot;query&quot;  id=&quot;searchQuery&quot; placeholder=&quot;검색어를 입력해주세요.&quot;&gt;

                &lt;button&gt;검색&lt;/button&gt;
            &lt;/form&gt;

        &lt;/section&gt;
    &lt;/main&gt;


    &lt;!-- 썸네일 클릭 시 모달창 출력 --&gt;
    &lt;div class=&quot;modal&quot;&gt;
        &lt;span id=&quot;modalClose&quot;&gt;&amp;times;&lt;/span&gt;
        &lt;img id=&quot;modalImage&quot; src=&quot;/resources/images/user.png&quot;&gt;
    &lt;/div&gt;


    &lt;jsp:include page=&quot;/WEB-INF/views/common/footer.jsp&quot;/&gt;
    &lt;script src=&quot;/resources/js/board/boardList.js&quot;&gt;&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="boardlistjs">boardList.js</h3>
<pre><code class="language-js">const insertBtn = document.getElementById(&quot;insertBtn&quot;);

// 글쓰기 버튼 을 클릭했을 때 

if(insertBtn != null){

    insertBtn.addEventListener(&quot;click&quot;, ()=&gt;{
        // JS BOM 객체 location

        // location.href=&quot;주소&quot;
        // 해당 주소 요청(GET방식)

        // location.href=&quot;/board2/&quot;+location.pathname.split(&quot;/&quot;)[2]
        location.href=`/board2/${location.pathname.split(&quot;/&quot;)[2]}/insert`
                    //board2/1/insert
    })
}</code></pre>
<h3 id="boardwritejsp">boardWrite.jsp</h3>
<pre><code class="language-jsp">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot;  %&gt;
&lt;%@ taglib prefix=&quot;fn&quot; uri=&quot;http://java.sun.com/jsp/jstl/functions&quot;  %&gt;

&lt;c:forEach items=&quot;${boardTypeList}&quot; var=&quot;boardType&quot;&gt;
    &lt;c:if test=&quot;${boardType.BOARD_CODE == boardCode}&quot; &gt;
        &lt;c:set var=&quot;boardName&quot; value=&quot;${boardType.BOARD_NAME}&quot;/&gt;
    &lt;/c:if&gt;
&lt;/c:forEach&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;${boardName}&lt;/title&gt;

    &lt;link rel=&quot;stylesheet&quot; href=&quot;/resources/css/board/boardWrite-style.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;main&gt;
        &lt;jsp:include page=&quot;/WEB-INF/views/common/header.jsp&quot;/&gt;


                            &lt;%-- @PathVariable에서 request scope로 가져옴 --%&gt;
        &lt;form action=&quot;/board2/${boardCode}/insert&quot; method=&quot;POST&quot; 
                class=&quot;board-write&quot; id=&quot;boardWriteFrm&quot; enctype=&quot;multipart/form-data&quot;&gt; 
                         &lt;%--enctype=&quot;multipart/form-data&quot; : 제출 이코딩 X
                         -&gt; 파일제출 가능
                         -&gt; Multipath Resolver 가 문자열, 파일 구분
                         --&gt; 문자열, String, int, DTO, Map (HttpMessageConverter)
                         --&gt; 파일 -&gt; MultiPathFile 객체 -&gt; transferTo() (파일을 서버에 저장) --%&gt;

            &lt;h1 class=&quot;board-name&quot;&gt;${boardName}&lt;/h1&gt;

            &lt;!-- 제목 --&gt;
            &lt;h1 class=&quot;board-title&quot;&gt;
                &lt;input type=&quot;text&quot; name=&quot;boardTitle&quot; placeholder=&quot;제목&quot; value=&quot;&quot;&gt;
            &lt;/h1&gt;


            &lt;!-- 썸네일 영역 --&gt;
            &lt;h5&gt;썸네일&lt;/h5&gt;
            &lt;div class=&quot;img-box&quot;&gt;
                &lt;div class=&quot;boardImg thumbnail&quot;&gt;
                    &lt;label for=&quot;img0&quot;&gt;
                        &lt;img class=&quot;preview&quot; src=&quot;&quot;&gt;
                    &lt;/label&gt;
                    &lt;input type=&quot;file&quot; name=&quot;images&quot; class=&quot;inputImage&quot; id=&quot;img0&quot; accept=&quot;image/*&quot;&gt;
                    &lt;span class=&quot;delete-image&quot;&gt;&amp;times;&lt;/span&gt;
                &lt;/div&gt;
            &lt;/div&gt;


            &lt;!-- 업로드 이미지 영역 --&gt;
            &lt;h5&gt;업로드 이미지&lt;/h5&gt;
            &lt;div class=&quot;img-box&quot;&gt;

                &lt;div class=&quot;boardImg&quot;&gt;
                    &lt;label for=&quot;img1&quot;&gt;
                        &lt;img class=&quot;preview&quot; src=&quot;&quot;&gt;
                    &lt;/label&gt;
                    &lt;input type=&quot;file&quot; name=&quot;images&quot; class=&quot;inputImage&quot; id=&quot;img1&quot; accept=&quot;image/*&quot;&gt;
                    &lt;span class=&quot;delete-image&quot;&gt;&amp;times;&lt;/span&gt;
                &lt;/div&gt;

                &lt;div class=&quot;boardImg&quot;&gt;
                    &lt;label for=&quot;img2&quot;&gt;
                        &lt;img class=&quot;preview&quot; src=&quot;&quot;&gt;
                    &lt;/label&gt;
                    &lt;input type=&quot;file&quot; name=&quot;images&quot; class=&quot;inputImage&quot; id=&quot;img2&quot; accept=&quot;image/*&quot;&gt;
                    &lt;span class=&quot;delete-image&quot;&gt;&amp;times;&lt;/span&gt;
                &lt;/div&gt;

                &lt;div class=&quot;boardImg&quot;&gt;
                    &lt;label for=&quot;img3&quot;&gt;
                        &lt;img class=&quot;preview&quot; src=&quot;&quot;&gt;
                    &lt;/label&gt;
                    &lt;input type=&quot;file&quot; name=&quot;images&quot; class=&quot;inputImage&quot; id=&quot;img3&quot; accept=&quot;image/*&quot;&gt;
                    &lt;span class=&quot;delete-image&quot;&gt;&amp;times;&lt;/span&gt;
                &lt;/div&gt;

                &lt;div class=&quot;boardImg&quot;&gt;
                    &lt;label for=&quot;img4&quot;&gt;
                        &lt;img class=&quot;preview&quot; src=&quot;&quot;&gt;
                    &lt;/label&gt;
                    &lt;input type=&quot;file&quot; name=&quot;images&quot; class=&quot;inputImage&quot; id=&quot;img4&quot; accept=&quot;image/*&quot;&gt;
                    &lt;span class=&quot;delete-image&quot;&gt;&amp;times;&lt;/span&gt;
                &lt;/div&gt;
            &lt;/div&gt;

            &lt;!-- 내용 --&gt;
            &lt;div class=&quot;board-content&quot;&gt;
                &lt;textarea name=&quot;boardContent&quot;&gt;&lt;/textarea&gt;
            &lt;/div&gt;


             &lt;!-- 버튼 영역 --&gt;
            &lt;div class=&quot;board-btn-area&quot;&gt;
                &lt;button type=&quot;submit&quot; id=&quot;writebtn&quot;&gt;등록&lt;/button&gt;
            &lt;/div&gt;


        &lt;/form&gt;

    &lt;/main&gt;

    &lt;jsp:include page=&quot;/WEB-INF/views/common/footer.jsp&quot;/&gt;

    &lt;script src=&quot;/resources/js/board/boardWrite.js&quot;&gt;&lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="boardwritejs">boardWrite.js</h3>
<pre><code class="language-js">//미리보기 관련 요소 모두 얻어오기

//img 5개
const preview = document.getElementsByClassName(&quot;preview&quot;);
//file 5개
const inputImage = document.getElementsByClassName(&quot;inputImage&quot;);

// X버튼 5개
const deleteImage = document.getElementsByClassName(&quot;delete-image&quot;);

// 위에 얻어온 요소들의 개수가 같음 == 인덱스가 일치함

for( let i=0; i &lt; inputImage.length; i++){

    // 파일이 선택되거나, 선택 후 취소 되었을 때
    inputImage[i].addEventListener(&quot;change&quot;, e=&gt;{

        const file = e.target.files[0]; //선택된 파일의 데이터

        if(file != undefined){ //파일이 선택되었을 때

            const reader = new FileReader(); //파일을 읽는 객체

            reader.readAsDataURL(file);
            //지정된 파일을 읽은 후 result변수에 URL형식으로 저장

            reader.onload = e =&gt;{ //파일을 다 읽은 후 수행
                preview[i].setAttribute(&quot;src&quot;, e.target.result);
            }

        }else{ // 파일 선택 후 취소 눌렀을 때
            // -&gt; 선택된 파일이 없다 -&gt; 미리보기 없다
            preview[i].removeAttribute(&quot;src&quot;);



        }
    });

    // 미리보기 삭제 버튼(x버튼)
    deleteImage[i].addEventListener(&quot;click&quot;, ()=&gt;{

        // 미리보기 이미지가 있을 경우 
        if(preview[i].getAttribute(&quot;src&quot;) != &quot;&quot;){

            // 미리보기 삭제
            preview[i].removeAttribute(&quot;src&quot;);

            // input type=&quot;file&quot; 태그의 value를 삭제
            // input type=&quot;file&quot; 의 value &quot;&quot;(빈칸)만 대입가능
            inputImage[i].value=&quot;&quot;;
        }
    })


}

// 게시글 등록 시 제목, 내용 작성 여부 검사 
const boardWriteFrm = document.getElementById(&quot;boardWriteFrm&quot;)
const boardTilte = document.querySelector(&quot;[name=&#39;boardTitle&#39;]&quot;)
const boardContent = document.querySelector(&quot;[name=&#39;boardContent&#39;]&quot;)

boardWriteFrm.addEventListener(&quot;submit&quot;, e=&gt;{

    if( boardTilte.value.trim().length == 0 ){
        alert(&quot;제목을 입력해주세요&quot;)
        boardTilte.value=&quot;&quot;;
        boardTilte.focus();
        e.preventDefault();
        return;
    }

    if( boardContent.value.trim().length == 0){
        alert(&quot;내용을 입력해주세요&quot;);
        boardContent.value=&quot;&quot;;
        boardContent.focus();
        e.preventDefault();
        return;
    }
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[공공데이터 활용]]></title>
            <link>https://velog.io/@ee_ji0/%EA%B3%B5%EA%B3%B5%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@ee_ji0/%EA%B3%B5%EA%B3%B5%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Sun, 27 Aug 2023 08:27:06 GMT</pubDate>
            <description><![CDATA[<h1 id="🌦️공공데이터를-활용해서-원하는-데이터-정보를-가져와-보자">🌦️공공데이터를 활용해서 원하는 데이터 정보를 가져와 보자!</h1>
<h2 id="☔공공데이터-포털-사이트에서">☔공공데이터 포털 사이트에서!</h2>
<p><a href="https://www.data.go.kr">https://www.data.go.kr</a></p>
<p>1) 로그인 후 홈페이지에서 원하는 데이터를 검색
나는 &quot;<strong>한국환경공단 대기오염</strong>&quot;을 검색
<img src="https://velog.velcdn.com/images/ee_ji0/post/742c63e4-5b82-4dbf-8134-f77c9de6c31e/image.png" alt="">
<img src="https://velog.velcdn.com/images/ee_ji0/post/a06107da-e8ba-41df-a01e-5cd3379f9117/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/30e93ff1-508a-4907-90ea-5de7d0115b3b/image.png" alt=""></p>
<p>2) 활용 신청 누른 후 공공데이터를 받으려면 <strong>인증키</strong>가 필요하니 확인하러 가보자!!!
<img src="https://velog.velcdn.com/images/ee_ji0/post/54d5e04f-b079-4ab0-ac20-21adf3485075/image.png" alt=""></p>
<p>3) 마이페이지에서 <strong>인증키 발급 현황</strong>을 확인 할 수 있다
잘 발급 되었다!
<img src="https://velog.velcdn.com/images/ee_ji0/post/b66b60b1-2c4b-4ae0-bbce-470941acfcaa/image.png" alt=""></p>
<p>4) 마이페이지-&gt; 활용신청 현황-&gt; &quot;내가 신청한 데이터&quot;-&gt;상세기능 정보
<img src="https://velog.velcdn.com/images/ee_ji0/post/41527d64-269d-4c31-b17f-d3ba24e6bdc4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/dc98684d-39a5-4bc4-b876-be1446302de6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/56a759be-98bc-4153-a69b-d880470d0c66/image.png" alt="">
xml -&gt; item : 하나의 데이터 정보(실질적으로 필요한 정보)
item에 있는 데이터 vo에 담아고 또 List에 담아 가져올수 있다 </p>
<blockquote>
<p>*<em>openData 프로젝트 만들기 *</em> 
opendata Spring Legacy Project 생성
spring 폴더 옮기기
web.xml 
pom.xml 라이브러리 추가
mvn업뎃
최상위 주소 &#39;/&#39;로 바꾸기 </p>
</blockquote>
<h3 id="💧vo-만들기">💧VO 만들기</h3>
<blockquote>
<p>item에 있는 데이터 중 필요한 부분만 VO 필드에 담는다. </p>
</blockquote>
<pre><code class="language-java">package com.kh.opendata.model.vo;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Air {
    //item에 있는 데이터 중 필요한 부분만 필드에 담는다. 

    //필드
    private String stationName; //측정소명
    private String dataTime; //측정일시
    private String khaiValue; //통합대기환경수치

    private String pm10Value; //미세먼지농도
    private String so2Value; //아황산가스농도
    private String coValue;//일산화탄소농도
    private String no2Value;//이산화질소농도
    private String o3Value;//오존농도
}</code></pre>
<h3 id="💧airpollutionjavaapprunjava">💧AirPollutionJavaAppRun.java</h3>
<blockquote>
<p><strong>HttpURLConnection 객체를 활용해서 OpenAPI 요청절차</strong> </p>
</blockquote>
<ol>
<li>작성된 url 정보를 넣어 URL 객체 생성</li>
<li>생성된 URL 객체로 URLCpnnection 생성</li>
<li>요청 시 필요한 Header 설정</li>
<li>해당 OpenAPI 서버로 요청 후 입력스트립을 통해서 응답객체 얻어오기</li>
<li>다 사용한 스트림 반납 및 연결 해제</li>
</ol>
<blockquote>
<p><strong>json데이터를 원하는 데이터만 추출하여 VO에 담기</strong>
 JSONObject, JSONArray을 이용해서 파싱할 수 있다.
 이 때, (gson라이브러리 필요)
** &quot;responseText&quot; -&gt; total-&gt; response -&gt; body -&gt; totalCount -&gt; items-&gt; item **
 JsonObj 객체 : &quot;responseText&quot;, response, body
 int : totalCount
 JsonArray : items
 JsonArray List에 담긴 객체 for문 이용해서 하나씩 꺼내오기!(이때 배열길이 .size() 사용)</p>
</blockquote>
<pre><code class="language-java">package com.kh.opendata.run;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

public class AirPollutionJavaAppRun {

    //발급 받은 인증키 변수처리
    public static final String SERVICEKEY = &quot;서비스키&quot;;

    public static void main(String[] args) throws IOException { // UnsupportedEncodingException의
                                                                // 부모로 예외 처리
        //OpenAPI 서버로 요청하고자 하는 url 작성
        String url =&quot;http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty&quot;;

        //필수
        url += &quot;?serviceKey=&quot; + SERVICEKEY; 
        //서비스키가 제대로 부여 되지 않았을 경우 SERVICE_KEY_IS_NOT_REGISTERED_ERROR 발생
        url +=&quot;&amp;sidoName=&quot;+ URLEncoder.encode(&quot;서울&quot;,&quot;UTF-8&quot;);

        url += &quot;&amp;returnType=json&quot;;

        //System.out.println(url);

        //****HttpURLConnection 객체를 활용해서 OpenAPI 요청절차**** 
        // 1. 요청할 주소를 전달해서 java.net.URL 객체 생성하기
        URL requestURL = new URL(url);

        // 2. 생성된 URL 객체를가지고 HttpUrlConnection 객체 얻어내기
        HttpURLConnection urlConn = (HttpURLConnection)requestURL.openConnection();
                                    //다운캐스팅

        // 3. 요청 시 필요한 Header 설정하기
        urlConn.setRequestMethod(&quot;GET&quot;);

        // 4. 해당 OpenAPI 서버로 요청 보낸 후 입력 스트림을 통해 응답데이터 받기
        BufferedReader br = new BufferedReader( new InputStreamReader(urlConn.getInputStream()));
        // 보조 스트림                         문자(2바이트)             보조스트림        기반스트림 Input(1바이트)
        // BufferedReader : 한줄 단위로 읽어 오자!

        String responseText=&quot;&quot;;
        String line;
        // 한 줄 씩 읽어와 line 에 담고 line null일 때 반복문 종료
        while( (line = br.readLine()) != null) { // 한줄 씩 읽어올 때 데이터가 있는 동안 반복

            //System.out.println(line);
            responseText += line;
        }
        //System.out.println(responseText);

        //*********json데이터를 원하는 데이터만 추출하여 VO에 담기********
        // JSONObject, JSONArray을 이용해서 파싱할 수 있다.
        // 이 때, (gson라이브러리 필요)

        JsonObject totalObj = JsonParser.parseString(responseText).getAsJsonObject();
        //System.out.println(&quot;total : &quot; + totalObj);

        //response 속성 접근
        JsonObject responseObj = totalObj.getAsJsonObject(&quot;response&quot;);
        //System.out.println(&quot;response : &quot;+ responseObj);

        //body 속성 접근
        JsonObject bodyObj = responseObj.getAsJsonObject(&quot;body&quot;);
        //System.out.println(&quot;body : &quot;+ bodyObj);

        //totalCount 속성 접근
        int totalCount = bodyObj.get(&quot;totalCount&quot;).getAsInt();
        //System.out.println(&quot;totalCount: &quot;+totalCount);

        //items (JsonArray형태)속성 접근 
        JsonArray itemArr = bodyObj.getAsJsonArray(&quot;items&quot;);
        System.out.println(&quot;itemArr : &quot;+itemArr);


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


        //ites에 담겨 있는 item객체 하나씩 추출
        for(int i=0; i&lt;itemArr.size(); i++) {
            //JsonArr배열에서 하나씩 꺼내올 때 .size사용!

            JsonObject item = itemArr.get(i).getAsJsonObject();

            //System.out.println(item);

            Air air = new Air();
            air.setStationName(item.get(&quot;stationName&quot;).getAsString()); //측정소명
            air.setDataTime(item.get(&quot;dataTime&quot;).getAsString()); // 측정일시
            air.setKhaiValue(item.get(&quot;khaiValue&quot;).getAsString()); //통합대기화

            air.setPm10Value(item.get(&quot;pm10Value&quot;).getAsString()); //미세먼지농도
            air.setSo2Value(item.get(&quot;so2Value&quot;).getAsString()); //이황가스농도
            air.setCoValue(item.get(&quot;coValue&quot;).getAsString()); //일산화탄소농도
            air.setNo2Value(item.get(&quot;no2Value&quot;).getAsString()); //이산화탄소농도
            air.setO3Value(item.get(&quot;o3Value&quot;).getAsString()); //오존농도

            list.add(air);
        }

        System.out.println(&quot;list :&quot; +list);

        // list에 담긴 VO 객체확인
        for( Air air : list) {
            System.out.println(&quot;air: &quot;+air);
        }

        // 5. 다 사용한 스트림 객체 반납하기
        br.close();
        urlConn.disconnect();
    }

}</code></pre>
<blockquote>
<p>출력 구문 주소 브라우저에서 검색하면 결과확인 해 볼 수 있음 
<strong>resultType : XML</strong>
<img src="https://velog.velcdn.com/images/ee_ji0/post/526f9935-5491-45dc-8179-a5cb2b00197c/image.png" alt="">
<strong>resultType : JSON</strong>
<img src="https://velog.velcdn.com/images/ee_ji0/post/efd61f44-4554-46e0-96b1-d9b064848a31/image.png" alt=""></p>
</blockquote>
<h3 id="💧indexjsp">💧index.jsp</h3>
<blockquote>
<p><strong>비동기식으로 웹 어플리케이션에  적용하고자 할 때의 절차</strong></p>
</blockquote>
<ol>
<li>Jsp에서 현재 웹 애플리케이션 서버로 ajax 요청</li>
<li>Controller에서 요청 받기 (이때 요청 시 전달값이 있다면 기록)</li>
<li>HttpURLConnection 객체 활용해서 OpenAPI서버에 요청하여 응답데이터 받기</li>
<li>3번 과정에서의 응답데이터를 Client에게 다시 응답</li>
<li>Client측에서 돌려받은 응답데이터를 가지고 파싱 작업 후 웹 페이지에 시각화 하기</li>
</ol>
<pre><code class="language-jsp">&lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot; pageEncoding=&quot;UTF-8&quot;%&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot;&gt;

&lt;!-- jquery 연결 --&gt;
&lt;script src=&quot;https://code.jquery.com/jquery-3.7.0.min.js&quot; integrity=&quot;sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt;

&lt;title&gt;대기오염 공공데이터&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;실시간 대기오염 정보&lt;/h1&gt;
    지역:
    &lt;select id=&quot;location&quot;&gt;
        &lt;option&gt;서울&lt;/option&gt;
        &lt;option&gt;부산&lt;/option&gt;
        &lt;option&gt;대전&lt;/option&gt;
    &lt;/select&gt;

    &lt;button id=&quot;btn1&quot;&gt;해당 지역 대기 오염 정보&lt;/button&gt;
    &lt;br&gt;
    &lt;br&gt;

    &lt;table border=&quot;1&quot; id=&quot;result1&quot;&gt;
        &lt;thead&gt;
            &lt;tr&gt;
                &lt;th&gt;측정소명&lt;/th&gt;
                &lt;th&gt;측정일시&lt;/th&gt;
                &lt;th&gt;통합대기환경수치&lt;/th&gt;
                &lt;th&gt;미세먼지농도&lt;/th&gt;
                &lt;th&gt;아황산가스농도&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;/tbody&gt;
    &lt;/table&gt;

    &lt;script&gt;
           $(function(){
               $(&quot;#btn1&quot;).click(function(){
                                       // 응답 데이터를 xml 형식으로 받을 때
                    $.ajax({
                        url : &quot;air&quot;,
                        data : {
                            location : $(&quot;#location&quot;).val()
                        },
                        success : function(result) {
                            console.log(result);

                            // $(&#39;요소명&#39;).find(매개변수)
                            // - 기준이 되는 요소의 하위 요소들 중 특정 요소를 찾을 때 사용
                            // - html, xml은 같은 마크업 언어기 때문에 사용 가능하다
                            // console.log($(result).find(&quot;item&quot;))

                            // xml형식의 응답데이터를 받았을 때 
                            // 1. 넘겨 받은 데이터를 Jquesry화 시킨 후에
                            //   응답데이터 안에 실제 데이터가 담겨 잇는 요소를 선택해 준다

                            const itemArr = $(result).find(&quot;item&quot;);
                            // 2. 반복문을 통해 실제 데이터가 담긴 요소들에 접근해서 동적으로 요소 만들기
                            let value;
                            itemArr.each(function(index, item) {

                                //console.log(item)                                   
                                console.log($(item).find(&quot;stationName&quot;)
                                        .text())

                                value += &quot;&lt;tr&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;stationName&quot;)
                                                .text() + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;dataTime&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;khaiValue&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;pm10Value&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;so2Value&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;coValue&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;no2Value&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;td&gt;&quot;
                                        + $(item).find(&quot;o3Value&quot;).text()
                                        + &quot;&lt;/td&gt;&quot; + &quot;&lt;/tr&gt;&quot;

                            })

                            //3. 동적으로 만들어낸 요소를 화면에 출력
                            $(&quot;#result1 &gt; tbody&quot;).html(value);

                        },
                        error : function() {
                            console.log(&quot;통신실패&quot;)
                        }
                });

            });


        });
   &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3 id="💧openapicontrollerjavajson형식">💧OpenAPIController.java(JSON형식)</h3>
<pre><code class="language-java">package com.kh.opendata.controller;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class OpenAPIController {

    //발급 받은 인증키 변수처리
    public static final String SERVICEKEY = &quot;서비스키&quot;;


    //Json형식으로 대기오염 OpenAPI 활용하기
    @RequestMapping(value = &quot;air&quot;, produces = &quot;application/json; charset=UTF-8&quot;)
    @ResponseBody
    public String airMethod( String location ) throws IOException{ 

        //OpenAPI 서버로 요청하고자 하는 url 작성
        String url =&quot;http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty&quot;;

        url += &quot;?serviceKey=&quot; + SERVICEKEY;  //서비스키 추가
        url += &quot;&amp;numOfRows=&quot;+10; // 옵션 선택이고 행의 개수
        url += &quot;&amp;sidoName=&quot;+ URLEncoder.encode(location,&quot;UTF-8&quot;); //지역명 추가(한글이 들어가면 인코팅 추가)
        url += &quot;&amp;returnType=json&quot;;    // 리턴 타입    


        // 1. 작성된 url 정보를 넣어 URL 객체 생성
        URL requestUrl = new URL(url);

        // 2. 생성된 URL 객체로 URLCpnnection 생성
        HttpURLConnection urlConn = (HttpURLConnection)requestUrl.openConnection();

        //3. 요청 시 필요한 Header 설정
        urlConn.setRequestMethod(&quot;GET&quot;);

        //4. 해당 OpenAPI 서버로 요청 후 입력스트립을 통해서 응답객체 얻어오기
        BufferedReader br = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));


        String responseText =&quot;&quot;;
        String line;
        while((line = br.readLine()) != null){
            responseText += line;
        }

        //5. 다 사용한 스트림 반납 및 연결 해제
        br.close();
        urlConn.disconnect();


        return responseText;
    }
}
</code></pre>
<h3 id="💧openapicontrollerjavaxml형식">💧OpenAPIController.java(XML형식)</h3>
<pre><code class="language-java">    @RequestMapping(&quot;air&quot;)
    public String airPollution(String location) throws IOException {

        String url =&quot;http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/getCtprvnRltmMesureDnsty&quot;;

        url += &quot;?serviceKey=&quot; + SERVICEKEY;  //서비스키 추가
        url += &quot;&amp;sidoName=&quot;+ URLEncoder.encode(location,&quot;UTF-8&quot;); //지역명 추가(한글이 들어가면 인코팅 추가)
        url += &quot;&amp;returnType=xml&quot;;
        url += &quot;&amp;numOfRows=&quot;+20; // 옵션 선택이고 행의 개수

        //1. 작성된 url 정보를 넣어 URL 객체 생성
        URL requestUrl = new URL(url);

        //2. 생성된 URL 객체로 URLCpnnection 생성
        HttpURLConnection urlConn = (HttpURLConnection)requestUrl.openConnection();

        //3. 요청 시 필요한 Header 설정
        urlConn.setRequestMethod(&quot;GET&quot;);

        //4. 해당 OpenAPI 서버로 요청 후 입력스트립을 통해서 응답객체 얻어오기
        BufferedReader br = new BufferedReader(new InputStreamReader(urlConn.getInputStream()));

        String responseText =&quot;&quot;;
        String line;
        while((line = br.readLine()) != null){
            responseText += line;
        }

        //5. 다 사용한 스트림 반납 및 연결 해제
        br.close();
        urlConn.disconnect();

        System.out.println(responseText);

        return responseText;

    }</code></pre>
<h3 id="💧결과-화면">💧결과 화면</h3>
<p><img src="https://velog.velcdn.com/images/ee_ji0/post/882e9383-ce58-4b8a-a904-af62bc2fee93/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>