<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>HWANG.log</title>
        <link>https://velog.io/</link>
        <description>성격존나급한 개발자</description>
        <lastBuildDate>Wed, 30 Oct 2024 06:40:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>HWANG.log</title>
            <url>https://velog.velcdn.com/images/js_hwang/profile/6cf75069-6dcf-4ade-884f-04d653f3e856/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. HWANG.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/js_hwang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[톰캣으로 돌리는 프로젝트 Let's Encrypt (SSL -HTTPS) 적용하기]]></title>
            <link>https://velog.io/@js_hwang/%ED%86%B0%EC%BA%A3%EC%9C%BC%EB%A1%9C-%EB%8F%8C%EB%A6%AC%EB%8A%94-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Lets-Encrypt-SSL-HPPTS-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@js_hwang/%ED%86%B0%EC%BA%A3%EC%9C%BC%EB%A1%9C-%EB%8F%8C%EB%A6%AC%EB%8A%94-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Lets-Encrypt-SSL-HPPTS-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 30 Oct 2024 06:40:25 GMT</pubDate>
            <description><![CDATA[<p>CentOS 서버에 테스트 프로젝트를 하나 돌리고 있는데 
Http 통신을 하고 있어서 외부에 보여주기 그래서 
Https 통신을 할 수 있도록 세팅을 좀 해볼려한다.</p>
<h3 id="1-cerbot-설치">1. Cerbot 설치</h3>
<pre><code>sudo yum install certbot</code></pre><p>yum 패키지 관리자를 이용해서 Cerbot 설치를 한다.</p>
<h3 id="2-ssl-인증서-발급">2. SSL 인증서 발급</h3>
<pre><code>sudo certbot certonly --standalone -d demo.내도메인.com</code></pre><p>cerbot 을 통해서 demo.내도메인.com 도메인에 대한 SSL 인증서를 발급받았다.
standalone 모드로 인증서를 발급 받기위해서 기존에 돌아가고 있던 Tomcat, nginx 를 중지한다.</p>
<blockquote>
<p>*<em>standalone 모드 *</em>:
Cerbot이 자체적으로 간단한 웹 서버를 실행하여 Let&#39;s Encrypt 인증 과정을 처리하는 방식이다. 이때는 Cerbot이 Http-01 챌린지(포트80)을 통해 도메인 소유 여부를 확인한다.</p>
</blockquote>
<h3 id="3-tomcat에-사용할-keystore-생성">3. Tomcat에 사용할 keystore 생성</h3>
<pre><code>sudo openssl pkcs12 -export -in /etc/letsencrypt/live/demo.내도메인.com/fullchain.pem \
    -inkey /etc/letsencrypt/live/demo.내도메인.com/privkey.pem \
    -out /etc/letsencrypt/live/demo.내도메인.com/keystore.p12 \
    -name tomcat -CAfile /etc/letsencrypt/live/demo.내도메인.com/chain.pem -caname root
</code></pre><p>Tomcat에서 인증서를 사용하기 위해 openssl 을 사용하여 인증서와 개인 키 파일을 PKCS#12 형식(keystore.p12)로 변환해주었다.</p>
<p>비밀번호는 server.xml 에서 쓰일거니깐 입력하고, 비밀번호 확인부분도 동일하게 입력만 해주면 된다.</p>
<h3 id="4-tomcat-설정-serverxml-구성">4. Tomcat 설정 (server.xml) 구성</h3>
<pre><code>&lt;Connector port=&quot;443&quot; protocol=&quot;org.apache.coyote.http11.Http11NioProtocol&quot;
           maxThreads=&quot;150&quot; SSLEnabled=&quot;true&quot;&gt;
    &lt;SSLHostConfig&gt;
        &lt;Certificate certificateKeystoreFile=&quot;/etc/letsencrypt/live/demo.내도메인.com/keystore.p12&quot;
                     type=&quot;RSA&quot;
                     certificateKeystorePassword=&quot;설정한 비밀번호&quot; /&gt;
    &lt;/SSLHostConfig&gt;
&lt;/Connector&gt;
</code></pre><p>Https 커넥터를 추가하고 keystore 파일 경로와 비밀번호를 설정해준다.</p>
<pre><code>&lt;Connector port=&quot;80&quot; protocol=&quot;HTTP/1.1&quot;
           connectionTimeout=&quot;20000&quot;
           redirectPort=&quot;443&quot; /&gt;</code></pre><p>이제 포트80요청을 HTTPS로 리다이렉트하도록 추가만 하면 끝!</p>
<p><strong>Tomcat을 재시작하고 HTTPS가 적용되었는지 확인해보자.</strong></p>
<h3 id="추가-자동-갱신-설정">추가+ (자동 갱신 설정)</h3>
<p>음 추가로 Let&#39;s Encrypt의 인증서는 유효기간이 90일이다.
그래서 만료전에 주기적으로 갱신해야하는데 너무 귀찮다 ;;</p>
<p>그래서 그냥 자동으로 갱신 할 수 있도록 세팅을 해놓자</p>
<pre><code>sudo crontab -e</code></pre><p>Cerbot의 갱신 스크립트를 크론탭에 추가해서 주기적으로 실행 할 수 있도록한다.</p>
<pre><code>0 0 * * * /usr/bin/certbot renew --quiet &amp;&amp; /home/gaonsoft/www/gnbis/opt/apache-tomcat-8.5.65/bin/shutdown.sh &amp;&amp; /home/gaonsoft/www/gnbis/opt/apache-tomcat-8.5.65/bin/startup.sh</code></pre><p>이와 같이 크론탭 항목을 추가하여 매일 자정에 
톰캣 서버 종료 및 실행을 해주어서 자동갱신을 완료한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React Native - AI 추천 데이터를 일관성있게 정리 후 다른 페이지로 넘겨 자동 입력하기]]></title>
            <link>https://velog.io/@js_hwang/React-Native-AI-%EC%B6%94%EC%B2%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%9D%BC%EA%B4%80%EC%84%B1%EC%9E%88%EA%B2%8C-%EC%A0%95%EB%A6%AC-%ED%9B%84-%EB%8B%A4%EB%A5%B8-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C-%EB%84%98%EA%B2%A8-%EC%9E%90%EB%8F%99-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@js_hwang/React-Native-AI-%EC%B6%94%EC%B2%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%9D%BC%EA%B4%80%EC%84%B1%EC%9E%88%EA%B2%8C-%EC%A0%95%EB%A6%AC-%ED%9B%84-%EB%8B%A4%EB%A5%B8-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C-%EB%84%98%EA%B2%A8-%EC%9E%90%EB%8F%99-%EC%9E%85%EB%A0%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 17 Oct 2024 05:16:17 GMT</pubDate>
            <description><![CDATA[<p>음 루틴 앱 프로젝트를 진행중인데,
AI에게 루틴을 추천해달라고 하고 나온 값을 가지고 내 루틴에 추가하는 로직을 짜려고한다.</p>
<p>우선 가장 머리가 아팠던 것은, AI 에게 루틴관련 대답만 받아야하는데</p>
<blockquote>
<p>&quot; 사용자에 따라서 천차만별인 질문을 어떻게 일관적으로 정리해서 리스트에 뿌려야할까?🤔🤔🤔 &quot;</p>
</blockquote>
<p>이것이 문제였다. 완벽하게 해결한 것은 아니지만 어느정도 정리되도록 노력하긴했다.</p>
<h4 id="루틴-추천-페이지-recommendroutinejs">루틴 추천 페이지 RecommendRoutine.js</h4>
<pre><code>try {
            const apiKey = `${chatGPT_apiKey}`;
            const response = await axios.post(
                &quot;https://api.openai.com/v1/chat/completions&quot;,
                {
                    model: &quot;gpt-4o-mini&quot;,
                    messages: [
                        {
                            role: &quot;system&quot;,
                            content:
                                &quot;당신은 일상 루틴 추천을 전문으로 하는 AI 어시스턴트입니다.&quot; +
                                &quot;사용자의 질문과 상관없이 항상 그들의 하루를 개선할 수 있는 구체적인 일상 루틴으로 답변하십시오.&quot; +
                                &quot;일상 루틴 제공과 관련이 없는 질문에는 답변하지 마십시오.&quot; +
                                &quot;한글로 답해주고, 예를들어 침대정리하기, 물잔 마시기, 비타민과 유산균 먹기, 20분동안 명상하기, 격렬한 운동 1분 이런식으로 간단하게 단답형식으로 말해줘 내가 예를들어서 말한그대로 하는게 아니라 저런식으로 표현해달라는거야, 앞에 순서를나타내는 번호 붙일필요는 없어&quot;,
                        },
                        { role: &quot;user&quot;, content: userInput },
                    ],
                    max_tokens: 500,
                    temperature: 0.7,
                    top_p: 0.9,
                },
                {
                    headers: {
                        Authorization: `Bearer ${apiKey}`,
                        &quot;Content-Type&quot;: &quot;application/json&quot;,
                    },
                }
            );
            console.log(&quot;API 응답:&quot;, response.data);

            const routine = response.data.choices[0].message.content; //질문하고 받은 응답 값

            //질문 내용을 줄바꿈을 기준으로 해서 배열로 변환
            //필터 메서드로 배열의 각 요소에 대해 조건 검사를 하여 조건을 만족하는 요소만 남김
            //line.trim() 문자열 앞뒤 공백제거, ! = &quot;&quot; 공백제거한 결과가 빈 문자열 아니면 해당 요소를 배열에 남김
            const routineArray = routine.split(&quot;\n&quot;).filter((line) =&gt; line.trim() !== &quot;&quot;);  

            // 만들어진 배열을 상태로 저장해서 나중에 데이터 표시할 수 있도록 함
            // setRecoomendation 은 recommedation에다가 상태값을 변경해주는 업데이트 함수임
            setRecommendation(routineArray);
        } catch (error) {
            console.error(&quot;루틴 추천 받는데 에러났음&quot;, error);
            setRecommendation([&quot;루틴 추천 중 오류 발생&quot;]);
        } finally {
            setLoading(false);
        }</code></pre><p>대답해주는 시스템에게 역할 부여해주는 방식으로 진행했다.
AI 시스템에게 직업을 정해주고, 그외 조건들을 추가시켜 필요한 답변만 응답 할 수 있도록
content를 정해주었다.</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/84ed544d-b4c0-48f6-a57f-d970801de0fb/image.gif" alt=""></p>
<p>(사실 역할 부여하는데 있어서 참 많은 시도를 해보았는데 이게 그나마 최선이었다..😭
더 좋은 방법이 있다면 누군가 좀 알려주세요...)</p>
<p>추가로 대답해준 내용을 배열처리해서 그거를 리스트로 보여주기 위한 작업을 했다.</p>
<pre><code>const [recommendation, setRecommendation] = useState([]);

const routine = response.data.choices[0].message.content; //질문하고 받은 응답 값

            //질문 내용을 줄바꿈을 기준으로 해서 배열로 변환
            //필터 메서드로 배열의 각 요소에 대해 조건 검사를 하여 조건을 만족하는 요소만 남김
            //line.trim() 문자열 앞뒤 공백제거, ! = &quot;&quot; 공백제거한 결과가 빈 문자열 아니면 해당 요소를 배열에 남김
            const routineArray = routine.split(&quot;\n&quot;).filter((line) =&gt; line.trim() !== &quot;&quot;);  

            // 만들어진 배열을 상태로 저장해서 나중에 데이터 표시할 수 있도록 함
            // setRecoomendation 은 recommedation에다가 상태값을 변경해주는 업데이트 함수임
            setRecommendation(routineArray);</code></pre><p>routine 에다가 질문하고 응답한 값을 할당해주고
응답값을 줄바꿈을 기준으로해서 배열로 변환해준다.</p>
<p>배열값(routineArray)을 setRecommendation() 함수를 이용해서
recommendation에다가 상태값을 업데이트 시켜준다.
그럼 처음에는 빈 값이던 recommendation이 질문 후에는 업데이트 되어서
루틴 배열이 담기게 된다. </p>
<p>자 이제 화면에 뿌려줘야 겠지?</p>
<pre><code>&lt;ScrollView&gt;
  {recommendation.length &gt; 0 ? (
      recommendation.map((routine, index) =&gt; (        //map() 으로 배열의 각 요소를 순회하면 새로운 컴포넌트 생성
           &lt;TouchableOpacity key={index} style={styles.aiList}
              onPress={() =&gt; handleRoutineSelect(routine)}
           &gt;
              &lt;Image
                 style={[styles.aiListIcon]}
                 source={require(&quot;../../../assets/img/ic_checked_03.png&quot;)}
              &gt;&lt;/Image&gt;
              &lt;Text style={styles.aiListText}&gt;{routine}&lt;/Text&gt;
           &lt;/TouchableOpacity&gt;
    ))
    ) : (
        &lt;Text&gt;&lt;/Text&gt;
    )}
&lt;/ScrollView&gt;</code></pre><p>처음 렌더링 때는 recommendation 상태 값이 비어있으니 아무것도 나오지 않을 것이고
질문 후 상태 값이 업데이트가 될 것이다
map() 함수를 이용해서 배열의 각 요소를 순회하면서 리스트를 뿌려준다.</p>
<p>콘솔에다가 routine 과 index를 찍어보면 해당 값이 나온다.</p>
<pre><code>첫 번째 순회:
routine = &quot;침대 정리하기&quot;
index = 0
두 번째 순회:
routine = &quot;물잔 마시기&quot;
index = 1
세 번째 순회:
routine = &quot;비타민과 유산균 먹기&quot;
index = 2</code></pre><p><img src="https://velog.velcdn.com/images/js_hwang/post/fe68a0e1-be7a-4fd5-9d1b-7a7bb77f2b05/image.png" alt=""></p>
<p>추천만 해주면 사용자가 좋아할까? 내 루틴으로 바로 추가할 수 있게 해주면 더 좋아하겠지? ㅎㅋ</p>
<pre><code>const navigation = useNavigation();

const handleRoutineSelect = (routine) =&gt; {
        navigation.navigate(&quot;AddRoutine&quot;, {selectedRoutine: routine});
};</code></pre><p>리스트를 클릭하면 AddRoutine 페이지로 루틴데이터를 가지고 넘어가보자</p>
<h4 id="루틴-추가-페이지-addroutinejs">루틴 추가 페이지 AddRoutine.js</h4>
<p>route 객체를 통해서 전달된 데이터에 접근해보자!</p>
<pre><code>const route = useRoute();

useEffect(() =&gt; {
        if (route.params?.selectedRoutine) {
            setRoutineName(route.params.selectedRoutine);
        }
    }, [route.params?.selectedRoutine]); 

    &lt;RoutineInput
      placeholder=&quot;루틴을 입력해주세요&quot;
      value={routineName}
      onChangeText={setRoutineName}
      onPress={navigateToRecommendRoutine}
      buttonImage={require(&quot;../../../assets/img/ic_ai.png&quot;)} // 아이콘 이미지를 props로 전달
    /&gt;</code></pre><p>추천페이지에서 selectedRoutine 에다가 선택한 루틴을 넣어서 AddRoutine으로 넘기고
루틴추가페이지에서 route로 접근해서
잘넘겨주었다</p>
<blockquote>
<h4 id="🥲-ai-사용법에-대해서-더-좋은-방법을-아는-고수들이-있다면-공유-부탁드립니다-ㅜ">🥲 AI 사용법에 대해서 더 좋은 방법을 아는 고수들이 있다면 공유 부탁드립니다 ㅜ</h4>
</blockquote>
<h4 id="쨋든--처음보다는-훨나아진-루틴-추천-및-추가-작업-과정이었다">쨋든 ! 처음보다는 훨나아진 루틴 추천 및 추가 작업 과정이었다..</h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[ReactNative Hooks (useState, useEffect, useRef)]]></title>
            <link>https://velog.io/@js_hwang/ReactNative-Hooks-useState-useEffect-useRef</link>
            <guid>https://velog.io/@js_hwang/ReactNative-Hooks-useState-useEffect-useRef</guid>
            <pubDate>Thu, 10 Oct 2024 01:46:58 GMT</pubDate>
            <description><![CDATA[<p>리액트 네이티브에서 가장 많이 쓰이는 훅들을 좀 정리하려고 한다.
잘알지는 못하지만 아는 선에서만 조금 정리를 해보자</p>
<h2 id="1-usestate상태관리">1. useState(상태관리)</h2>
<p>useState는 컴포넌트의 상태를 관리하는데 사용된다. 상태가 변경되면 다시 렌더링된다.</p>
<pre><code>import React, { useState } from &#39;react&#39;;
import { Text, Button, View } from &#39;react-native&#39;;

function Counter() {
  const [count, setCount] = useState(0); // count라는 상태와, setCount라는 상태를 변경하는 함수가 생김

  return (
    &lt;View&gt;
      &lt;Text&gt;Count: {count}&lt;/Text&gt;
      &lt;Button title=&quot;Increase&quot; onPress={() =&gt; setCount(count + 1)} /&gt;
    &lt;/View&gt;
  );
}
</code></pre><p>위에 코드로 정리를 하자면
count : 컴포넌트에서 관리하고자 하는 상태 값을 나타낸다.
setCount : 상태를 변경할 수 있는 함수이다. 이 함수가 호출되면 상태가 변경되고 컴포넌트가 다시 렌더링된다.
useState(0) : 0은 상태의 초기값이다. 컴포넌트가 처음 렌더링될 때 이값으로 상태가 설정된다.</p>
<p>처음화면이 렌더링되면 0으로 시작해서, 버튼을 클릭해 setCount 함수가 작동하면
상태가 변경되어 재렌더링이 된다.</p>
<p>useState는 초기 상태값으로 기본 데이터타입 뿐만 아니라 함수나 객체도 사용할 수 있다.</p>
<h4 id="기본값-예시">기본값 예시</h4>
<pre><code>const [count, setCount] = useState(0); // 숫자형 초기 값
const [name, setName] = useState(&#39;&#39;); // 문자열 초기 값
const [isVisible, setIsVisible] = useState(false); // 불리언 초기 값
</code></pre><h4 id="객체나-배열을-초기-값으로-설정한-예시">객체나 배열을 초기 값으로 설정한 예시</h4>
<pre><code>const [user, setUser] = useState({ name: &#39;John&#39;, age: 25 });
const [items, setItems] = useState([1, 2, 3]);
</code></pre><h4 id="상태변경이-리렌더링을-유발하는-이유">상태변경이 리렌더링을 유발하는 이유</h4>
<p>useState로 상태가 변경되면 리액트는 그 상태가 변경되었음을 감지하고, 해당 컴포넌트만 다시 렌더링한다.
가상 DOM 매커니즘에 따라, 변경된 부분만 다시 그려지기 때문에 성능이 최적화된다.</p>
<blockquote>
<p>useState 요약</p>
</blockquote>
<ul>
<li>useState는 함수형 컴포넌트에서 상태를 관리할 수 있게 해주는 기본 훅이다.</li>
<li>setState 함수는 상태를 변경하고 컴포넌트를 리렌더링 하는 역할을 한다.</li>
<li>복잡한 객체나 배열을 관리할 때는 불변성을 유지하면서 상태를 업데이트해야한다.</li>
<li>상태 업데이트는 비동기적으로 처리된다.</li>
</ul>
<hr>
<h2 id="2-useeffect부수효과">2. useEffect(부수효과)</h2>
<p>useEffect는 부수효과(side effect)를 처리하기 위해 사용되는 훅이다.
부수효과란 <strong>컴포넌트가 리렌더링될 때마다 발생하는 비동기 작업이나 DOM 조작, 외부 데이터를 가져오는 등의 작업</strong>을 의미한다.</p>
<p>컴포넌트의 라이프사이클 메서드인 componentDidMount, componentDidUpdate, componentWillUnmount를 대체하는 역할을 한다.</p>
<pre><code>useEffect(() =&gt; {
    //부수효과 코드
},[의존성배열]);</code></pre><ul>
<li>첫 번째 인자로 전달된 함수가 부수효과를 수행한다.</li>
<li>두 번째 인자로 의존성배열을 전달하면, <strong>이 배열에 있는 값이 변경될 때만 부수효과가 실행한다.</strong>
(의존성 배열을 사용하지 않으면 컴포넌트가 렌더링될때마다 useEffect 실행됨, 하단에 예시 있음)</li>
</ul>
<h3 id="부수효과-처리-타이밍">부수효과 처리 타이밍</h3>
<ul>
<li>컴포넌트가 마운트될 때(처음 렌더링될 때)</li>
<li>컴포넌트가 업데이트될 때(상태나 props가 변경될 때)</li>
<li>컴포넌트가 언마운트될 때(화면에서 사라질 때)</li>
</ul>
<h3 id="의존성-배열-유무에-대한-예시">의존성 배열 유무에 대한 예시</h3>
<h4 id="1-의존성배열이-없는-경우">1) 의존성배열이 없는 경우</h4>
<pre><code>useEffect(() =&gt; {
  console.log(&#39;렌더링될 때마다 실행&#39;);
});</code></pre><p>의존성 배열이 없는 경우, <span style="background-color: rgba(242,179,188,0.5)"><strong>컴포넌트가 렌더링될 때마다 useEffect가 실행</strong></span>됨, 상태나 props가 변경될 때마다 이 함수가 호출된다.</p>
<h4 id="2-의존성-배열이-빈-경우">2) 의존성 배열이 빈 경우</h4>
<pre><code>useEffect(() =&gt; {
  console.log(&#39;마운트 시 한 번만 실행&#39;);
}, []); // 빈 배열</code></pre><p>의존성 배열이 빈 배열이면, <span style="background-color: rgba(242,179,188,0.5)"><strong>컴포넌트가 마운트될 때 한번만 실행</strong></span>된다.</p>
<h4 id="3-의존성-배열에-값이-있는-경우">3) 의존성 배열에 값이 있는 경우</h4>
<pre><code>useEffect(() =&gt; {
  console.log(&#39;name이 변경될 때만 실행&#39;);
}, [name]); // name 상태가 변경될 때만 실행</code></pre><p><span style="background-color: rgba(242,179,188,0.5)"><strong>의존성 배열에 특정 값(name)이 있으면, 해당 값이 변경될 때만 부수효과가 실행</strong></span>된다.
이를 통해 필요할때만 부수효과 실행가능</p>
<blockquote>
<p>useEffect 요약</p>
</blockquote>
<ul>
<li>useEffect는 부수효과를 처리하는 훅으로, 컴포넌트가 렌더링되거나 업데이트 될 때 동작한다.</li>
<li>의존성 배열을 사용하여 특정 값이 변경될 때만 실행할 수 있도록 제어 가능</li>
<li>API 호출 및 이벤트리스너 관리 등 작업에 많이 사용됨</li>
</ul>
<hr>
<h2 id="3-useref참조">3. useRef(참조)</h2>
<p>이 또한 리액트에서 많이 쓰이는 훅 중 하나로,** DOM 요소에 직접 접근<strong>하거나, **리렌더링과 관계없이 값을 유지</strong>하고 싶을때 사용된다.
이 훅은 참조(Reference)를 생성하며, 해당 참조값은 컴포넌트가 리렌더링되더라도 변하지 않고 유지하게 된다. 그렇기 때문에 DOM 조작이나 특정 값을 저장해두고 싶을 때 유용하다.</p>
<h3 id="특징">특징</h3>
<ul>
<li>DOM 요소 접근 가능</li>
<li>리렌더링을 유발하지 않으면서 값 저장가능</li>
<li>리렌더링 하더라도 변하지 않는 참조 값 유지 가능</li>
<li>초기 값은 null로 설정할 수 있으며, DOM 요소나 변수를 참조할 때 값을 설정한다.</li>
</ul>
<h3 id="dom-요소-접근">DOM 요소 접근</h3>
<pre><code>import React, { useRef } from &#39;react&#39;;

function TextInputWithFocusButton() {
  const inputRef = useRef(null); // 초기값으로 null 설정

  const handleButtonClick = () =&gt; {
    // useRef를 사용해 input DOM 요소에 직접 접근하여 포커스 설정
    inputRef.current.focus();
  };

  return (
    &lt;div&gt;
      &lt;input ref={inputRef} type=&quot;text&quot; /&gt;
      &lt;button onClick={handleButtonClick}&gt;포커스 주기&lt;/button&gt;
    &lt;/div&gt;
  );
}</code></pre><ul>
<li>useRef(null)로 참조를 생성하고, <code>&lt;input&gt;</code>의 ref속성에 inputRef를 할당한다.</li>
<li>inputRef.current는 <code>&lt;input&gt;</code>요소를 가리키게 되며, 이를 통해 focus() 메서드를 호출하여 해당 <code>&lt;input&gt;</code>에 포커스를 줄 수 있다.</li>
<li><span style="background-color: rgba(242,179,188,0.5)"><strong>useRef로 DOM 을 참조해도 컴포넌트는 다시 렌더링 되지 않는다.</strong></span></li>
</ul>
<h3 id="리렌더링과-관계없는-값-저장">리렌더링과 관계없는 값 저장</h3>
<p>useRef는 리렌더링과 관계없이 값을 유지할 수 있다. 컴포넌트가 렌더링 될 때마다 값이 변하지 않게 하려면 useRef를 사용할 수 있다. useRef의 current 값은 컴포넌트가 리렌더링 되어도 그대로 유지되며, 값을 변경해도 리렌더링을 유발하지 않는다.</p>
<pre><code>import React, { useState, useRef } from &#39;react&#39;;

function Timer() {
  const [count, setCount] = useState(0);
  const countRef = useRef(0); // 초기값 0 설정

  const incrementCount = () =&gt; {
    countRef.current += 1; // current 값이 변경되어도 리렌더링되지 않음
    console.log(&quot;Ref count:&quot;, countRef.current); // 콘솔에 current 값 출력
  };

  const updateCount = () =&gt; {
    setCount(countRef.current); // 상태로 값을 업데이트 -&gt; 리렌더링 발생
  };

  return (
    &lt;div&gt;
      &lt;p&gt;Count: {count}&lt;/p&gt;
      &lt;button onClick={incrementCount}&gt;Ref 값 증가&lt;/button&gt;
      &lt;button onClick={updateCount}&gt;상태 업데이트&lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre><ul>
<li>countRef는 컴포넌트가 리렌더링될 때도 값이 그대로 유지된다.</li>
<li>countRef.current 값을 증가시켜도 컴포넌트가 리렌더링되지 않는다.</li>
<li>상태로 값을 반영하고 싶을 때는 setCount를 호출해 리렌더링을 유발할 수 있다.</li>
</ul>
<blockquote>
<p>useRef 요약</p>
</blockquote>
<ul>
<li>useRef는 DOM 요소나 리렌더링과 관계없는 값을 참조하는 데 사용된다.</li>
<li>값이 변경되어도 리렌더링을 유발하지 않으면서 상태를 저장할 수 있다.</li>
<li>포커스 관리,애니메이션제어, DOM접근, 이전 상태 기억 등에서 많이 사용된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 블루투스 키보드 연결시 웹뷰(액티비티) 재생성 현상 방지하기]]></title>
            <link>https://velog.io/@js_hwang/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4-%ED%82%A4%EB%B3%B4%EB%93%9C-%EC%97%B0%EA%B2%B0%EC%8B%9C-%EC%9B%B9%EB%B7%B0%EC%95%A1%ED%8B%B0%EB%B9%84%ED%8B%B0-%EC%9E%AC%EC%83%9D%EC%84%B1-%ED%98%84%EC%83%81-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@js_hwang/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EB%B8%94%EB%A3%A8%ED%88%AC%EC%8A%A4-%ED%82%A4%EB%B3%B4%EB%93%9C-%EC%97%B0%EA%B2%B0%EC%8B%9C-%EC%9B%B9%EB%B7%B0%EC%95%A1%ED%8B%B0%EB%B9%84%ED%8B%B0-%EC%9E%AC%EC%83%9D%EC%84%B1-%ED%98%84%EC%83%81-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 08 Oct 2024 00:41:10 GMT</pubDate>
            <description><![CDATA[<p>나는 웹뷰로 된 앱을 관리 중에 있다. 처음보는 현상이 일어났고 그것을 해결했기에 까먹지 않게 해결 과정을 정리는 해놓을려고 한다.</p>
<blockquote>
<p>우선 해당 현상은** 블루투스 키보드 연결시 액티비티가 재생성되는 현상**이다. 이는 액티비티가 파괴되어서 다시 재생성 되는 것으로 보인다. 그 이유는 블루투스 키보드를 연결시 액티비티의 구성변경이 일어나기 때문에 재생성 되는 것으로 보인다. </p>
</blockquote>
<p>재생성에 대해서 자세하게 사이클을 한번 보자면</p>
<ul>
<li>onSaveInstanceState(Bundle) : 화면 상태를 저장한다.</li>
<li>onPause()</li>
<li>onStop()</li>
<li>onDestroy()</li>
<li>onCreate(Bundle)</li>
<li>onStart()</li>
<li>onRestoreInstanceState(Bundle) : 아까 저장했던 화면 상태를 복원시킨다</li>
<li>onResume()</li>
</ul>
<p>이런식으로 흘러간다.</p>
<p>하지만 그냥 다른 작업 필요없이 재생성시키지 않고 유지만 시키면 되기 때문에</p>
<p>AndroidMainfest.xml 파일에 있는 </p>
<pre><code>&lt;activity
     android:name=&quot;.MainActivity&quot;
     android:configChanges=&quot;keyboard|keyboardHidden|orientation|screenSize&quot;/&gt;</code></pre><p><em><strong>configChanges</strong></em> 에 변경될 수 있는 설정들을 지정해주기로 하였다.
이런식으로 설정을 하면 재생성 대신에 <em><strong>onConfigurationChange()</strong></em> 만을 호출한다.</p>
<hr>
<blockquote>
<p>그래서 편하게 저것만 수정해서 시스템에서 자동으로 흘러갈 수 있도록 해결을 할려했지만,
왠걸 configChanges 에 keyboard|keyboardhidden 을 넣어도 액티비티가 재생성되는 현상이 계속 발생한다.</p>
</blockquote>
<p>도저히 이유를 찾을 수가 없었다. 심지어 다른 키보드마다 먹히는게 있었고 안먹히는게 있어서 이유를 모른상태로 onSaveInstanceState() onRestoreInstanceState() 을 쓰는 등 여러가지 방법들을 시도하였는데, 특정 키보드는 아무리해도 해결이 되지 않았다. 2일~3일정도 방법을 계속 찾았지만 해결을 못하던 찰나,</p>
<h2 id="혹시-얘를-이-키보드를-블루투스-키보드로-인식하지-못하는-건-아닐까">혹시 얘를 이 키보드를 블루투스 키보드로 인식하지 못하는 건 아닐까?..</h2>
<p>라는 생각이 머리속을 스쳐가고,,,</p>
<p>혹시 몰라서 configChanges 에 넣을 수 있는것들을 하나씩 전부 넣어보면서 찾았는데</p>
<pre><code>&lt;activity
     android:name=&quot;.MainActivity&quot;
     android:configChanges=&quot;keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode|mcc|mnc|locale|fontScale|density|navigation&quot;/&gt;</code></pre><h4 id="문제는-바로-이놈">문제는 바로 이놈</h4>
<h1 id="navigation">navigation</h1>
<h4 id="이놈이다">이놈이다.</h4>
<p>“navigation”: Navigation Type ( 트랙볼 / DPad ) 가 변경되었을 때 ( 보통 절대 일어나지 않는 Case )</p>
<p>후,, 나는 하드웨어에 대해 잘 알지 못하지만,,
이 키보드 녀석을 DPAD 로 인식을 하고 있는거 같았다.</p>
<p>구글링을 했을때도 보통 절대 일어나지 않은 CASE 라고 봤었기에 간과하고 있었는데
이 녀석을 넣으니 아무일도 없었다는듯이 모든게 해결되었다.</p>
<p>혹시 나같은 현상을 겪은 사람들은 configChanges에 하나씩 넣으면서 어떤것으로 인식을 하고 있는지 확인하기 바란ㄷ...ㅠ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[toss slash 24 ]]></title>
            <link>https://velog.io/@js_hwang/toss-slash-24</link>
            <guid>https://velog.io/@js_hwang/toss-slash-24</guid>
            <pubDate>Mon, 02 Sep 2024 01:46:35 GMT</pubDate>
            <description><![CDATA[<p>요즘 사이드 프로젝트로 앱을 개발중인데
리액트 네이티브를 사용하고 있다.</p>
<p>백엔드를 많이 접해본 나로서는 프론트쪽이 부족하여
배울 것이 많을 것 같아</p>
<p>이번에 toss slash24를 신청하게되었다.</p>
<p>아마 배울 것이 엄청 많고 몰랐던 것을 많이 접해볼 수 있는 기회가 될 것같다...</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/b33d0d1a-e0aa-498f-a7a1-dfbf1d00ee74/image.png" alt=""></p>
<p>더 나은 개발자가 되기 위해,, 우리 모두 화이팅</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버로그 - 파일로 저장하기]]></title>
            <link>https://velog.io/@js_hwang/%EC%84%9C%EB%B2%84%EB%A1%9C%EA%B7%B8-%ED%8C%8C%EC%9D%BC%EB%A1%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@js_hwang/%EC%84%9C%EB%B2%84%EB%A1%9C%EA%B7%B8-%ED%8C%8C%EC%9D%BC%EB%A1%9C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 18 Jul 2024 03:55:57 GMT</pubDate>
            <description><![CDATA[<p>다른서버로 데이터를 전송할 일이 있는데 제대로 갔는지 안갔는지 
혹은 에러가 났는지 확인을 할 수 있어야 할 것 같다고 생각이 들었다</p>
<blockquote>
<p>💥 그래서 찾아본 방법 중 하나인 Logger을 이용해서 txt 파일에 찍힌 로그 내용을 저장해서
서버에 특정 폴더에 넣을려고 한다.</p>
</blockquote>
<p>그냥 간단하게 java.util.logging 패키지를 이용해서
Logger를 설정하고 파일 핸들러를 추가하여 로그를 파일에 저장하는 방식을 이용</p>
<pre><code>
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

public class SendLogger {
    private static final Logger logger = Logger.getLogger(SendLogger.class.getName());

    static {
        try {
             FileHandler fileHandler = new FileHandler(&quot;저장할 위치/application.log&quot;, 10 * 1024 * 1024, 5, true);
            fileHandler.setFormatter(new SimpleFormatter());
            logger.addHandler(fileHandler);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Logger getLogger() {
        return logger;
    }
}
</code></pre><p>파일은 최대 10MB , 넘어가면 다음 파일을 생성해서 저장한다.
파일 개수는 최대 5개</p>
<p>만약 5개가 넘어간다면 가장 오래된 파일이 삭제되고 새로운 파일이 생성된다.</p>
<p>그리고 어디서든 
Logger logger = SendLogger.getLogger();
을 선언해서</p>
<p>logger.severe(&quot;저장할 로그내용&quot;);</p>
<p>이런식으로 써주게 되면 </p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/d92c080c-4ea7-4744-bd97-34975fca87be/image.png" alt=""></p>
<p>이런식으로 잘 저장이 된 걸 볼 수 있다 !</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis(In-Memory DB)를 사용해서 속도를 끌어올리자]]></title>
            <link>https://velog.io/@js_hwang/RedisIn-Memory-DB%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%86%8D%EB%8F%84%EB%A5%BC-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EC%9E%90</link>
            <guid>https://velog.io/@js_hwang/RedisIn-Memory-DB%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%86%8D%EB%8F%84%EB%A5%BC-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%AC%EC%9E%90</guid>
            <pubDate>Wed, 17 Jul 2024 01:46:22 GMT</pubDate>
            <description><![CDATA[<p>웹/앱을 다하고 있지만 하드웨어 관련된 프로그램을 개발하는건 역시 손도 많이가고 신경써야할 부분이 너무 많은 것 같다</p>
<p>우선 원래 구현 해놓은 것은</p>
<blockquote>
<p>단말기 태깅 -&gt; 태깅 데이터 우리서버에서 가공 -&gt; 가공한데이터를 다른 서버로 전송</p>
</blockquote>
<p>해당 로직으로 구현을 해놓았다.</p>
<p>그런데 한번에 많은 사람들이 태깅을 하다보니 통신에 문제인가? 속도의 문제인가?
정확한 원인은 알 수 없지만 TimeOut 이 나는 것으로 보인다.</p>
<p>태깅하면 탑승자의 정보를 MYSQL에다가 정보를 담고 있었는데,</p>
<p>MYSQL에 관련된 로직은 다 빼고 Redis로 처리를 하고,
나중에 Redis에 있는 탑승자 정보를 스케줄러를 통해서 MYSQL로 데이터를 옮길려고 한다.</p>
<p>Redis 를 쓰면 속도가 올라가서 타임아웃에러가 안나길 바라면서 진행하였다..
많은 자료구조로 넣을 수 있지만 Key-Value 값으로 처리하면 속도가 가장 빠르다고 하던데,
데이터 타입마다 다를 것 같긴하다. 어쨋든 key-Value 형식으로 진행!</p>
<blockquote>
</blockquote>
<p>간단하게 보면 <strong>key</strong> 값을 이용해 <strong>value</strong>(데이터)를 조회한다.</p>
<pre><code>set key1 &quot;HelloWorld&quot; 
</code></pre><p>로 넣으면 , </p>
<pre><code>get key1
-&gt; &quot;HelloWorld&quot;</code></pre><p>로 조회한다.</p>
<p>spring 프로젝트(gradle)였기에</p>
<p>📌 우선 Redis 세팅을 위해서 build.gradle 에다가 추가</p>
<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre><p>📌 그리고 Config 파일을 만들어서 설정값을 작성한다.</p>
<pre><code>Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(&quot;내 서버 IP&quot;, 6379);
    }

    @Bean
    public RedisTemplate&lt;String, Object&gt; redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate&lt;String, Object&gt; template = new RedisTemplate&lt;&gt;();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}</code></pre><p>(참고로 properties에 ip랑 port번호를 넣었는데 접근이 안되서 
LettuceConnectionFactory로 해결했다..)</p>
<p>자 이제 Redis 세팅 끝. </p>
<p>📌 이제 서비스단 만들어서 구현을 시작하면 된다.</p>
<pre><code>@Service
public class RedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private final ObjectMapper objectMapper = new ObjectMapper();

    public Map&lt;String, Object&gt; getBusDeviceInfo(String deviceID){
        String key = &quot;busDeviceInfo:&quot; + deviceID;
        String data = redisTemplate.opsForValue().get(key);

        if(data != null) {
            try {
                return objectMapper.readValue(data, Map.class);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public void cacheBusDeviceInfo(String deviceId, Map&lt;String, Object&gt; deviceInfo) {
        String key = &quot;busDeviceInfo:&quot; + deviceId;
        try {
            redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(deviceInfo));
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    public String getRouteIdentificationNumber(String deviceID) {
        Map&lt;String, Object&gt; deviceInfo = getBusDeviceInfo(deviceID);
        if(deviceInfo == null) {
            return null;
        }

        int areaCode = Integer.parseInt(deviceInfo.get(&quot;area_code&quot;).toString());
        String currentTime = LocalTime.now().format(DateTimeFormatter.ofPattern(&quot;HHmm&quot;));

        if(areaCode == 1 &amp;&amp; currentTime.compareTo(&quot;0000&quot;) &gt;= 0 &amp;&amp; currentTime.compareTo(&quot;1159&quot;) &lt;= 0) {
            return &quot;ChangwonBundang&quot;;
        } else if(areaCode == 1 &amp;&amp; currentTime.compareTo(&quot;1159&quot;) &gt; 0) {
            return &quot;BundangChangwon&quot;;
        } else if(areaCode == 2 &amp;&amp; currentTime.compareTo(&quot;0000&quot;) &gt;= 0 &amp;&amp; currentTime.compareTo(&quot;1159&quot;) &lt;=0) {
            return &quot;BundangChangwon&quot;;
        } else if(areaCode == 2 &amp;&amp; currentTime.compareTo(&quot;1159&quot;) &gt; 0) {
            return &quot;ChangwonBundang&quot;;
        }

        return null;
    }

    public Map&lt;String, Object&gt; getBusInfo(int biCode) {
        String key = &quot;busCode:&quot; + biCode;
        String data = redisTemplate.opsForValue().get(key);

        if(data != null) {
            try {
                return objectMapper.readValue(data, Map.class);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    public void insertPassengerInfo(Map&lt;String, Object&gt; passengerInfo) {
        String key = &quot;busPassengerInfo:&quot; + passengerInfo.get(&quot;employeeId&quot;) + &quot;:&quot; + passengerInfo.get(&quot;tagDt&quot;);
        try {
            redisTemplate.opsForValue().set(key,objectMapper.writeValueAsString(passengerInfo));
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
}</code></pre><p>간단하게 보면 select와 insert가 있는데
단말기ID(deviceID)가 키값이 되기에,</p>
<p>busDeviceInfo:&quot;단말기에서 넘겨받은 ID값&quot; (key 값)으로
data를 조회한다.</p>
<p>그리고 insert부분은 insertPassengerInfo가 되는데
컨트롤러단에서 받아온 값을 파라미터로 넘겨받아서
busPassengerInfo:&quot;파라미터로 받은 값&quot;(key)값을 이용해서 데이터를 insert 시킨다.</p>
<p>후에 Redis에 이런식으로 처리를하여 가공한 데이터를 외부 서버로 보내었다.</p>
<p>처음해본 Redis 였는데, 서버에 메모리를 사용하여 데이터 처리를 한다는게 좀 신기했던 것 같고
속도가 빨라진게 확실히 느껴지긴했다(아주많이,,).</p>
<p>간단한 데이터를 담고 빼고 빠른속도가 필요할 땐 쓸모가 많을 거 같은 녀석인 것 같다.
(세션같은거?)</p>
<p>그래도 하드웨어가 첨가된 개발은 좀,, 손이 많이 간다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 JSON 값으로 웹서버로 데이터 전송]]></title>
            <link>https://velog.io/@js_hwang/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-JSON-%EA%B0%92%EC%9C%BC%EB%A1%9C-%EC%9B%B9%EC%84%9C%EB%B2%84%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EC%86%A1</link>
            <guid>https://velog.io/@js_hwang/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-JSON-%EA%B0%92%EC%9C%BC%EB%A1%9C-%EC%9B%B9%EC%84%9C%EB%B2%84%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EC%86%A1</guid>
            <pubDate>Wed, 26 Jun 2024 08:04:03 GMT</pubDate>
            <description><![CDATA[<p>음 간만에 글을 쓰게 됐는데,
이번엔 안드로이드 기기에서 웹서버(백엔드) 쪽으로 JSON 값으로 된 값을 전송할려고 한다.
내가 보낼 값들은  FCM 토큰 값, UUID 값 이렇게 두 가지를 보낼거다.</p>
<p><strong>근데 로컬로 켜놨기때문에 http 라는 것을 참고해야한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/23c84850-6551-4063-910f-5feed35ce03c/image.png" alt=""></p>
<p><strong>1. network_security_config.xml 파일 생성</strong></p>
<p>http 환경이니까 
res/xml 폴더에
network_security_config.xml 파일을 하나 만들자</p>
<pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;network-security-config&gt;
    &lt;domain-config cleartextTrafficPermitted=&quot;true&quot;&gt;
        &lt;domain includeSubdomains=&quot;true&quot;&gt;192.168.114.18&lt;/domain&gt;
    &lt;/domain-config&gt;
&lt;/network-security-config&gt;</code></pre><p>그리고 AndroidMainfest.xml에다가 </p>
<pre><code>&lt;application
        android:name=&quot;.App&quot;
        android:allowBackup=&quot;true&quot;
        android:dataExtractionRules=&quot;@xml/data_extraction_rules&quot;
        android:fullBackupContent=&quot;@xml/backup_rules&quot;
        android:networkSecurityConfig=&quot;@xml/network_security_config&quot;
        android:icon=&quot;@mipmap/ic_launcher&quot;
        android:label=&quot;@string/app_name&quot;
        android:roundIcon=&quot;@mipmap/ic_launcher_round&quot;
        android:supportsRtl=&quot;true&quot;
        android:theme=&quot;@style/Theme.FCMExample&quot;
        tools:targetApi=&quot;31&quot;&gt;</code></pre><p>android:networkSecurityConfig=&quot;@xml/network_security_config&quot;
해당 내용을 application 영역에 추가해주자</p>
<p><strong>2. Retrofit2</strong></p>
<p>나는 레트로핏2을 사용해서 전송하였다.
build.gradle(app) 에다가</p>
<p>라이브러리 추가하자</p>
<pre><code>    implementation &#39;com.squareup.retrofit2:retrofit:2.9.0&#39;
    implementation &#39;com.squareup.retrofit2:converter-gson:2.9.0&#39;
    implementation &#39;com.google.code.gson:gson:2.8.6&#39;</code></pre><p><strong>3. ApiService</strong></p>
<p>다음으로 apiservice 인터페이스를 생성해준다.</p>
<pre><code>public interface ApiService {

    @POST(&quot;/regToken&quot;)
    Call&lt;Void&gt; sendToken(@Body TokenRequest tokenRequest);
}
</code></pre><p><strong>4. RetrofitClient</strong></p>
<pre><code>public class RetrofitClient {
    private static Retrofit retrofit = null;

    public static Retrofit getClient(String baseUrl){
        if(retrofit == null) {
            retrofit = new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();
        }
        return retrofit;
    }</code></pre><p><strong>5. TokenRequest</strong></p>
<p>토큰값과 uuid를 담을 객체 생성</p>
<pre><code>public class TokenRequest {
    private String token;
    private String uuid;

    public TokenRequest(String token, String uuid) {
        this.token = token;
        this.uuid = uuid;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }
}
</code></pre><p><strong>6. MainActivity</strong></p>
<p>나는 테스트앱이므로 앱이 켜지자말자 (onCreate) 부분에서 토큰값과 uuid가 전송되도록 하였다.</p>
<pre><code>public class MainActivity extends AppCompatActivity {

    private static final String TAG = &quot;MainActivity&quot;;
    private static final String SERVER_URL = &quot;http://내 로컬 ip/regToken&quot;;
    private OkHttpClient client = new OkHttpClient();
    private Gson gson = new Gson();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirebaseMessaging.getInstance().getToken()
                .addOnCompleteListener(new OnCompleteListener&lt;String&gt;() {
                    @Override
                    public void onComplete(@NonNull Task&lt;String&gt; task) {
                        if (!task.isSuccessful()) {
                            Log.w(TAG, &quot;토큰 실패&quot;, task.getException());
                            return;
                        }

                        String token = task.getResult();
                        String uuid = App.getInstance().getUUID();

                        Log.d(&quot;토큰 값 : &quot;, token);
                        Log.d(&quot;uuid 값 : &quot;, uuid);

                        TokenRequest tokenRequest = new TokenRequest(token, uuid);
                        try {
                            sendTokenToServer(tokenRequest);
                        } catch (IOException e) {
                            e.printStackTrace();
                            Log.e(TAG, &quot;토큰 전송 실패&quot;, e);
                        }
                    }
                });
    }

    private void sendTokenToServer(TokenRequest tokenRequest) throws IOException {
        MediaType JSON = MediaType.parse(&quot;application/json; charset=utf-8&quot;);
        String json = gson.toJson(tokenRequest);

        RequestBody body = RequestBody.create(JSON, json);
        Request request = new Request.Builder()
                .url(SERVER_URL)
                .post(body)
                .build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.e(TAG, &quot;서버 요청 실패: &quot; + e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    Log.d(TAG, &quot;서버 요청 성공: &quot; + response.body().string());
                } else {
                    Log.e(TAG, &quot;서버 요청 실패: &quot; + response.message());
                }
            }
        });
    }
}
</code></pre><p>그리고 콘솔에 성공여부, 실패여부를 쓸 수 있도록 해놨으니
oncreate 되자말자 콘솔에 여부를 알 수 있다.
또한 응답값도 response.body().toString()으로 받을 수 있다!</p>
<p>간단한 api 호출 예제를 한번 테스트해보았다...</p>
<p>바쁜 요즘,, 간만에 글을 끄적였다..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flutter 에서 MYSQL에 INSERT (JPA)]]></title>
            <link>https://velog.io/@js_hwang/Flutter-%EC%97%90%EC%84%9C-MYSQL%EC%97%90-INSERT-JPA</link>
            <guid>https://velog.io/@js_hwang/Flutter-%EC%97%90%EC%84%9C-MYSQL%EC%97%90-INSERT-JPA</guid>
            <pubDate>Fri, 10 May 2024 04:32:10 GMT</pubDate>
            <description><![CDATA[<p>흠,, 요즘 Flutter 에 관심이 생겨서 만들어보려고 한다
그런데 앱을 구현하면서 FireBase로만 데이터를 저장해봤지,
MYSQL,ORACLE 등의 DB에는 저장을 못해봐서 REST API를 이용해서 한번 넣어보자.</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/7dc5bba1-0fb4-4ecb-b001-4562cef7a381/image.png" alt=""></p>
<h4 id="✔-fe-부분-먼저-작성해보자">✔ FE 부분 먼저 작성해보자</h4>
<h3 id="maindart">main.dart</h3>
<pre><code>import &#39;package:flutter/material.dart&#39;;
import &#39;package:flutter_application_1/screen/screen_home.dart&#39;;

void main() {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home : HomeScreen(),
    )
  );
}</code></pre><p>main.dart에 바로 작성 안하고 homescreen.dart에 작성하고 메인으로 불러온다.</p>
<h3 id="screen_homedart">screen_home.dart</h3>
<pre><code>class _HomeScreenState extends State&lt;HomeScreen&gt; {
  final _valueList = [&#39;KT&#39;, &#39;SKT&#39;, &#39;LG&#39;];
  var _selectedValue = &#39;KT&#39;;
  final TextEditingController _phoneNumberController = TextEditingController();
  final TextEditingController _nameController = TextEditingController();

  void dispose() {
    _phoneNumberController.dispose();
    _nameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              &#39;입력한 정보가 맞다면 \n아래 확인 버튼을 눌러주세요&#39;,
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.w600,
              ),
            ),
            SizedBox(
              height: 40,
            ),
            Text(
              &quot;휴대폰 번호&quot;,
              textAlign: TextAlign.start,
              style: TextStyle(
                color: Colors.grey,
              ),
            ),
            TextFormField(
              controller: _phoneNumberController,
               keyboardType: TextInputType.number,
              decoration: InputDecoration(
                focusedBorder: UnderlineInputBorder(
                  borderSide: BorderSide(color: Colors.blue),
                ),
                enabledBorder: UnderlineInputBorder(
                  borderSide: BorderSide(color: Color.fromARGB(15, 55, 55, 55)),
                )
              ),
              cursorColor: Colors.transparent,
            ),
            SizedBox(
              height: 20,
            ),
            Text(
              &quot;통신사&quot;,
              textAlign: TextAlign.start,
              style: TextStyle(
                color: Colors.grey,
              ),
            ),
            SizedBox(
              width: double.infinity, // DropdownButton을 텍스트 필드의 너비와 일치시킵니다.
              child: DropdownButton(
                isExpanded: true, // 버튼이 가능한 최대 너비를 가집니다.
                value: _selectedValue,
                items: _valueList.map(
                  (value) {
                    return DropdownMenuItem(
                      value: value,
                      child: Text(value),
                    );
                  },
                ).toList(),
                onChanged: (value) {
                  setState(() {
                    _selectedValue = value.toString();
                  });
                },
              ),
            ),
            const SizedBox(
              height: 20,
            ),
            Text(&#39;이름&#39;, 
            style: TextStyle(
              color: Colors.grey,
            ),),
            SizedBox(
                  child: TextFormField(
                    controller: _nameController,
               keyboardType: TextInputType.name,
              decoration: const InputDecoration(
                focusedBorder: UnderlineInputBorder(
                  borderSide: BorderSide(color: Colors.blue),
                ),
                enabledBorder: UnderlineInputBorder(
                  borderSide: BorderSide(color: Color.fromARGB(15, 55, 55, 55)),
                )
              ),
              cursorColor: Colors.transparent,
            ),
            ),
            SizedBox(height: (20),),
            ElevatedButton(onPressed: () {
              sendDataToServer(
                _phoneNumberController.text,
                _selectedValue,
                _nameController.text,
              );
            }, child: Text(&#39;확인&#39;,style: TextStyle(color: Colors.white),),
            style: ElevatedButton.styleFrom(
              backgroundColor: Color.fromARGB(255,41, 119, 243),
              minimumSize: Size(double.infinity,50),
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10)
              )

            ),
            )
          ],
        ),
      ),
    );
  }</code></pre><p>하면서 느낀거지만 html 에 익숙해져있던지라 화면을 구성하는데 어려움이 많이 느껴졌다..
아마 이렇게 짜는게 아닌데,, 잘못짜서 이렇게 길게 나온거 같은데</p>
<p>고수님이 혹시라도 이 글을 보시고 조언해주셨으면 좋겠다...</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/01ed10b9-40f1-46f1-bbb5-10618bb8c0dc/image.jpg" alt=""></p>
<p>어찌됐건 일단 완성,,,</p>
<p>일단 화면은 만들어졌고 해당 TextField 에 값을 입력하고
확인 버튼을 누르면 내가 만든 MYSQL 테이블에 입력값들이 저장이 되어야한다!</p>
<h3 id="다음-백엔드로-넘어가보자">다음 백엔드로 넘어가보자</h3>
<h3 id="membercontrollerjava">MemberController.java</h3>
<pre><code>@RestController
@RequestMapping(&quot;/&quot;)
public class MemberCotroller {
    private final MemberService memberService;

    public MemberCotroller(MemberService memberService) {
        this.memberService = memberService;
    }


    @PostMapping(&quot;/register&quot;)
    public ResponseEntity&lt;String&gt; register(@RequestBody Member member) {
        boolean exists = memberService.existsByName(member.getName());



        boolean result = memberService.createMember(member);
        if(result) {
            return ResponseEntity.ok(&quot;{\&quot;message\&quot;: \&quot;성공했습니다.\&quot;}&quot;);
        } else {
            if(exists) {
                return ResponseEntity.badRequest().body(&quot;{\&quot;error\&quot;: \&quot;duplicate_name\&quot;}&quot;);
            }
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(&quot;{\&quot;error\&quot;: \&quot;internal_error\&quot;}&quot;);
        }
    }


}</code></pre><p>우선 백엔드쪽도 새로운 걸 도전해보고 싶어서 JPA를 사용하기로 했다.
@RestController를 만들어주고</p>
<p>post 방식으로 보낼거기때문에 @PostMapping(&quot;/register)로 구현해준다.</p>
<p>성공하면 &quot;성공했습니다&quot;를 응답하고 실패하면 500에러가 응답할 것이고,
중복된 아이디가 있다면, duplicate_name을 응답하도록 했다.</p>
<p>자 그럼 다시 프론트 쪽에서 데이터 전송 로직을 만들어주자</p>
<pre><code>Future&lt;void&gt; sendDataToServer(String phoneNumber, String telecom, String name) async{
    final url = Uri.parse(&#39;http://내 아이피:8080/register&#39;);


    final response = await http.post(
      url,
      headers: &lt;String,String&gt;{
        &#39;Content-Type&#39;: &#39;application/json; charset=UTF-8&#39;,
      },
      body: jsonEncode(&lt;String, String&gt;{
        &#39;phoneNumber&#39; : phoneNumber,
        &#39;telecom&#39; : telecom,
        &#39;name&#39; : name,
      }),
    );

    if(response.statusCode == 200) {
      print(&#39;데이터 전송 성공!&#39;);
      showDialog(context: context, 
                  builder: (BuildContext context){
                    return AlertDialog(
                      title: Text(&quot;&quot;),
                      content: Text(&quot;성공적으로 가입되었습니다&quot;),
                      actions: &lt;Widget&gt;[
                        FloatingActionButton(onPressed: (){
                          Navigator.of(context).pop();
                        },
                        child: Text(&quot;확인&quot;),)
                      ],
                    );
                  }
                  );
    }else {
      print(&#39;실패했습니다. ${response.statusCode}&#39; );
      print(response.body);
      final responseData = json.decode(response.body);
      if(responseData[&#39;error&#39;] == &#39;duplicate_name&#39;) {
        showDialog(context: context, 
        builder: (BuildContext context){
          return AlertDialog(
            title: Text(&quot;실패&quot;),
            content: Text(&#39;이미 존재하는 아이디입니다,&#39;),
            actions: &lt;Widget&gt;[
              ElevatedButton(onPressed: (){
                Navigator.of(context).pop();
              },
              child: Text(&quot;확인&quot;),)
            ],
          );
        });
      }</code></pre><p>성공하면 &quot;성공적으로 가입되었습니다&quot; 다이얼로그를 띄워주고,
중복 된 이름을 입력하면 &quot;이미 존재하는 이름입니다&quot; 라고 띄워준다.
(그냥 중복체크 다이얼로그를 띄워보고 싶어 그냥 이름에다가 unique key 값을 적용했다)
<img src="https://velog.velcdn.com/images/js_hwang/post/eae44b1d-b6d0-49a3-aba1-b4509cdfee72/image.png" alt=""></p>
<h3 id="잘들어온다">잘들어온다..!!!</h3>
<p>파이어베이스에다가 앞단에서 바로 넣는게 아닌
API로 백엔드 쪽으로 데이터를 보내서 저장하는건 처음해보았는데,,</p>
<p>개린이다보니 프론트 백엔드 구분이 잘안되는 상태였는데,
아직 확실히 이해 못했지만 조금은 어떤 느낌으로 구분하는지 알게 된 것 같다.</p>
<p>이런식으로 프론트, 백엔드 둘다 할 줄 알면 풀스택 개발자가 되는건가?..</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[RESTful API 에 대해서]]></title>
            <link>https://velog.io/@js_hwang/RESTful-API-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</link>
            <guid>https://velog.io/@js_hwang/RESTful-API-%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C</guid>
            <pubDate>Wed, 24 Apr 2024 08:07:35 GMT</pubDate>
            <description><![CDATA[<p>썸네일 두 번째 사람이 요즘 내 기분이자 표정을 보여주는 것 같다..</p>
<p>어쨋든!
최근에 HTTP , URI 기반으로 자원에 대해 접근 할 일이 많아졌다.
그래서 이걸 사용하면서도 확실히 알고 넘어가는게 중요할 것 같아서 정리를 간단한게 해볼려한다.</p>
<h3 id="restful-api-라는게-도대체-뭘까">RESTful API 라는게 도대체 뭘까?</h3>
<blockquote>
<p><span style="font-size:24px">[ Representational State Transfer ]</span>
먼저 <span style="color: orange; font-size: 30px">REST</span> 란 
HTTP URI를 통해 자원을 명시하여
HTTP 메서드를 통해서
해당 자원을 메서드 로직에 있는 CRUD 작업을 적용하는 의미이다.
<span style="color: red">자원을 이름으로 구분하여! 자원의 상태를 주고 받는 모든 것!</span></p>
</blockquote>
<p>구성요소는 <em><strong>[HTTP URI],  [HTTP Method], [HTTP Message Pay LOAD]</strong></em> 가 있다.</p>
<p>음,, 간단하게 생각하면 애플리케이션 개발 인터페이스인데
HTTP 메서드와 URI만으로 데이터들을 CRUD 하는거라고 생각하면 될 것 같다</p>
<p>더 간단하게 말하자면,,
두 컴퓨터 시스템이 인터넷을 통해서 정보를 안전하게 교환하기 위해 사용하는 인터페이스라고 
하면 될 것 같다.</p>
<ul>
<li>장점</li>
</ul>
<ol>
<li>HTTP 프로토콜에 사용하는 플랫폼이라면 모두 사용 가능하다.</li>
<li>HTTP 프로토콜의 표준을 최대한 활용해서 여러가지 장점을 함께 가져갈 수 있다.</li>
<li>서버와 클라이언트의 역할을 명확하게 분리해준다.</li>
</ol>
<ul>
<li>단점</li>
</ul>
<ol>
<li>표준이 없다.</li>
<li>메서드 형태가 제한되어있다. 4가지!</li>
<li>구형브라우저는 제대로 지원이 안될 수가 있다.<pre><code></code></pre><pre><code></code></pre><h3 id="특징">특징</h3>
</li>
</ol>
<ul>
<li>클라이언트와 서버로 분리되어야 하며 서로 의존성이 없어야한다</li>
<li>상태 정보는 따로 저장하지 않고 접근하는자가 누구인지 관계없이 동일한 결과를 응답해야한다.</li>
<li>API 는 REST 조건을 만족하려면 오픈 될 수 밖에 없기에, 요청 정보를 검색하는데 있어 계층 구조로 분리가 되어 있어야한다.</li>
<li>API를 통해 전송되는 내용은 직관성이 좋아야한다. (JSON이 많이 사용된다고 한다)
<img src="https://velog.velcdn.com/images/js_hwang/post/ef57dfdd-dc15-4ee3-a719-562b754059cb/image.png" alt="">
(딱봐도 알겠지 뭔지? 👍)<pre><code></code></pre><pre><code></code></pre><h3 id="사용-이유">사용 이유?</h3>
여러가지 이유가 있겠지만, Client side를 고정(정형화)해두고 사용하는 것이 아닌 
PC,모바일,어플리케이션 등 플랫폼 자체에 제약을 두지 않고 사용하기 위해서 인 것 같다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tomcat 서브도메인]]></title>
            <link>https://velog.io/@js_hwang/Tomcat-%EC%84%9C%EB%B8%8C%EB%8F%84%EB%A9%94%EC%9D%B8</link>
            <guid>https://velog.io/@js_hwang/Tomcat-%EC%84%9C%EB%B8%8C%EB%8F%84%EB%A9%94%EC%9D%B8</guid>
            <pubDate>Mon, 22 Apr 2024 03:15:24 GMT</pubDate>
            <description><![CDATA[<h3 id="✨-서브도메인을-추가해보자">✨ 서브도메인을 추가해보자</h3>
<p>한 서버로 두개의 도메인을 돌려야하는 일이 생겼다.
안해본 작업이라 생각을 계속 해보고 자료를 좀 찾아보니,
톰캣으로 서버를 돌리기 때문에 
sever.xml 파일에 호스트(HOST) 요소를 추가하면 되는 방법이 있었다.</p>
<p>우선 웹서버에서 원래 돌리던 HOST가 있었는데 이것을 Main Host 라고 한다.
나는 또 DB만 스키마를 따로 파서 연결해서 사용할 예정이라, 
Virtual Host(가상호스트)만 추가해주었다.</p>
<h4 id="서브도메인의-목적이-뭘까">서브도메인의 목적이 뭘까?</h4>
<p>음,, 생각을 해보았다. 내가 하는 목적과 다르게 또 쓰이는 곳이 어디가 있을까?
모바일? 음 m. 을 붙여서 서브도메인을 구성하는 모바일 전용페이지에도 쓰인다는 사실을 알게 되었다. 그 뿐만 아니라 관리자페이지라던지 API 를 사용할때? 여러가지를 생각했을 때,
URI를 설계하기에 좋고 관리 및 유지보수에 강점들이 많아서 쓰인다고 생각이 들었다.</p>
<pre><code>&lt;Host name=&quot;기존 메인도메인.com&quot; appBase=&quot;webapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;false&quot;&gt;
&lt;Valve className=&quot;org.apache.catalina.valves.AccessLogValve&quot; directory=&quot;logs&quot; prefix=&quot;localhost_access_log&quot; suffix=&quot;.txt&quot; pattern=&quot;%h %l %u %t &quot;%r&quot; %s %b&quot;/&gt;
&lt;Context path=&quot;/&quot; docBase=&quot;/서버에프로젝트경로&quot; reloadable=&quot;true&quot;/&gt;
&lt;Context path=&quot;/files&quot; docBase=&quot;/서버에프로젝트경로&quot; reloadable=&quot;true&quot;/&gt;
&lt;Context path=&quot;/upload&quot; docBase=&quot;/서버에프로젝트경로&quot; reloadable=&quot;true&quot;/&gt;

&lt;/Host&gt;
&lt;Host name=&quot;추가한 서브 도메인.com&quot; appBase=&quot;webapps&quot; unpackWARs=&quot;true&quot; autoDeploy=&quot;false&quot;&gt;
&lt;Valve className=&quot;org.apache.catalina.valves.AccessLogValve&quot; directory=&quot;logs&quot; prefix=&quot;localhost_access_log&quot; suffix=&quot;.txt&quot; pattern=&quot;%h %l %u %t &quot;%r&quot; %s %b&quot;/&gt;
&lt;Context path=&quot;/&quot; docBase=&quot;/서버에프로젝트경로&quot; reloadable=&quot;true&quot;/&gt;
&lt;Context path=&quot;/files&quot; docBase=&quot;/서버에프로젝트경로&quot; reloadable=&quot;true&quot;/&gt;
&lt;Context path=&quot;/upload&quot; docBase=&quot;/서버에프로젝트경로&quot; reloadable=&quot;true&quot;/&gt;</code></pre><p>이렇게해서 원격서버에 server.xml 파일 바꿔주고
새로운 프로젝트 파일을 경로에 위치에 해주고,
properites에는 같은 디비에 다른 스키마로 연결하여
재배포하니깐 서브 도메인으로 접속이 가능했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Eclipse Runnable JAR 파일 추출하기]]></title>
            <link>https://velog.io/@js_hwang/Runnable-JAR-%ED%8C%8C%EC%9D%BC-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@js_hwang/Runnable-JAR-%ED%8C%8C%EC%9D%BC-%EC%B6%94%EC%B6%9C%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Apr 2024 02:06:32 GMT</pubDate>
            <description><![CDATA[<h3 id="span-stylecolor-orange-쓰는-이유랄까span"><span style="color: Orange" >쓰는 이유랄까?..</span></h3>
<p>배치 프로그램이나 스케줄러 등등 따로 돌려줘야할 때가 있는 것 같다.
이유는 뭐,, 본 프로젝트에서 주기적인 대량의 데이터작업이 필요할때 라던지?..
여튼 그럴 때 쓰려고 만들어 놓은 프로젝트를 JAR 파일로 뽑아보자</p>
<p>우선, 프로젝트를 짜고</p>
<p>프로젝트 우클릭 -&gt; Run As -&gt; Run Configuration</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/dbefd3e1-3b24-4425-8c3d-6772bd9f8218/image.png" alt=""></p>
<p>다음으로는 왼쪽 리스트 중에 JavaApplication을 우클릭해서 New Configuration을 해주자</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/2705bfac-6595-4a3d-b0c0-e0dea4796282/image.png" alt=""></p>
<p>이제부터 좀 중요하다 <span style="color: Orange" >집중</span></p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/ec86397e-e2ea-467b-bc67-1b05f7049fa7/image.png" alt=""></p>
<p>이런식으로 창이 나올텐데, 
상단에 있는 Name에는 원하는 이름을 정해주고 Project를 선택하고,
<span style="color: red" >Main class</span>에 search를 클릭해서 선택해주자
<img src="https://velog.velcdn.com/images/js_hwang/post/e0bd208e-371e-4070-b000-71bebd460a14/image.png" alt="">
이런식으로 나올거다 (참고로 나는 이걸 선택안하고 넘어가서 찾는데 엄청 오래 걸렸다..)
자 그럼 이제 우측 하단에 Apply !</p>
<p>이제 그럼 추출해보자
<img src="https://velog.velcdn.com/images/js_hwang/post/c69b05a0-e2c0-4238-9446-804e10dc4a19/image.png" alt=""></p>
<p>내 프로젝트 우클릭 그리고 export 클릭!
<img src="https://velog.velcdn.com/images/js_hwang/post/448c429c-04c6-4a4a-8b39-029278c2953e/image.png" alt="">
Runnable JAR File 을 선택하고 Next 를 누르자.
이제 다왔다..</p>
<p><img src="https://velog.velcdn.com/images/js_hwang/post/671d07d5-0737-4d78-bd02-3529f70f7984/image.png" alt=""></p>
<p>아까 configuration 했던 거 선택해주고 저장위치하고 finish 누르면 
해당 경로에 jar 파일이 저장된다. 👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HttpURLConnection 을 이용한 데이터 전송(POST)]]></title>
            <link>https://velog.io/@js_hwang/HttpURLConnection-%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EC%86%A1POST</link>
            <guid>https://velog.io/@js_hwang/HttpURLConnection-%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EC%86%A1POST</guid>
            <pubDate>Fri, 19 Apr 2024 01:46:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>개발자로 취업한 뒤 이제 수습이 끝났다.
첫 글을 써보려고 한다.
내가 사용했던 코드들을 정리하면 다음에 다시 보면서 공부도 하고, 
다시 쓸 때에도 도움이 될 것 같다.
(틀린 점이 있을 수도 있지만,, 알게될 때마다 고쳐야지..)</p>
</blockquote>
<p>🚩 고객사 서버쪽으로 데이터를 보내야할 일이 생겼다.</p>
<p>처음에 GET 방식으로 요구를 했고,
보내야할 Key 값들과 데이터 포맷들을 전달 받았다.</p>
<p>그리고 구현을 했지만 변경이 되어야 할 것 같다고
POST 방식으로 바꾸게 되었다 FormData 형식으로 달라고하네,,</p>
<p>단말기에 태깅을 하였을 때 카드에 있는 태깅 정보를 고객사 서버로 넘겨줘야하는 로직인데
근데 컨트롤러에서 거쳐야할 로직들이 많아서 코드를 쳐서 구현을하니
속도 때문인지 이유는 정확하게 모르지만 
태깅이 많이 들어오게 되면 중간에 멈추는 현상이 일어났다.</p>
<p>그래서 고민을 하던중 내가 생각한건,,
runnable jar 파일을 따로 돌려주는 프로젝트를 새로 생성하였다.</p>
<pre><code>try {
        service.insertTagdataTransfer(paramMap2);
            boolean isLinux = System.getProperty(&quot;os.name&quot;).toLowerCase().startsWith(&quot;linux&quot;);
            Process process;
            if (isLinux) 
            {
                try {
                System.out.println(&quot;일로 들어왔음&quot;);
                String command = String.format(&quot;java -jar /home/gnbismngr/apps/datasender/DataSender2.jar \&quot;%s\&quot; \&quot;%s\&quot; \&quot;%s\&quot; \&quot;%s\&quot;&quot;, paramMap2.get(&quot;cardLoc&quot;).toString(), paramMap2.get(&quot;originDestination&quot;).toString(), paramMap2.get(&quot;cardUid&quot;).toString(), paramMap2.get(&quot;tagTime&quot;).toString());
                System.out.println(&quot;실행 명령: &quot; + command); // 실행 명령이 올바르게 구성되었는지 확인
                process = Runtime.getRuntime().exec(command);

                BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(&quot;Runnable 파일 콘솔값 : &quot;+line);
                }

                // 프로세스의 에러 출력도 캡처하여 콘솔에 출력
                BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                String errorLine;
                while ((errorLine = errorReader.readLine()) != null) {
                    System.out.println(&quot;Error: &quot; + errorLine);
                }

                // 프로세스가 종료될 때까지 대기
                int exitCode = process.waitFor();
                System.out.println(&quot;프로세스 종료 코드: &quot; + exitCode);

                }catch (IOException e) {
                    e.printStackTrace();
                }
            } 
    } catch (Exception e) {
        e.printStackTrace();
    }</code></pre><p>우선 DB에 태깅정보를 저장하고,</p>
<p>리눅스 서버에 jar파일을 올려놓고 태깅 될 때 마다 해당 API를 타고 와서
실행시키는 방법으로 구현을 해보았다.</p>
<p>그리고 jar 파일이 잘작동하는지 콘솔에 찍어보고 싶어서
버퍼를 이용해서 콘솔에 결과값이 나오도록 해놨다.</p>
<p>이제 해당 runnable jar파일을 뽑아낸 프로젝트에 코드이다.</p>
<pre><code> Map&lt;String, String&gt; formData = new HashMap&lt;&gt;();

 String cardLoc = args[0];
 String originDestination = args[1];
 String cardUid = args[2];
 String tagTime = args[3];</code></pre><p>넘겨준 인자를 받아서 변수에 담아주는 작업을 한다.</p>
<pre><code>formData.put(&quot;area&quot;, sPass_gb);
formData.put(&quot;course&quot;, sCos);
formData.put(&quot;keyId&quot;, sKey_id);
formData.put(&quot;time&quot;, sTime);</code></pre><p>formData에 담아주고,</p>
<pre><code> String url = &quot;http://apiURL을 넣는다&quot;;

 // URL객체 생성
 URL apiUrl = new URL(url);
 //URL 객체사용해서 HTTP 연결 오픈, 나중에 HTTP 요청보내는데 사용함
 HttpURLConnection connection = (HttpURLConnection) apiUrl.openConnection();
 connection.setRequestMethod(&quot;POST&quot;);    
 connection.setRequestProperty(&quot;X-Authorization&quot;, &quot;인증 토큰 값&quot;);
 //HTTP 요청에서 데이터를 전송할 수 있도록한다.
 connection.setDoOutput(true);

 // 연결을 통해 데이터를 전송하는데 사용되는 출력 스트림을 연다.
 try (OutputStream os = connection.getOutputStream()) {
                //Post 요청에 포함된 데이터를 구성하는 StringBuilder 객체를 생성한다.
                StringBuilder postData = new StringBuilder();
                //formData 맵의 각 항목을 반복한다.
                for (Map.Entry&lt;String, String&gt; entry : formData.entrySet()) {
                    System.out.println(&quot;formData : &quot; + formData);
                    // 이미 postData에 데이터가 추가된 경우, 항목들을 &#39;&amp;&#39;구분한다.
                    if (postData.length() != 0) {
                        postData.append(&#39;&amp;&#39;);
                    }
                    // 키와 값을 URL 인코딩 하고, &#39;=&#39;로 연결하여 postData에 추가한다.
                    postData.append(URLEncoder.encode(entry.getKey(), &quot;UTF-8&quot;));
                    postData.append(&#39;=&#39;);
                    postData.append(URLEncoder.encode(entry.getValue(), &quot;UTF-8&quot;));
                    System.out.println(&quot;postData : &quot;+ postData);
                }
                //구성된 데이터들을 바이트 배열로 반환한다.
                byte[] postDataBytes = postData.toString().getBytes(StandardCharsets.UTF_8);
                System.out.println(&quot;postDataBytes : &quot; + postDataBytes);
                // 바이트 배열을 출력 스트림에 쓴다. 이는 서버로데이터를 전송하는 과정
                os.write(postDataBytes);
            }
//서버로 부터 받은 HTTP응답 코드를 가져온다.
int responseCode = connection.getResponseCode();
//응답 코드가 HTTP_OK(200) 인 경우 , 요청이 성공적으로 처리되었음을 의미한다.
if (responseCode == HttpURLConnection.HTTP_OK) {
                    updateDatabase(1,cardUid,tagTime);
                    // 응답 본문 읽기 위해 BufferedReader 를 생성한다.
                    try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
                        StringBuilder response = new StringBuilder();
                        String responseLine;
                        // 응답 본문의 각 라인을 읽고, 전체 응답을 하나의 문자열로 구성한다.
                        while ((responseLine = br.readLine()) != null) {
                            response.append(responseLine.trim());
                        }
                        // 응답 내용 콘솔에 출력
                        System.out.println(&quot;HTTP POST 요청 결과: &quot; + response.toString());
                        // 응답을 파일에 기록
                        try (FileWriter fileWriter = new FileWriter(&quot;output123.txt&quot;)) {
                            fileWriter.write(response.toString());
                        } catch(IOException e ) {
                            e.printStackTrace();
                        }
                    }
            } else {
                    System.out.println(&quot;HTTP POST 요청 실패. 응답 코드: &quot; + responseCode);
                    updateDatabase(0,cardUid,tagTime);
            }
            connection.disconnect();</code></pre><p>updateDataBase는 아까 태깅정보를 인서트 했었는데
그 테이블에 태깅정보가 잘넘어갔는지 안넘어갔는지 플래그 처리를 해주기 위해 하는 작업이다.</p>
<blockquote>
<p>정리</p>
</blockquote>
<p>단말기에 태깅했을 때 컨트롤러에 태깅 API로 들어오게 되고 해당 API에서 특정 경로에 있는 jar파일을 실행시키도록 하였고,
그 jar파일에서는 HttpURLConnection을 통해서 필요한 데이터들을 넘겨주고 있다.</p>
<blockquote>
<p>부족한 점</p>
</blockquote>
<p>jar 파일을 실행시킬 때 console에 print 라도 찍어서 보고 싶었는데,
볼 수 있는 방법을 몰라 시간이 좀 걸렸는데 버퍼에 담아 출력스트림으로 jar파일을 실행시키는
본 프로젝트에서 로그내용들을 볼 수 있게 하였다.
부족한 점들이 너무너무너무 많치만 차근차근 한단계씩 밟아가봐야할듯.</p>
]]></description>
        </item>
    </channel>
</rss>