<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>A Better Tomorrow</title>
    <link>https://cases.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 19 Jun 2026 22:53:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Sponge_</managingEditor>
    <image>
      <title>A Better Tomorrow</title>
      <url>https://tistory1.daumcdn.net/tistory/4743013/attach/8c0164eda58249b082a9c5b7440c2c06</url>
      <link>https://cases.tistory.com</link>
    </image>
    <item>
      <title>요즘 난리 난 Clawdbot? AI 비서 끝판왕의 등장</title>
      <link>https://cases.tistory.com/entry/%EC%9A%94%EC%A6%98-%EB%82%9C%EB%A6%AC-%EB%82%9C-Clawdbot-AI-%EB%B9%84%EC%84%9C-%EB%81%9D%ED%8C%90%EC%99%95%EC%9D%98-%EB%93%B1%EC%9E%A5</link>
      <description>&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;요즘 AI 관련 커뮤니티에서 정말 핫한 Clawdbot(최근 Moltbot으로 이름이 바뀌었죠)에 대해 들어보셨나요?&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;묻는 말에 답만 해주는 챗봇 시대는 이제 끝난 것 같아요.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;이제는 내 컴퓨터를 직접 제어하고, 대신 일을 해주는 '에이전트 AI'의 시대가 왔거든요.&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;오늘은 왜 다들 클로드봇에 열광하는지, 그리고 우리 같은 개발자나 기획자들에게 왜 필요한지 핵심만 쏙쏙 뽑아 정리해 드릴게요!&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;1. Claude봇(Clawdbot), 대체 뭐가 다른데?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;기존의 챗봇들이 &quot;이 코드 짜줘&quot; 하면 코드만 줬다면, 클로드봇은 &quot;이 코드 짜서 내 컴퓨터에 파일로 저장하고 실행까지 해줘&quot;가 가능한 녀석이에요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,0,0&quot;&gt;로컬 환경 제어:&lt;/b&gt; 내 맥이나 윈도우 터미널에 직접 접속해서 명령어를 실행해요.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,1,0&quot;&gt;미친 기억력 (Persistent Memory):&lt;/b&gt; 어제 했던 이야기, 내 취향, 업무 스타일을 다 기억합니다. 매번 설명할 필요가 없어서 진짜 '비서' 같아요.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;8,2,0&quot;&gt;강력한 자동화:&lt;/b&gt; 이메일, 캘린더, 슬랙까지 연결해서 루틴한 업무를 대신 처리해 줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;2. 왜 지금 '클로드' 기반 봇이 뜰까?&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;사용자님도 아시다시피, 최근 &lt;b data-index-in-node=&quot;16&quot; data-path-to-node=&quot;10&quot;&gt;Claude 3.5 Sonnet&lt;/b&gt;이나 &lt;b data-index-in-node=&quot;36&quot; data-path-to-node=&quot;10&quot;&gt;4.5&lt;/b&gt; 모델의 코딩 능력이 압도적이잖아요? (저도 프론트 디자인이나 복잡한 로직 짤 때 도움을 많이 받죠 ㅎㅎ). 이 강력한 '두뇌'에 '손발(도구 실행 능력)'을 달아준 게 바로 클로드봇이라고 보시면 됩니다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;특히 Anthropic의 MCP(Model Context Protocol) 덕분에 외부 앱이랑 연동되는 속도가 장난이 아니에요.&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;3. 실전 활용 포인트 (이런 분들께 강추!)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,0,0&quot;&gt;코딩 테스트 준비족:&lt;/b&gt; &quot;이 문제 파이썬으로 풀고 효율성 체크해서 오답 노트 파일로 만들어줘&quot; 하면 끝!&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,1,0&quot;&gt;업무 자동화가 필요한 분:&lt;/b&gt; 특정 키워드가 들어간 댓글 알림을 받거나, 매주 MS 업데이트 소식 취합해서 보고서 만드는 일? 이제 봇한테 맡기면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b data-index-in-node=&quot;0&quot; data-path-to-node=&quot;12,2,0&quot;&gt;AI 교육 관계자:&lt;/b&gt; 학생들에게 AI 원리를 설명할 때, 이론만 말하는 게 아니라 &quot;AI가 직접 파일을 만들고 수정하는 과정&quot;을 라이브로 보여주기 딱 좋아요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;4. 주의할 점 (보안은 필수!)&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;내 컴퓨터 권한을 주는 거라 보안이 제일 중요해요! 클로드봇은 데이터를 로컬에 저장하려고 노력하지만, 항상 &lt;b data-index-in-node=&quot;60&quot; data-path-to-node=&quot;14&quot;&gt;실행 권한(Action Permission)&lt;/b&gt; 설정을 꼼꼼히 확인해야 한다는 점, 잊지 마세요!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;채팅을 넘어 실행하는 AI로 넘어가는 과도기에 우리가 서 있는 것 같아요.&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;저도 요즘 이 봇을 활용해서 인스타 댓글 알림 자동화나 업무 시트 정리를 더 편하게 할 방법을 고민 중이랍니다.&lt;/p&gt;</description>
      <category>AI</category>
      <category>clawdbot</category>
      <category>클로드봇</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/90</guid>
      <comments>https://cases.tistory.com/entry/%EC%9A%94%EC%A6%98-%EB%82%9C%EB%A6%AC-%EB%82%9C-Clawdbot-AI-%EB%B9%84%EC%84%9C-%EB%81%9D%ED%8C%90%EC%99%95%EC%9D%98-%EB%93%B1%EC%9E%A5#entry90comment</comments>
      <pubDate>Sat, 31 Jan 2026 23:39:47 +0900</pubDate>
    </item>
    <item>
      <title>React Native Expo SDK 버전 관리</title>
      <link>https://cases.tistory.com/entry/React-Native-Expo-SDK-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 진행 중인 최종 프로젝트에 Frontend를 React-Native를 사용하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 우리 팀의 주제가 웹 애플리케이션 보다는 모바일 앱이 더 적합하다고 판단하여 안드로이드, ios를 동시 개발이 가능한 React-Native를 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 React-Native는 한 번도 사용해본 적이 없는 언어였기에, 마냥 쉽지만은 않았다. 그래도 요즘 워낙 성능 좋은 AI 코딩 툴들이 많아서, 그들의 도움을 받아가며 열심히 진행을 하는 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 최근에 발생했던 Expo 버전 관리 이슈에 대한 내용을 다뤄보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Expo로 개발을 할때, 웹뷰, ios 시뮬레이터, 안드로이드 시뮬레이터 등 여러가지 Viewer로 자신의 프로젝트를 실행해볼 수가 있는데, 실제 자신의 모바일 폰으로도 해당 앱을 실행시켜 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2N79q/btsQwSEMc6E/KaGkSacMQX2Vona8z5OGCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2N79q/btsQwSEMc6E/KaGkSacMQX2Vona8z5OGCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2N79q/btsQwSEMc6E/KaGkSacMQX2Vona8z5OGCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2N79q%2FbtsQwSEMc6E%2FKaGkSacMQX2Vona8z5OGCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;751&quot; height=&quot;391&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx expo start 라는 명령어로 실행시키면 QR 코드가 하나 나오는 것을 볼 수 있는데, 해당 QR을 카메라로 찍으면 Expo Go 앱을 통해 현재 나의 프로젝트 앱을 사용할 수 있다. 물론, Expo Go 앱이 자신의 폰에 설치가 되어 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난 대부분 그냥 개발을 하며, ios 시뮬레이터를 통해 QA를 진행하지만, 그래도 가끔 실제 모바일 환경에서 잘 작동을 하는지, 또는 두 기기간의 상호작용 등이 필요할때는 모바일로 확인이 가능해야 하기 때문에 종종 사용을 하곤 했다. (최종적으론 모바일에서 사용하기 위해서 개발을 하는 것이기 때문에..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 바로 어제 오랜만에 또 QR을 통해 모바일에서 확인을 해보려던 찰나, Expo Go 앱이 업데이트가 되었는지, 현재 내가 프로젝트에 사용중인 Expo SDK 버전과 Expo Go 앱의 SDK 버전이 맞지 않다는 에러가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 난 expo 버전을 업데이트 시켜주기만 하면 된다고 생각하여, 그렇게 업데이트를 해주었더니, 이제 온갖 오류가 발생하기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고보니 expo 버전을 업데이트 시켜주었으면 현재 프론트에서 사용 중인 다른 모듈들의 버전도 현재 업데이트 된 expo 버전과 호환되는 버전으로 맞춰주어야 되는 것이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react native로 버전 관리를 할때 보아야 할 파일은 package.json, package-lock.json 이렇게 두 개를 볼 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1757774007587&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;dependencies&quot;: {
  &quot;react&quot;: &quot;^18.2.0&quot;,
  &quot;react-native&quot;: &quot;0.74.3&quot;,
  &quot;expo&quot;: &quot;~51.0.8&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;package.json에는 이런 식으로 대략적인 버전이 적혀있을 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;466&quot; data-start=&quot;306&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;370&quot; data-start=&quot;306&quot;&gt;&quot;^18.2.0&quot; &amp;rarr; &lt;b&gt;18.x 버전대에서 최신 버전&lt;/b&gt;을 허용 (예: 18.3.1도 설치될 수 있음)&lt;/li&gt;
&lt;li data-end=&quot;430&quot; data-start=&quot;371&quot;&gt;&quot;~51.0.8&quot; &amp;rarr; &lt;b&gt;패치 버전만 변경 허용&lt;/b&gt; (51.0.9는 가능, 51.1.0은 불가)&lt;/li&gt;
&lt;li data-end=&quot;466&quot; data-start=&quot;431&quot;&gt;&quot;0.74.3&quot; &amp;rarr; &lt;b&gt;정확히 0.74.3만 설치&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 딱 정확한 버전을 지정해서 설치하는 것이 아니기 때문에 다른 팀원과 설치 시점 등이 달라지면 동일한 버전이 설치되지 않아서 충돌이 발생할 수 있다. 이것을 방지하기 위해서 존재하는 것이 package-lock.json이다.&lt;/p&gt;
&lt;pre id=&quot;code_1757774171370&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;node_modules/react&quot;: {
  &quot;version&quot;: &quot;18.2.0&quot;,
  &quot;resolved&quot;: &quot;https://registry.npmjs.org/react/-/react-18.2.0.tgz&quot;,
  &quot;integrity&quot;: &quot;sha512-somehash&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;801&quot; data-start=&quot;768&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 정확한 버전이 작성되어 있는것이 package-lock.json이기 때문에 팀원이 npm install을 했을 때,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;914&quot; data-start=&quot;802&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;866&quot; data-start=&quot;802&quot;&gt;package.json만 보고 설치하면 &amp;rarr; 최신 버전 범위 안에서 각자 조금씩 다른 버전이 깔릴 수 있다&lt;/li&gt;
&lt;li data-end=&quot;914&quot; data-start=&quot;867&quot;&gt;package-lock.json도 같이 있으면 &amp;rarr; 정확히 같은 버전이 깔린다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현재 내가 중요한 것은 54 버전으로 업데이트를 진행한 expo에 맞춰서 사용중인 다른 모듈들의 버전도 모두 맞춰주어야 하는데, 그것을 어떻게 하나하나 다 알 수 있냐는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 유용한 명령어가 바로 npx expo-doctor 이다.&lt;/p&gt;
&lt;pre id=&quot;code_1757774550473&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(final-project)% npx expo-doctor       
env: load .env
env: export GOOGLE_MAPS_API_KEY API_URL
15/17 checks passed. 2 checks failed. Possible issues detected:
Use the --verbose flag to see more details about passed checks.

✖ Check that no duplicate dependencies are installed
Your project contains duplicate native module dependencies, which should be de-duplicated.
Native builds may only contain one version of any given native module, and having multiple versions of a single Native module installed may lead to unexpected build errors.
Found duplicates for expo-constants:
  ├─ expo-constants@17.1.7 (at: node_modules/expo-constants)
  ├─ expo-constants@18.0.8 (at: node_modules/expo/node_modules/expo-constants)
  └─ expo-constants@18.0.8 (at: node_modules/expo-asset/node_modules/expo-constants)
Advice:
Resolve your dependency issues and deduplicate your dependencies. Learn more: https://expo.fyi/resolving-dependency-issues

✖ Check that packages match versions required by installed Expo SDK

❗ Major version mismatches
package                         expected  found    
expo-router                     ~6.0.1    5.1.6    
expo-status-bar                 ~3.0.8    2.2.3    
react-native-reanimated         ~4.1.0    3.19.1   
expo-constants                  ~18.0.8   17.1.7   
expo-linking                    ~8.0.8    7.1.7    

⚠️ Minor version mismatches
package                         expected  found    
react-dom                       19.1.0    19.0.0   
react-native-gesture-handler    ~2.28.0   2.24.0   
react-native-safe-area-context  ~5.6.0    5.4.0    
react-native-screens            ~4.16.0   4.11.1   
react-native-web                ^0.21.0   0.20.0   
react-native-webview            13.15.0   13.16.0  
typescript                      ~5.9.2    5.8.3    

  Patch version mismatches
package                         expected  found    
react-native                    0.81.4    0.81.0   

Changelogs:
- expo-router &amp;rarr; https://github.com/expo/expo/blob/sdk-54/packages/expo-router/CHANGELOG.md
- expo-status-bar &amp;rarr; https://github.com/expo/expo/blob/sdk-54/packages/expo-status-bar/CHANGELOG.md
- expo-constants &amp;rarr; https://github.com/expo/expo/blob/sdk-54/packages/expo-constants/CHANGELOG.md
- expo-linking &amp;rarr; https://github.com/expo/expo/blob/sdk-54/packages/expo-linking/CHANGELOG.md

13 packages out of date.
Advice:
Use 'npx expo install --check' to review and upgrade your dependencies.
To ignore specific packages, add them to &quot;expo.install.exclude&quot; in package.json. Learn more: https://expo.fyi/dependency-validation

2 checks failed, indicating possible issues with the project.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어를 입력하면 서로 호환되지 않는 패키지 목록이 쭉 나오는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npx expo install --check 명령어를 사용하면 위의 미스매치된 패키지들을 현재 54SDK 버전에 맞게 자동으로 설치를 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;되도록이면&lt;b&gt; rm -rf node_modules package-lock.json&amp;nbsp;&lt;/b&gt;로 삭제를 하고 다시 npm install을 하는 것이 추후에 꼬일 위험을 방지할 수 있어서 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 과정을 거치고, npx expo start -c 로 캐시 삭제 후 실행을 해주면 아주 말끔히 정리된 것을 확인할 수 있다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/89</guid>
      <comments>https://cases.tistory.com/entry/React-Native-Expo-SDK-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC#entry89comment</comments>
      <pubDate>Sat, 13 Sep 2025 23:46:14 +0900</pubDate>
    </item>
    <item>
      <title>AI 900 자격증 취득 후기</title>
      <link>https://cases.tistory.com/entry/AI-900-%EC%9E%90%EA%B2%A9%EC%A6%9D-%EC%B7%A8%EB%93%9D-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 MS AI School 타운홀 미팅이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 미팅에는 AI 900 자격증 시험이 치뤄졌기에, 며칠 공부를 하고 시험을 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, 내가 찾아본 정보에 의하면 시험이 난이도가 엄청 어려운 편은 아니고, 지금까지 수업을 열심히 들었다면 통과할 수 있는 시험이어서 딱히 긴장이 되진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/credentials/certifications/azure-ai-fundamentals/?practice-assessment-type=certification&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/ko-kr/credentials/certifications/azure-ai-fundamentals/?practice-assessment-type=certification&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1756031786077&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Microsoft Certified: Azure AI 기본 사항 - Certifications&quot; data-og-description=&quot;AI 솔루션을 만들기 위한 Microsoft Azure의 소프트웨어 및 서비스 개발과 관련된 기본 AI 개념을 보여 줍니다.&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/ko-kr/credentials/certifications/azure-ai-fundamentals/?practice-assessment-type=certification&quot; data-og-url=&quot;https://learn.microsoft.com/ko-kr/credentials/certifications/azure-ai-fundamentals/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/e4e5f/hyZC0eFo30/5PpYWuXX4VQAlHUD3PHHgK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/credentials/certifications/azure-ai-fundamentals/?practice-assessment-type=certification&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/ko-kr/credentials/certifications/azure-ai-fundamentals/?practice-assessment-type=certification&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/e4e5f/hyZC0eFo30/5PpYWuXX4VQAlHUD3PHHgK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Microsoft Certified: Azure AI 기본 사항 - Certifications&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;AI 솔루션을 만들기 위한 Microsoft Azure의 소프트웨어 및 서비스 개발과 관련된 기본 AI 개념을 보여 줍니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-24 오후 7.36.07.png&quot; data-origin-width=&quot;1883&quot; data-origin-height=&quot;957&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/catOKv/btsP5hp36TB/95Zt7xVIfNVUScujvmGjRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/catOKv/btsP5hp36TB/95Zt7xVIfNVUScujvmGjRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/catOKv/btsP5hp36TB/95Zt7xVIfNVUScujvmGjRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcatOKv%2FbtsP5hp36TB%2F95Zt7xVIfNVUScujvmGjRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1883&quot; height=&quot;957&quot; data-filename=&quot;스크린샷 2025-08-24 오후 7.36.07.png&quot; data-origin-width=&quot;1883&quot; data-origin-height=&quot;957&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 사항은 위의 링크에 접속하면 안내가 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 시험은 온라인으로 이루어지고, $59 USD의 비용을 지불해야 하지만 현재 내가 수강 중인 과정이 Microsoft AI School이라 비용을 모두 제공받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 시험 감독까지 운영진 측에서 진행을 하여, 온라인 시험이지만 오프라인과 다를 바 없는 시험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 총 45문제가 출제 되었고, 모두 객관식 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신러닝, 딥러닝, NLP, LLM, Azure AI 등 AI 분야를 공부 하는 중이라면 기본적으로 알아야 하는 내용들로 구성되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난이도는 생각했던대로 크게 어렵진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Microsoft에서 주관하는 자격증이라서 그런지, 자사 서비스에 대한 문제가 나오는게 조금 불편했던 점(?)이었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 1000점 만점에 700점 이상이 통과이고, 당연히 난 합격을 했다 ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험이 종료되고 나면, 바로 해당 시험에 대한 결과가 나와서 확인을 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-08-24 오후 10.51.14.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;749&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBeVvj/btsP4vbkw09/pfqshtTAfYoMUPBSupoegk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBeVvj/btsP4vbkw09/pfqshtTAfYoMUPBSupoegk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBeVvj/btsP4vbkw09/pfqshtTAfYoMUPBSupoegk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBeVvj%2FbtsP4vbkw09%2FpfqshtTAfYoMUPBSupoegk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;790&quot; height=&quot;749&quot; data-filename=&quot;스크린샷 2025-08-24 오후 10.51.14.png&quot; data-origin-width=&quot;790&quot; data-origin-height=&quot;749&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시험 응시할 때 작성했던 나의 이메일로 바로 자격증 배지를 전송 받을 수 있다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>ai900</category>
      <category>AI자격증</category>
      <category>Microsoft</category>
      <category>msaischool</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/88</guid>
      <comments>https://cases.tistory.com/entry/AI-900-%EC%9E%90%EA%B2%A9%EC%A6%9D-%EC%B7%A8%EB%93%9D-%ED%9B%84%EA%B8%B0#entry88comment</comments>
      <pubDate>Sun, 24 Aug 2025 22:53:12 +0900</pubDate>
    </item>
    <item>
      <title>알고리즘 공부 내용 메모 - 시간 복잡도, 빅 오 표기법</title>
      <link>https://cases.tistory.com/entry/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B3%B5%EB%B6%80-%EB%82%B4%EC%9A%A9-%EB%A9%94%EB%AA%A8-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-%EB%B9%85-%EC%98%A4-%ED%91%9C%EA%B8%B0%EB%B2%95</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시간 복잡도는 서로 다른 알고리즘의 효율성을 비교할 때 사용는데, 몇 가지 규칙이 존재한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;- input &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;\geq&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;ge;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp; 0&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;입력값(n)은 항상 0보다 크다.&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;입력값이 음수일 수는 없기 때문에 복잡도는 항상 0보다 크다고 가정하고 계산을 해야한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;- functions do more work&lt;span&gt;&amp;nbsp;&lt;/span&gt;for more input&lt;/i&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;함수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;많은 입력값이 있을 때 더 많은 작업&lt;/b&gt;을 하게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;더 많은 입력값이 주어지면 어떤 작업을 하는 데 필요한 계산이나 처리 시간이 길어집니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;- drop all constants&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시간 복잡도에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;모든 상수를 삭제한다&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 어떤 알고리즘의 복잡도가 &amp;nbsp;&lt;span&gt;&lt;span&gt;3n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;이라면&amp;nbsp;3은 고려하지 않고 복잡도는&amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;이 된다. &amp;nbsp;&lt;span&gt;&lt;span&gt;2n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;3n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;10n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;모두 복잡도가 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 인 알고리즘.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;- ignore lower order terms&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;낮은 차수의 항들은 무시한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시간 복잡도에서는 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;과&amp;nbsp;&lt;span&gt;&lt;span&gt;n^2&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;top: -0.363em; margin-right: 0.05em;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;를 비교할 때에는 항상&amp;nbsp;&lt;span&gt;&lt;span&gt;n^2&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;top: -0.363em; margin-right: 0.05em;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;이 더 오래 걸리는 알고리즘이라고 판단한다. 여기서 의문이 들 수 있는 점은 그래프에서 (1,1)인 지점 전까지는 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 이 더 오래 걸릴 수 있다는 것.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 알고리즘에서는 입력값( &lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;)이 1보다 작은 값은 고려하지 않고 큰 값에 대해서만 생각을 하므로&amp;nbsp;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&amp;nbsp;이 무한으로 커진 경우를 가정&lt;/b&gt;하고 비교해야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이런 이유로 시간 복잡도에서는 낮은 차수의 항들은 무시한다.&amp;nbsp; &lt;span&gt;&lt;span&gt;n^3 + n^2 + n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;top: -0.363em; margin-right: 0.05em;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;top: -0.363em; margin-right: 0.05em;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;이라는 함수가 있을 때, &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;과&amp;nbsp;&lt;span&gt;&lt;span&gt;n^2&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;top: -0.363em; margin-right: 0.05em;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;은 알고리즘의 시간 복잡도에 영향을 미치지 않고, 입력값이 무한이 될 때 고려해야 할 부분은&amp;nbsp;&amp;nbsp;&lt;span&gt;&lt;span&gt;n^3&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;top: -0.363em; margin-right: 0.05em;&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;font-size: 0em;&quot;&gt;​&lt;/span&gt;&lt;/span&gt;​&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 이다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;- ignore the base of logs&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;시간 복잡도 함수가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;log 함수를 포함할 경우 밑은 무시한다&lt;/b&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;모든 로그는 서로 배수 관계. 그래서 복잡도에 관해서 이야기할 때는 로그의 밑에 대해서는 고려하지 않아도 됨.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;로그의 밑은 무시하고 로그 ( &lt;span&gt;&lt;span&gt;logn&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;margin-right: 0.01968em;&quot;&gt;l&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span style=&quot;margin-right: 0.03588em;&quot;&gt;g&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; ) 알고리즘이라고만 말하면 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;복잡도가&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;margin-right: 0.01968em;&quot;&gt;l&lt;/span&gt;&lt;span&gt;o&lt;/span&gt;&lt;span style=&quot;margin-right: 0.03588em;&quot;&gt;g&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;인 알고리즘은 보통&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;무언가를 반으로 나누거나 2를 곱한 경우&lt;/b&gt;에 자주 사용된다. 그래서 만약 for 반복문을 사용해서 무언가를 탐색하면서 반으로 나누거나, 2를 곱할 때 복잡도는 밑이 2인 로그가 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;10으로 나누거나 10을 곱하게 되면 밑이 10인 로그가 된다. 하지만 시간 복잡도를 표시할 때에는 로그의 밑은 무시하고 그냥 log n 복잡도를 가진다고 표현한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;-&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;2n = O(n)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span style=&quot;margin-right: 0.02778em;&quot;&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;=&amp;gt;&amp;nbsp;&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;&lt;span&gt;2n \in O(n)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&amp;isin;&lt;/span&gt;&lt;span style=&quot;margin-right: 0.02778em;&quot;&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;등호를 사용하여 표현한다&lt;/b&gt;.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;2n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;O(n)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;margin-right: 0.02778em;&quot;&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;과 같습니다. 여기서&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;span&gt;O(n)&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;margin-right: 0.02778em;&quot;&gt;O&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&amp;nbsp;은 &amp;nbsp;&lt;span&gt;&lt;span&gt;2n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 이 어떤 함수의 집합에 속한다는 의미를 가진다. 그렇기 때문에 아래와 같은 등호를 활용하여 이 관계를 수학적으로 쓸 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2n = O(n),&amp;nbsp; 2n&amp;nbsp;&amp;isin; O(n)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;빅 오 표기법&lt;/b&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;빅 오 표기법은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;알고리즘의 효율성을 표시하는 표기법이다&lt;/b&gt;. 빅 오 표기법을 사용하면 어떤 알고리즘을 다른 알고리즘과 비교해서 표현하는 것이 가능하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbSz7M/btsPyDf6EB4/4seLiUuJkDZRKp2GhgkZ3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbSz7M/btsPyDf6EB4/4seLiUuJkDZRKp2GhgkZ3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbSz7M/btsPyDf6EB4/4seLiUuJkDZRKp2GhgkZ3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbSz7M%2FbtsPyDf6EB4%2F4seLiUuJkDZRKp2GhgkZ3k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;377&quot; height=&quot;321&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 그래프는 복잡도가 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 인 알고리즘에 빅 오 표기법을 적용한 결과이다. x축은 복잡도 n, y축은 필요한 일의 양이나 메모리를 의미한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다른 알고리즘이 이 그래프의 어떤 위치에 있는지에 따라&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;복잡도 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 인 알고리즘과 다른 알고리즘의 복잡도를 비교할 수 있다. 다른 알고리즘이 복잡도 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 인 알고리즘의 아래에 있다면, 같은 일을 하는 데 시간이 덜 들기 때문에 더 빠른 알고리즘이라 한다. 반대로, 복잡도 &amp;nbsp;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;n&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; 인 알고리즘의 위에 있다면, 더 느린 알고리즘이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;빅 오 표기법에서는 이러한 알고리즘 간의 관계를 다음과 같이 표현한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;O (빅 오 복잡도) : 비교 대상인 그래프가 일치 혹은 아래에 있을 때. 비교 대상인 다른 알고리즘과 같거나 더 빠르다.&lt;/li&gt;
&lt;li&gt;&amp;theta; (세타 복잡도) : 비교 대상인 그래프가 일치할 때. 비교 대상인 다른 알고리즘과 같다.&lt;/li&gt;
&lt;li&gt;&amp;Omega; (빅 오메가 복잡도)&amp;nbsp;:&amp;nbsp;비교 대상인&amp;nbsp;그래프가&amp;nbsp;일치&amp;nbsp;혹은&amp;nbsp;위에 있을 때. 비교 대상인 다른 알고리즘과 같거나 느리다.&lt;/li&gt;
&lt;li&gt;o (리틀 오 복잡도) : 비교 대상인 그래프가 아래에 있을 때. 비교 대상인 다른 알고리즘보다 더 빠르다.&lt;/li&gt;
&lt;li&gt;&amp;omega; (리틀 오메가 복잡도) : 비교 대상인 그래프가 위에 있을 때. 비교 대상인 다른 알고리즘과 느리다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>CS</category>
      <category>알고리즘</category>
      <category>컴싸</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/87</guid>
      <comments>https://cases.tistory.com/entry/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EA%B3%B5%EB%B6%80-%EB%82%B4%EC%9A%A9-%EB%A9%94%EB%AA%A8-%EC%8B%9C%EA%B0%84-%EB%B3%B5%EC%9E%A1%EB%8F%84-%EB%B9%85-%EC%98%A4-%ED%91%9C%EA%B8%B0%EB%B2%95#entry87comment</comments>
      <pubDate>Tue, 19 Aug 2025 23:09:51 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 2차 팀프로젝트 - 날 힘들게 했던 오류들 모음</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-2%EC%B0%A8-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%82%A0-%ED%9E%98%EB%93%A4%EA%B2%8C-%ED%96%88%EB%8D%98-%EC%98%A4%EB%A5%98%EB%93%A4-%EB%AA%A8%EC%9D%8C</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;프로젝트를 진행 하는 중에도 수 많은 오류들을 해결하면서 진행이 되었지만, 발표 전날 나를 책상에서 15시간 동안 일어나지 못하게 했던 여러 오류들을 모아보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 날 모든걸 다 해결하고 책상에서 일어나려는 순간, 허리에 극심한 통증을 느끼며 그대로 저녁도 못 먹고 침대에 누웠다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(왜 사람들이 비싸도 좋은 의자를 쓰라고 하는지 알겠음.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 배포를 어떤식으로 했는지 간략히 설명을 하자면, 모든 작업물을 Github의 팀 Repository에 push 해놓고, Azure VM을 활용하여 구축한 가상환경에서 그대로 pull로 당겨오는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 실행은 로컬과 마찬가지로 FastAPI의 uvicorn서버로 실행을 해주었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;1. CORS Error&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 오류는 개발 단계에서 로컬 환경으로 테스트를 하며 발생했던 오류였는데, 나름 인상 깊었던(?) 오류였어서 한 번 다뤄 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트의 기능 중 하나인 챗봇이 유튜브 영상 추천을 할 때, 유튜브 API를 통해 영상 search query를 보내고, 결과를 받아 오게 되어있다. 그런데 유튜브 영상을 받아 오려고 하면 자꾸 에러가 나서 확인을 해본 결과, CORS Error라고 나오는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 CORS에 대해서 조금씩 찾아보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CORS(Cross-Origin-Resource-Sharing)는 &lt;/b&gt;서로&amp;nbsp;다른 출처들 끼리도&amp;nbsp; 리소스를 공유한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다른&amp;nbsp;&lt;b&gt;출처(Origin)란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 출처는 다음과 같이 3요소로 정의된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;프로토콜&lt;/b&gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;http://&lt;span&gt;, &lt;/span&gt;https://&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;호스트&lt;/b&gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;localhost&lt;span&gt;, &lt;/span&gt;127.0.0.1&lt;span&gt;, &lt;/span&gt;myserver.test.org&lt;span&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;포트 번호&lt;/b&gt;&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;3000&lt;span&gt;, &lt;/span&gt;8000&lt;span&gt; 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 다음은 모두 서로 다른 origin이라고 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;http://localhost:3000&lt;/li&gt;
&lt;li&gt;http://127.0.0.1:3000&lt;/li&gt;
&lt;li&gt;http://localhost:8000&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 origin에서는 요청이 오는 곳과 처리하는 곳이 일치하기 때문에 따로 보안처리를 해줄 필요가 없다. 하지만 다른 origin에서 오는 요청이라면 내가 요청으로 받아온 결과가 믿을만한지 검증하는 과정이 필요하다. 대부분의 브라우저에서는 기본적으로 결과의 헤더 값을 통해 CORS를 확인한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서는 localhost:8080 서버에서 전달 받은 응답 중 헤더에 Access-Control-Allow-Origin 값을 확인하고, 이 값에 현재 origin이 포함되어 있는지 확인한다. 만약 있다면 CORS를 수행하고, 그렇지 않다면 에러를 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 따지고 보면 CORS 에러는 브라우저에서 발생시키는 에러이다. 그래서 curl을 통해 서버에 요청을 보내면 문제없이 응답을 보내주지만, 프론트 환경에서 테스트를 할 때만 CORS 에러가 발생하는 것도 그런 이유였던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 간단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드에서 그냥 모든 주소를 Access-Control-Allow-Origin로 주면 해결된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754309119382&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[&quot;*&quot;],
    allow_credentials=True,
    allow_methods=[&quot;*&quot;],
    allow_headers=[&quot;*&quot;],
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 실제 서비스 배포시에는 더 정밀한 보안 작업이 이루어져야겠지만, 학습이 목적인 프로젝트에서, 그리고 계속해서 테스트를 해야하는 단계에서는 위와 같은 방법으로 간단히 해결을 할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;2. 414 Error&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 공부를 하는 사람이라면, 그래도 404, 405 정도는 많이 봤을 거다. 그런데 414 Error는 본 적 있는가? 나는 솔직히 이번에 처음 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 오류는 유튜브 API 요청을 보내는 부분에서 발생했는데, 위에서 겪었던 CORS 문제랑 비슷한 맥락이긴 하다. 다만 중요한 건, 그 문제는 로컬 환경에서도 발생했던 반면, 이번 414 Error는 &lt;span&gt;&lt;b&gt;VM 환경에서만&lt;/b&gt;&lt;/span&gt; 발생했다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 414 Error는 &lt;span&gt;&lt;b&gt;URI Too Long&lt;/b&gt;&lt;/span&gt;이라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는 콘솔에 뜨는 에러 메시지가 생각만큼 친절하지 않아서, 정확한 원인을 파악하기가 어려웠다. 그래서 여기저기 블로그 글도 찾아보고, 아는 LLM들을 총동원해서 끙끙 앓듯이 매달렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중, 요청 query를 직접 확인해보게 됐는데 원래라면 챗봇의 응답에서 &lt;span&gt;&lt;b&gt;3개의 키워드만 추출해서 유튜브 검색&lt;/b&gt;&lt;/span&gt;을 해야 하는데, 응답 전체가 query로 들어가고 있었던 거다. 이걸 보고 바로 눈치챘다. 아, 이건 키워드 3개만 추출하는 함수 자체가 &lt;span&gt;&lt;b&gt;애초에 제대로 작동을 안 했구나.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 파고들어보니, 챗봇의 응답 전체에서 키워드를 추출해야 하는 함수로 넘어가는 과정에서, &lt;span&gt;&lt;b&gt;응답이 너무 길어서&lt;/b&gt;&lt;/span&gt; 아예 그 함수까지 못 가고 에러가 바로 터진 거였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 여기서 또 하나의 의문이 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;ldquo;왜 로컬 환경에서는 멀쩡히 잘 작동했지?&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면, VM 환경에선 프로젝트가 FastAPI 서버로 구동되긴 했지만, 그 서버가 &lt;span&gt;&lt;b&gt;Apache를 통해 라우팅&lt;/b&gt;&lt;/span&gt;되고 있었기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 실제 요청이 Apache 웹 서버를 거쳐서 uvicorn으로 가는 구조였는데, &lt;span&gt;&lt;b&gt;Apache는 보안상의 이유로 URI 최대 길이에 제한이 걸려있다.&lt;/b&gt;&lt;/span&gt; 스팸이나 DoS 공격을 방지하기 위해 기본적으로 처리할 수 있는 URL 길이에 제한이 있는 거지. 그래서 저렇게 URI가 길어지면 414 에러가 발생할 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 간단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프론트에서 요청을 보낼 때, base URL에 8000 포트를 명시해주면 Apache를 거치지 않고 FastAPI(uvicorn)로 직접 연결되게&lt;/b&gt;&lt;span&gt; 할 수 있다. 그렇게 하니까 바로 해결됐다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 정리하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정 전 :&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BASE_API_URL이 포트 없이 설정되어 있었음 (예: &lt;a href=&quot;http://your-domain.com/api)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;http://your-domain.com) &lt;/a&gt;브라우저는 이 주소의 기본 포트인 80번으로 요청을 보냄.&lt;/li&gt;
&lt;li&gt;아파치가 80번 포트에서 이 매우 긴 URL 요청을 받음.&lt;/li&gt;
&lt;li&gt;아파치에는 스팸이나 서비스 거부(DoS) 공격 등을 방지하기 위해 처리할 수 있는 URL의 최대 길이를 제한하는 설정(LimitRequestLine)이 있음.&lt;/li&gt;
&lt;li&gt;요청된 URL이 이 설정된 길이보다 길었기 때문에, 아파치는 &quot;414 URI Too Long&quot; 에러를 응답하고 FastAPI 서버로 요청을 전달조차 하지 않고 연결을 끊어버림.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정 후 :&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;BASE_API_URL에 :8000을 추가. (예: &lt;a href=&quot;http://your-domain.com:8000/api)&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;http://your-domain.com:8000)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;브라우저는 이제 80번 포트가 아닌 8000번 포트로 직접 요청을 보냄.&lt;/li&gt;
&lt;li&gt;이 요청은 Apache를 거치지 않고, 방화벽에서 허용되어 있다면 곧바로 FastAPI 애플리케이션 서버(Uvicorn)에 도달.&lt;/li&gt;
&lt;li&gt;FastAPI를 실행하는 Uvicorn 같은 ASGI 서버들은 일반적으로 아파치의 기본 설정보다 URL 길이에 대한 제한이 훨씬 널널하기 때문에 에러를 발생시키지 않음.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;3. 프롬프트 엔지니어링&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명 로컬에서 할 때에는 잘 됐었는데, VM환경에서 테스트를 해보니까 거의 10% 확률로 플랜을 저장 해주었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인은 깊게 파고들 필요도 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 해당 기능의 흐름은, &lt;span&gt;&lt;b&gt;챗봇의 답변에서 필요한 정보(날짜, 운동 종류, 세트 수, 음식 이름 등)를 추출&lt;/b&gt;&lt;/span&gt;해서 내가 지정해준 &lt;span&gt;&lt;b&gt;JSON 형태로 파싱&lt;/b&gt;&lt;/span&gt;하고, 그걸 다시 DB 저장 함수에 넘겨서 저장하는 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 문제는, 챗봇의 답변에서 이 필요한 정보를 &lt;span&gt;&lt;b&gt;정확히 추출하지 못하면서&lt;/b&gt;&lt;/span&gt;, 일부 데이터만 DB에 저장되거나, &lt;span&gt;&lt;b&gt;아예 저장이 안 되고 Time Out 에러가 발생&lt;/b&gt;&lt;/span&gt;하는 상황이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는, 챗봇이 만든 답변에서 정보를 추출해주는 부분의 &lt;span&gt;&lt;b&gt;시스템 프롬프트를 계속 수정&lt;/b&gt;&lt;/span&gt;하면서 테스트를 돌렸다. 조금 더 명확하게, 상세하게 지시해주면 나아지겠지 싶어서 계속 바꿔봤지만, 아무리 잘 써도 프롬프트가 너무 길어져서 &lt;span&gt;&lt;b&gt;요청 시간만 오래 걸리고&lt;/b&gt;&lt;/span&gt;, 성능은 어느 정도 나아졌지만 결국 &lt;span&gt;&lt;b&gt;한계에 부딪히게 되었다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중, 한 가지 흐름에서 힌트를 얻었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;챗봇이 운동/식단 플랜에 대한 요청을 받았을 때의 답변 내용이 전반적으로 랜덤성이 있다는 점&lt;/b&gt;&lt;span&gt;이다. 특히 날짜 부분(예를 들어 &lt;/span&gt;&lt;span&gt;Day 1&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Day2&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;첫째날&lt;/span&gt;&lt;span&gt; 등)이 &lt;/span&gt;&lt;b&gt;고정된 양식 없이 랜덤하게&lt;/b&gt;&lt;span&gt; 표현되고 있었고, 이 때문에 &lt;/span&gt;&lt;b&gt;정보 추출 과정이 애매&lt;/b&gt;&lt;span&gt;해지는 상황이 반복되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;b&gt;루틴에 대한 답변 양식을 정확하게 정의해준 적이 없었기 때문&lt;/b&gt;&lt;span&gt;이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 방향을 바꿨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;애초에 챗봇이 운동/식단 플랜을 응답할 때부터 어느 정도의 양식을 정해주고&lt;/b&gt;&lt;/span&gt;, 그 양식대로 답변을 하도록 가이드라인을 만들어줬다. 그리고 이어지는 &lt;span&gt;&lt;b&gt;정보 추출 단계에서도&lt;/b&gt;&lt;/span&gt;, 챗봇이 어떤 양식으로 답변을 주게 될지를 염두에 두고, 해당 양식을 어떻게 JSON으로 파싱하면 되는지에 대한 프롬프트를 &lt;span&gt;&lt;b&gt;정확하고 명확하게 작성&lt;/b&gt;&lt;/span&gt;해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로, 이 방식으로 프롬프트를 수정한 이후에는 &lt;span&gt;&lt;b&gt;거의 100%에 가까운 정확도로 정보 추출과 DB 저장이 성공&lt;/b&gt;&lt;/span&gt;적으로 이루어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 기본 시스템 프롬프트 일부 (plan 요청에 대한 답변 가이드 라인 부분)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754314994956&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[운동 루틴 및 식단 플랜 생성 지침 (필수)]
    사용자가 운동 루틴 또는 식단 플랜을 요청하면, 다음 마크다운 형식을 엄격하게 준수하여 답변하세요.
    - **날짜는 항상 명확하게 'YYYY년 M월 D일' 형식으로 표시하세요.**
    - **운동 루틴과 식단 플랜은 항상 별도의 섹션으로 구분하세요.**

    **운동 루틴 형식:**
    ```
    # 운동 루틴

    ## YYYY년 M월 D일 (요일) - [운동 목표/부위]
    - 운동명: 세트수 x 횟수 (무게/시간)
    - 운동명: 세트수 x 횟수 (무게/시간)
    ...

    ## YYYY년 M월 D일 (요일) - [운동 목표/부위]
    - 운동명: 세트수 x 횟수 (무게/시간)
    ...
    ```
    *   예시: `- 스쿼트: 3x10 (50kg)`, `- 플랭크: 3x30초`
    *   무게나 시간이 없는 맨몸 운동은 괄호 생략

    **식단 플랜 형식:**
    ```
    # 식단 플랜

    ## YYYY년 M월 D일 (요일)
    ### 아침
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ### 점심
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ### 저녁
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ### 간식
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ```
    *   예시: `- 닭가슴살: 165kcal, 31g, 0g, 3.6g`
    *   정보가 없는 경우 '정보 없음'으로 표시 (예: `- 사과: 정보 없음, 정보 없음, 정보 없음, 정보 없음`)

    **두 가지 플랜을 모두 요청받았을 경우, 운동 루틴을 먼저 제시하고 이어서 식단 플랜을 제시하세요.**&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;답변 파싱 및 저장 함수의 시스템 프롬프트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1754315073892&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;당신은 AI 트레이너의 답변에서 제공된 마크다운 형식의 운동 루틴과 식단 플랜을 추출하여 JSON으로 변환하는 시스템입니다.
    AI의 답변은 다음 마크다운 형식을 엄격하게 준수합니다:

    **운동 루틴 형식:**
    ```
    # 운동 루틴

    ## YYYY년 M월 D일 (요일) - [운동 목표/부위]
    - 운동명: 세트수 x 횟수 (무게/시간)
    ...
    ```
    *   예시: `- 스쿼트: 3x10 (50kg)`, `- 플랭크: 3x30초`
    *   무게나 시간이 없는 맨몸 운동은 괄호 생략

    **식단 플랜 형식:**
    ```
    # 식단 플랜

    ## YYYY년 M월 D일 (요일)
    ### 아침
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ### 점심
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ### 저녁
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ### 간식
    - 음식명: 칼로리(kcal), 단백질(g), 탄수화물(g), 지방(g)
    ```

    **추출 지침:**
    1.  **날짜 파싱:** 'YYYY년 M월 D일 (요일)' 형식의 날짜를 'YYYY-MM-DD' 형식으로 변환하세요. 요일 정보는 무시합니다. 만약 시작 날짜가 명확히 명시되지 않았다면, 오늘({date.today().isoformat()})을 기준으로 삼으세요.
    2.  **운동 루틴 파싱:**
        - 각 운동 항목에서 '운동명', '세트수', '횟수', '무게(kg)' 또는 '시간(분)'을 추출하세요.
        - '무게/시간'이 없는 경우 해당 필드는 null로 처리하세요.
        - '무게'는 'weight_kg' 필드에 숫자로 저장하고, '시간'은 'duration_min' 필드에 분 단위 정수(integer)로 저장하세요. 둘 다 있는 경우는 없습니다. 둘 중 하나만 존재합니다.
        - 'exercise_name', 'reps', 'sets', 'weight_kg', 'duration_min' 필드를 사용하세요.
    3.  **식단 플랜 파싱:**
        - 각 식사 항목에서 'meal_type' (아침, 점심, 저녁, 간식), '음식명', '칼로리(kcal)', '단백질(g)', '탄수화물(g)', '지방(g)'을 추출하세요.
        - '칼로리', '단백질', '탄수화물', '지방'은 가능한 한 구체적인 수치로 제공하고, 정보가 없는 경우 null로 처리하세요.
        - 'food_name', 'calories', 'protein_g', 'carbs_g', 'fat_g' 필드를 사용하세요.

    **출력 형식:**
    ```json
    {{&quot;plans&quot;: [
        {{&quot;date&quot;: &quot;YYYY-MM-DD&quot;, &quot;type&quot;: &quot;workout&quot;, &quot;items&quot;: [
            {{&quot;exercise_name&quot;: &quot;운동명&quot;, &quot;reps&quot;: &quot;횟수&quot;, &quot;sets&quot;: &quot;세트수&quot;, &quot;weight_kg&quot;: &quot;무게(숫자) 또는 null&quot;, &quot;duration_min&quot;: &quot;시간(정수) 또는 null&quot;}}
        ]}},
        {{&quot;date&quot;: &quot;YYYY-MM-DD&quot;, &quot;type&quot;: &quot;diet&quot;, &quot;items&quot;: [
            {{&quot;meal_type&quot;: &quot;아침/점심/저녁/간식&quot;, &quot;food_name&quot;: &quot;음식명&quot;, &quot;calories&quot;: &quot;칼로리(숫자) 또는 null&quot;, &quot;protein_g&quot;: &quot;단백질(숫자) 또는 null&quot;, &quot;carbs_g&quot;: &quot;탄수화물(숫자) 또는 null&quot;, &quot;fat_g&quot;: &quot;지방(숫자) 또는 null&quot;}}
        ]}}
    ]}}
    ```
    만약 AI 답변이 운동 루틴이나 식단 계획이 아니거나 파싱할 수 없으면, {{&quot;plans&quot;: []}} 를 반환하세요.
    &quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 기간이 짧긴 했어도, 초반에 바짝 열심히 해서 나름 빨리 잘 마무리 하겠다 싶었는데 하필 하루 전날 이러한 오류들이 여기저기 터져 나와서 진짜 죽고 싶었다... 하지만 덕분에 더 완성도 높은 결과물을 만들게 된 것 같기도 하고, 결과적으론 모든 오류들을 다 해결할 수 있어서 아주 뿌듯했던 경험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음부터는 배포 환경에서의 테스트도 미리 염두에 두고, 프로젝트를 미리 배포 환경에서도 테스트를 계속 돌려보는 과정을 거치면 더 좋을 것 같다는 깨달음(?)도 얻을 수 있었다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>Chatbot</category>
      <category>msaischool</category>
      <category>챗봇</category>
      <category>프로젝트</category>
      <category>헬스케어</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/86</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-2%EC%B0%A8-%ED%8C%80%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EB%82%A0-%ED%9E%98%EB%93%A4%EA%B2%8C-%ED%96%88%EB%8D%98-%EC%98%A4%EB%A5%98%EB%93%A4-%EB%AA%A8%EC%9D%8C#entry86comment</comments>
      <pubDate>Mon, 4 Aug 2025 22:49:05 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 2차 팀 프로젝트 - 2</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-2%EC%B0%A8-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우리의 서비스는 기본적으로 사용자에 대한 정보가 저장이 되어야 했기에, 데이터 베이스를 구축하는 작업이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 사용할 프레임워크에 대해 고민을 하였는데, 비동기 처리에 적합한 FastAPI, 그리고 이와 잘 호환이 되기도 하고, 내가 기존에 사용해본 경험이 있는 Oracle SQL 과 비슷한 MySQL을 사용하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 나는 팀원들과 의논하여 DB에 저장되어야 할 데이터들에 대한 정의를 먼저 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;시작하기에 앞서, 대부분의 코드 작업들은 AI 에이전트의 도움을 받아 진행되었다는 점을 밝힌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;요즘은 코딩을 할때 Gemini CLI의 도움을 많이 받는데, 거의 무료나 다름 없는 이 녀석이 상당히 성능이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;DataBase 설계 &amp;amp; 구축&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 사용자의 정보가 담긴 테이블은 필요로 했고, 사용자는 가입 시, 본인의 운동 수준이 어느 정도인지 기입을 해야 하기에, 운동 레벨 테이블도 따로 필요로 했다.(각 운동 수준별 설명이 필요했기에 따로 분리해서 관리해주었다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 챗봇과 대화한 내역이 저장될 chat_histories 테이블, 마지막으로 시간의 흐름에 따른 체중 변화를 보여주기 위한 체중 테이블도 정의해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;967&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FWQix/btsPp0pN5iW/jp2Zq9nipfymM6iwDWANV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FWQix/btsPp0pN5iW/jp2Zq9nipfymM6iwDWANV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FWQix/btsPp0pN5iW/jp2Zq9nipfymM6iwDWANV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFWQix%2FbtsPp0pN5iW%2Fjp2Zq9nipfymM6iwDWANV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1033&quot; height=&quot;967&quot; data-origin-width=&quot;1033&quot; data-origin-height=&quot;967&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 테이블 생성 SQL 쿼리문&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752908861822&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE TABLE training_levels (
    level INT PRIMARY KEY,
    description TEXT
);

-- 사용자 테이블
CREATE TABLE users (
    user_id VARCHAR(50) PRIMARY KEY,
    age INT NOT NULL,
    gender ENUM('남성', '여성') NOT NULL,
    weight FLOAT NOT NULL,
    height FLOAT NOT NULL,
    level INT NOT NULL,
    injury_level VARCHAR(100),
    injury_part VARCHAR(100),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (level) REFERENCES training_levels(level)
        ON UPDATE CASCADE
        ON DELETE RESTRICT
);

CREATE TABLE chat_histories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id VARCHAR(50) NOT NULL,
    role_type ENUM('user', 'assistant', 'system') NOT NULL,
    content TEXT NOT NULL,
    embedding JSON,
    timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);

CREATE TABLE weight_history (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id VARCHAR(50),
    weight FLOAT NOT NULL,
    recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;백엔드 아키텍쳐&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 내가 설계한 백엔드 구조는 계층형 아키텍처(Layered Architecture)를 따르고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 디렉터리와 파일이 명확한 역할을 갖도록 설계하여, 코드가 복잡해져도 쉽게 관리하고 확장할 수 있도록 하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;main.py : FastAPI 애플리케이션의 시작점. 서버 실행, 미들웨어 설정, 라우터 등록 등 핵심 설정을 담당&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;routers/ : 클라이언트의 HTTP 요청을 직접 받는 API 엔드포인트들을 정의&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;crud/ : 데이터베이스와 상호작용하며 비즈니스 로직을 처리&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;schemas/ : Pydantic 모델을 사용하여 API 요청 및 응답의 데이터 형태를 정의하고 검증&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;utils/ : 특정 기능을 수행하는 재사용 가능한 함수들의 모임&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;core/ &amp;amp; database.py : 데이터베이스 연결 설정, 환경 변수 등 프로젝트의 핵심 설정을 관리&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;회원가입&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;흐름&amp;nbsp;요약&lt;/i&gt;&lt;br /&gt;&lt;i&gt;Client 요청 &amp;rarr; routers/user.py &amp;rarr; crud/user.py &amp;rarr; DB에 사용자 저장 &amp;rarr; JWT 발급 &amp;rarr; Client 응답&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(1)&amp;nbsp;API&amp;nbsp;엔드포인트&amp;nbsp;(routers/user.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 클라이언트가 회원가입 POST 요청을 보내면 아래의 코드가 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1752913510959&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# routers/user.py
from fastapi import APIRouter, HTTPException
from schemas.user import UserSignup
from schemas.user import UserLogin
from crud.user import verify_user
from crud.user import create_user
from utils.jwt_handler import create_access_token

router = APIRouter(prefix=&quot;/users&quot;)

@router.post(&quot;/signup&quot;)
async def signup(user: UserSignup):
    success = await create_user(user)
    if not success:
        raise HTTPException(status_code=400, detail=&quot;이미 존재하는 아이디입니다.&quot;)

    # JWT 토큰 생성
    access_token = create_access_token(data={&quot;sub&quot;: user.user_id})

    # 메시지 + 토큰 반환
    return {
        &quot;message&quot;: f&quot;{user.user_id}님 가입이 완료되었습니다.&quot;,
        &quot;access_token&quot;: access_token
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청으로 들어온 데이터가 schemas/user.py에 정의된 UserSignup 모델과 일치하는지 자동으로 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스 작업은 crud 계층으로 넘겨주고, 가입이 성공하면 바로 로그인 상태를 유지할 수 있도록 JWT토큰을 발급하게 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# schemas / user.py&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752913650760&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# schemas/user.py
from pydantic import BaseModel

class UserSignup(BaseModel):
    user_id: str
    gender: str
    age: int
    height: float
    weight: float
    level: int
    injury_level: str | None = None
    injury_part: str | None = None

class UserLogin(BaseModel):
    user_id: str&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(2)&amp;nbsp;데이터베이스&amp;nbsp;처리&amp;nbsp;(crud/user.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;라우터로 부터 요청을 받아 데이터베이스에 사용자 정보를 저장하는 부분.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;INSERT를 하기 전에 SELECT 로 사용자가 이미 존재하는지 먼저 확인하여 데이터 무결성을 보장하였다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752913936879&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# crud/user.py
from database import database
from schemas.user import UserSignup

async def create_user(user: UserSignup):
    # 1. 먼저 동일한 user_id가 이미 존재하는지 확인
    check_query = &quot;SELECT 1 FROM users WHERE user_id = :user_id&quot;
    existing = await database.fetch_one(query=check_query, values={&quot;user_id&quot;: user.user_id})
    
    if existing:
        return False  # 이미 존재함

    # 2. 존재하지 않는다면 새로 INSERT
    insert_query = &quot;&quot;&quot;
        INSERT INTO users (user_id, gender, age, height, weight, level, injury_level, injury_part)
        VALUES (:user_id, :gender, :age, :height, :weight, :level, :injury_level, :injury_part)
    &quot;&quot;&quot;
    await database.execute(query=insert_query, values={
        &quot;user_id&quot;: user.user_id,
        &quot;gender&quot;: user.gender,
        &quot;age&quot;: user.age,
        &quot;height&quot;: user.height,
        &quot;weight&quot;: user.weight,
        &quot;level&quot;: user.level,
        &quot;injury_level&quot;: user.injury_level,
        &quot;injury_part&quot;: user.injury_part,
    })
    return True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;로그인&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;흐름&amp;nbsp;요약&lt;/i&gt;&lt;br /&gt;&lt;i&gt;Client 요청 &amp;rarr; routers/user.py &amp;rarr; crud/user.py &amp;rarr; DB에서 사용자 확인 &amp;rarr; JWT 발급 &amp;rarr; Client 응답&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(1)&amp;nbsp;API&amp;nbsp;엔드포인트&amp;nbsp;(routers/user.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;crud&amp;nbsp;계층에&amp;nbsp;사용자&amp;nbsp;확인을&amp;nbsp;요청하고,&amp;nbsp;성공하면&amp;nbsp;JWT&amp;nbsp;토큰을&amp;nbsp;발급하여&amp;nbsp;반환한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752914228988&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@router.post(&quot;/login&quot;)
async def login(user: UserLogin):
    is_valid = await verify_user(user.user_id)
    if not is_valid:
        raise HTTPException(status_code=404, detail=&quot;존재하지 않는 사용자입니다.&quot;)
    # ✅ JWT 토큰 생성
    access_token = create_access_token(data={&quot;sub&quot;: user.user_id})
    
    return {
        &quot;access_token&quot;: access_token,
        &quot;token_type&quot;: &quot;bearer&quot;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(2)&amp;nbsp;사용자&amp;nbsp;검증&amp;nbsp;(crud/user.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;회원가입 때와 마찬가지로, SELECT 쿼리를 통해 데이터베이스에 해당 user_id가 있는지 확인.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752914374766&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async def verify_user(user_id: str) -&amp;gt; bool:
    query = &quot;SELECT user_id FROM users WHERE user_id = :user_id&quot;
    result = await database.fetch_one(query=query, values={&quot;user_id&quot;: user_id})
    return result is not None&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;JWT 토큰 인증&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인을 하지 않은 사용자들의 진입을 제한하기 위해서, JWT 토큰을 사용하여 사용자 인증 기능을 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(1)&amp;nbsp;JWT&amp;nbsp;생성&amp;nbsp;(utils/jwt_handler.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;토큰의 유효기간을 설정하여, 이 시간이 지나면 토큰은 자동으로 만료되게 하였다.&lt;br /&gt;사용자 정보와 만료 시간을 SECRET_KEY(비밀 키)를 이용해 암호화하여 아무나 위조할 수 없는 토큰을 생성한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752914551825&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# backend/utils/jwt_handler.py

from datetime import datetime, timedelta
from jose import JWTError, jwt
from fastapi import HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer
from core.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;users/login&quot;)  # 로그인 API 엔드포인트 경로

# 토큰 생성 함수
def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({&quot;exp&quot;: expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(2)&amp;nbsp;JWT&amp;nbsp;검증&amp;nbsp;및&amp;nbsp;사용자&amp;nbsp;인증&amp;nbsp;(dependencies.py)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;사용자가 로그인 후 인증이 필요한 API를 요청할 때, 이 토큰이 유효한지 검사해야 한다. &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;FastAPI의 Dependency Injection(의존성 주입) 시스템을 사용하면 이 과정을 쉽게 처리할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1752914801844&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# backend/dependencies.py

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.getenv(&quot;SECRET_KEY&quot;)
ALGORITHM = os.getenv(&quot;ALGORITHM&quot;)

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=&quot;users/login&quot;)

async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get(&quot;sub&quot;)
        if user_id is None:
            raise HTTPException(status_code=401, detail=&quot;토큰에 사용자 정보 없음&quot;)
        return {&quot;user_id&quot;: user_id}
    except JWTError:
        raise HTTPException(status_code=401, detail=&quot;유효하지 않은 토큰&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 get_current_user 함수는 보호된 API 엔드포인트에서 다음과 같이 사용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1752914859987&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@router.get(&quot;/me&quot;)
async def read_user_info(current_user: dict = Depends(get_current_user)):
    user_id = current_user[&quot;user_id&quot;]
    user = await get_user_by_id(user_id)
    if not user:
        raise HTTPException(status_code=404, detail=&quot;사용자 정보를 찾을 수 없습니다.&quot;)
    return {&quot;user&quot;: user}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Depends(get_current_user) 한 줄만 추가하면, FastAPI가 알아서 요청 헤더에서 토큰을 추출하고 get_current_user 함수로 검증한 뒤, 그 결과를 current_user 매개변수에 넣어준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 토큰이 없거나 유효하지 않으면, 함수 본문은 실행조차 되지 않고 바로 에러가 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;챗봇과 대화 하기&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;흐름&amp;nbsp;요약&lt;/i&gt;&lt;br /&gt;&lt;i&gt;Client 메시지 전송 &amp;rarr; routers/chat.py &amp;rarr; utils/openai_client.py &amp;rarr; OpenAI API 호출 &amp;rarr; AI 답변 수신 &amp;rarr; crud/chat.py &amp;rarr; DB에 대화 저장 &amp;rarr; Client에 답변 전송&lt;/i&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;(1) API 엔드포인트 (routers/chat.py)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753020596672&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
from dependencies import get_current_user
from utils.openai_client import (
    ask_openai_unified,
    get_embedding,
    should_search_long_term_memory
)
from crud.chat import (
    save_chat_history, 
    retrieve_and_rerank_history,
    get_recent_chat_history
)
from schemas.chat import ChatHistoryCreate
from typing import Dict, List

router = APIRouter()

# --- 단기 기억 캐시 (In-memory Cache) ---
chat_cache: Dict[str, List[Dict]] = {}
CACHE_MAX_LENGTH = 10 # 단기 기억으로 저장할 최대 대화 개수

@router.post(&quot;/chat/image&quot;)
async def chat_with_text_or_image(
    message: str = Form(&quot;&quot;),
    image: UploadFile = File(None),
    current_user: dict = Depends(get_current_user)
):
    user_id = current_user['user_id']
    rag_history = [] # 장기 기억 검색 결과를 담을 리스트
    embedding = None # 임베딩은 장기 기억 검색 시에만 생성

    try:
        # 1. 단기 기억 조회
        recent_history = chat_cache.get(user_id, [])

        # 2. 장기 기억 검색 여부 판단 (게이트키퍼)
        if await should_search_long_term_memory(message, recent_history):
            print(f&quot;[DEBUG] User({user_id}): 장기 기억 검색 필요. RAG 파이프라인 실행.&quot;)
            # --- 깊은 검색 경로 (RAG Pipeline) ---
            # a. 원본 질문을 직접 임베딩
            embedding = await get_embedding(message)
            
            # b. 검색 및 재정렬
            rag_history = await retrieve_and_rerank_history(
                user_id=user_id,
                original_question=message, # 재정렬 시에는 원본 질문을 사용
                transformed_embedding=embedding # 검색 시에는 원본 질문의 임베딩을 사용
            )
        else:
            print(f&quot;[DEBUG] User({user_id}): 단기 기억으로 충분. 빠른 응답 실행.&quot;)
            # --- 빠른 경로 (RAG 생략) ---
            # embedding은 None으로 유지

        # 3. 최종 답변 생성
        response = await ask_openai_unified(
            user_message=message,
            image=image,
            recent_history=recent_history,
            rag_history=rag_history
        )

        # 4. 기억 업데이트
        # a. 장기 기억 (DB) 저장
        user_chat_for_db = ChatHistoryCreate(user_id=user_id, role_type=&quot;user&quot;, content=message, embedding=embedding)
        assistant_chat_for_db = ChatHistoryCreate(user_id=user_id, role_type=&quot;assistant&quot;, content=response)
        await save_chat_history(user_chat_for_db)
        await save_chat_history(assistant_chat_for_db)

        # b. 단기 기억 (Cache) 업데이트
        if user_id not in chat_cache:
            chat_cache[user_id] = []
        chat_cache[user_id].append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: message})
        chat_cache[user_id].append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: response})
        
        # 캐시 길이 제한
        if len(chat_cache[user_id]) &amp;gt; CACHE_MAX_LENGTH:
            chat_cache[user_id] = chat_cache[user_id][-CACHE_MAX_LENGTH:]

        return {&quot;response&quot;: response}

    except Exception as e:
        print(f&quot;채팅 처리 중 오류 발생: {e}&quot;)
        raise HTTPException(status_code=500, detail=f&quot;An error occurred: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Trouble Shooting&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗봇을 구현하면서 가장 애를 많이 먹었던 부분이 이 대화 내역 저장 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 단순히 대화 내역을 DB에 저장하고, 그 내역을 가지고 챗봇이 대화를 하게 구현하면 된다고 생각을 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그렇게 짜여진다면, DB에 저장된 데이터가 점점 늘어날수록 엄청난 용량의 DB를 매번 스캔 할 것이고, 메모리도, 토큰도 많이 잡아먹는 아주 비효율적인 구조가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;First&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 내가 떠올렸던 방법은 일단 대화 내역은 모두 DB에 저장을 하고, 사용자의 프롬프트를 받으면 먼저 이 프롬프트가 과거의 데이터가 필요한 질문인지 GPT모델에게 Yes or No로 답을 하게 한다. 그리고 Yes라는 답이 떨어지면, 그 때만 DB에서 관련된 과거의 대화를 가져와서 모델이 해당 대화 내역을 참조하게 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 관련된 데이터를 가져오게 하는 방식은 대화 내역을 저장할때, 해당 대화내역을 텍스트 임베딩 모델을 거쳐서 임베딩 벡터도 함께 저장한 뒤, 나중에 사용자의 프롬프트의 임베딩 벡터와 유사도를 비교하여 가져오게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 또 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 사용자가 &quot;나 오늘 운동을 하다가 어깨에 부상을 당했어.&quot; 라는 대화를 했다고 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;며칠 뒤, 모델에게 &quot;나 저번에 부상 당한 곳 관리를 어떻게 하면 좋을까?&quot; 라고 프롬프트를 작성하여 보내면, 이게 문장 형식의 차이 때문인지 유사도가 너무 낮게 나와서 저 어깨 부상을 당했다는 과거 내역을 모델이 가져오지 못하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;Second&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 유사도를 계산을 하게 한 후, 그것을 다시 reranking 하는 방식을 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유사도가 많이 낮게 나오긴 하지만, 그래도 다른 대화들에 비해서는 유사도가 높게 나오기 때문에 재정렬을 한 상위 3개의 대화 내역을 모델이 참조 할 수 있도록 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 단기 기억으로는 10개의 대화 내역 까지는 모델이 기억을 할 수 있게 하여, 굳이 단기 기억으로 해결 할 수 있는 프롬프트들은 DB를 거치지 않게 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최종적으로 정리하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 프롬프트가 들어오면, 먼저 해당 질문을 GPT모델이 과거 대화내역이 필요한 질문인지(과거 부상 이력 등), 바로 답변 해도 되는 질문인지(ex : 안녕?) 판단하여 Yes or No로 내부적으로 답변을 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗봇은 단기 기억(캐시)과 장기 기억(DB), 두 가지 방식으로 대화를 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 대화 저장 (모든 대화는 두 곳에 동시 저장)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;* 장기 기억 (DB): 모든 대화 내용을 영구적으로 저장. 나중에 검색할 수 있도록 임베딩 벡터도 함께 저장됨.&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;* 단기 기억 (캐시): 가장 최근 대화 몇 개만 임시로 저장하여 빠르게 참조 할 수 있음.&lt;br /&gt;&lt;br /&gt;2. 대화 참조 (상황에 따라 2단계로 참조)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;- 항상 단기 기억 먼저 확인: 어떤 질문이든 일단 가장 최근 대화(단기 기억)를 먼저 확인.&lt;br /&gt;- 필요할 때만 장기 기억 검색:&lt;br /&gt;* 단순한 대화 : 최근 대화만으로 충분하면, 장기 기억(DB)을 보지 않고 바로 빠르고 가벼운 답변을 생성합니다.&lt;br /&gt;* 과거 정보가 필요한 질문 : 과거 정보가 필요하다고 판단되면, 장기 기억(DB)에서 관련 내용을 검색.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;이미지 처리&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 인바디 결과지 분석도 주요 기능 중 하나이기 때문에 이미지도 업로드 할 수 있게 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이미지 처리를 GPT 모델에게 바로 맡기니까, 모델이 텍스트를 잘못 인식하는 부분이 꽤 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 Azure의 Vision 모델로 텍스트를 추출하고, 원본 이미지와 추출한 텍스트를 모두 GPT모델에게 전달하여 텍스트를 잘못 인식할 확률을 최대한을 낮추었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;OCR 코드&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753092667000&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os
import tempfile
from azure.ai.vision.imageanalysis import ImageAnalysisClient
from azure.ai.vision.imageanalysis.models import VisualFeatures
from azure.core.credentials import AzureKeyCredential
from fastapi import UploadFile
from dotenv import load_dotenv

load_dotenv()

# Azure OCR 클라이언트 초기화
VISION_ENDPOINT = os.getenv(&quot;VISION_ENDPOINT&quot;)
VISION_KEY = os.getenv(&quot;VISION_KEY&quot;)

client = ImageAnalysisClient(
    endpoint=VISION_ENDPOINT,
    credential=AzureKeyCredential(VISION_KEY)
)

async def extract_text_from_bytes(image_bytes: bytes) -&amp;gt; str:
    # 전달받은 바이트를 임시파일에 저장
    with tempfile.NamedTemporaryFile(delete=False, suffix=&quot;.jpg&quot;) as tmp:
        tmp.write(image_bytes)
        tmp_path = tmp.name

    try:
        # Azure OCR 분석
        with open(tmp_path, &quot;rb&quot;) as f:
            result = client.analyze(
                image_data=f,
                visual_features=[VisualFeatures.READ]
            )

        if result.read is None or not result.read.blocks:
            return &quot;&quot;

        # OCR 결과 추출
        lines = []
        for block in result.read.blocks:
            for line in block.lines:
                lines.append(line.text)

        return &quot;\n&quot;.join(lines)

    finally:
        # 임시 파일 삭제
        os.remove(tmp_path)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;백엔드 라우터&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1753092857002&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@router.post(&quot;/chat/image&quot;)
async def chat_with_text_or_image(
    message: str = Form(&quot;&quot;),
    image: UploadFile = File(None),
    current_user: dict = Depends(get_current_user)
):
    user_id = current_user['user_id']
    rag_history = []
    embedding = None

    try:
        image_bytes = await image.read() if image else None
        recent_history = chat_cache.get(user_id, [])

        if await should_search_long_term_memory(message, recent_history):
            embedding = await get_embedding(message)
            rag_history = await retrieve_and_rerank_history(
                user_id=user_id,
                original_question=message,
                transformed_embedding=embedding
            )

        # RAG가 실행되지 않아 embedding이 None인 경우, 사용자 메시지를 임베딩합니다.
        if embedding is None and message:
            embedding = await get_embedding(message)

        return StreamingResponse(
            stream_generator(user_id, message, image_bytes, recent_history, rag_history, embedding),
            media_type=&quot;text/event-stream&quot;
        )

    except Exception as e:
        print(f&quot;채팅 처리 중 오류 발생: {e}&quot;)
        raise HTTPException(status_code=500, detail=f&quot;An error occurred: {e}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발이 진행되고 있는 단계라, 추후에 코드나 기능들이 조금씩 바뀔 수도 있지만, 현재까지 진행된 내용을 공유하자면 이렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 코드 작성을 어시스트 해주는 LLM도 너무 잘 되어 있어서, 개발을 이전보다 훨씬 쉽고 빠르게 할 수 있게 된 것 같다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>LLM</category>
      <category>msaischool</category>
      <category>운동</category>
      <category>챗봇</category>
      <category>헬스</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/85</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-2%EC%B0%A8-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2#entry85comment</comments>
      <pubDate>Mon, 21 Jul 2025 19:31:23 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 2차 팀 프로젝트 - 1</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-2%EC%B0%A8-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 한창 팀 프로젝트를 하느라 정신 없는 하루 하루를 보내고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 같이 프로젝트를 진행해보고 싶었던 분이 계셨는데, 감사하게도 그 분께서 먼저 팀 프로젝트를 같이 하지 않겠느냐고 연락을 주셔서 그렇게 팀을 꾸리게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트의 주제는 기나긴 주제 회의 끝에 헬스 케어 분야로 최종 선정이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 주제에 대해 자세히 설명하자면, 헬스나 식단 등을 하려고 하는 사람들이 요즘 꽤나 많이 있을텐데, 처음 입문한 사람이거나, 경험이 많은 사람들도 자신에게 맞춤형 운동 루틴, 식단 등을 케어 해주는 PT 비서 같은 서비스가 있으면 좋을 것 같다는 생각에 주제를 선정하게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 정해진 우리의 서비스 이름은 &lt;b&gt;&quot;Chat GymPT&quot;&lt;/b&gt;.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 운동 루틴, 운동 방법, 식단 등 자신의 운동 생활과 관련된 대부분의 것들을 다 보좌해주는 서비스라고 볼 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;서비스 기획&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 기획 부분은 기존에 IT 기업에서 기획자로 근무 하셨던 팀원의 주도하에 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 처음으로 &lt;b&gt;&quot;&lt;/b&gt;&lt;b&gt;MIRO&quot;&lt;/b&gt;라는 플랫폼을 사용해 보았는데, 팀 프로젝트 할 때 꽤 괜찮은 툴인 것 같았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.18.02.png&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7KvDa/btsPqS5zZxr/MWqQFIU2dBEv1j6lEPTl9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7KvDa/btsPqS5zZxr/MWqQFIU2dBEv1j6lEPTl9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7KvDa/btsPqS5zZxr/MWqQFIU2dBEv1j6lEPTl9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7KvDa%2FbtsPqS5zZxr%2FMWqQFIU2dBEv1j6lEPTl9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;990&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.18.02.png&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.18.22.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8otyA/btsPqk2ADK0/fArbeopu34p0zgZ14l8LT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8otyA/btsPqk2ADK0/fArbeopu34p0zgZ14l8LT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8otyA/btsPqk2ADK0/fArbeopu34p0zgZ14l8LT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8otyA%2FbtsPqk2ADK0%2FfArbeopu34p0zgZ14l8LT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1215&quot; height=&quot;936&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.18.22.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때까지 코드만 줄줄 써왔던 나에게는 이러한 체계적인 기획 단계에 참여해본 것이 너무 신선하고 좋은 경험이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 개발자는 코드만 쓸 줄 알면 되는것이 아니라는 것을 다시금 깨닫는 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.26.08.png&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDlIeG/btsPrqgtk5p/CPDrTsjTTP3YaOdyksVXok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDlIeG/btsPrqgtk5p/CPDrTsjTTP3YaOdyksVXok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDlIeG/btsPrqgtk5p/CPDrTsjTTP3YaOdyksVXok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDlIeG%2FbtsPrqgtk5p%2FCPDrTsjTTP3YaOdyksVXok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1174&quot; height=&quot;642&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.26.08.png&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.26.43.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0n8FY/btsPrdIqpZo/6EaxrPRTksuc5KtXCzcq0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0n8FY/btsPrdIqpZo/6EaxrPRTksuc5KtXCzcq0k/img.png&quot; data-alt=&quot;서비스 플로우&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0n8FY/btsPrdIqpZo/6EaxrPRTksuc5KtXCzcq0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0n8FY%2FbtsPrdIqpZo%2F6EaxrPRTksuc5KtXCzcq0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1444&quot; height=&quot;782&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.26.43.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서비스 플로우&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 쪽은 대부분 내가 담당하게 될거라, 나와 함께 서비스 플로우를 구체적으로 설계하였다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;822&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/llkcJ/btsPpjKfzuz/FEmWkdrVjgn81OV0DKkdlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/llkcJ/btsPpjKfzuz/FEmWkdrVjgn81OV0DKkdlk/img.png&quot; data-alt=&quot;서비스 화면 정의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/llkcJ/btsPpjKfzuz/FEmWkdrVjgn81OV0DKkdlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FllkcJ%2FbtsPpjKfzuz%2FFEmWkdrVjgn81OV0DKkdlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1215&quot; height=&quot;822&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;822&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서비스 화면 정의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 기간안에 완성을 해야 하기 때문에 디자인 보다는 기능 구현에 중점을 두고 서비스 화면의 흐름을 정리하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현하고 싶은 기능은 많았지만, 우선은 중심이 되는 기능 부터 먼저 구현을 하기로 하고, 시간이 남으면 하나씩 기능을 덧 붙이기로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;프로젝트 진행 관리&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;miro는 주로 프로젝트 진행하면서 필요한 기능 정리, 다이어그램 등의 용도로 사용하였고, 회의, 프로젝트 진행 관리 등은 notion으로 따로 관리를 하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1525&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u0frt/btsPpgtfnbF/sFeJKKiL33UNr0jMKbTylK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u0frt/btsPpgtfnbF/sFeJKKiL33UNr0jMKbTylK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u0frt/btsPpgtfnbF/sFeJKKiL33UNr0jMKbTylK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu0frt%2FbtsPpgtfnbF%2FsFeJKKiL33UNr0jMKbTylK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1525&quot; height=&quot;836&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1525&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 시작 전 각자 자신의 스킬셋이 어떻게 되는지, 얼마나 잘 다루는지, 목표한 바가 무엇인지 등을 작성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각자 자신이 잘 할 수 있는 것, 그리고 나중에 목표하는 것과 어느정도 일치하도록 역할을 배분하기 위함이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.34.26.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sRlco/btsPpQgrW9a/KeATnoClNkPyS0AZ6TlSxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sRlco/btsPpQgrW9a/KeATnoClNkPyS0AZ6TlSxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sRlco/btsPpQgrW9a/KeATnoClNkPyS0AZ6TlSxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsRlco%2FbtsPpQgrW9a%2FKeATnoClNkPyS0AZ6TlSxK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1504&quot; height=&quot;792&quot; data-filename=&quot;스크린샷 2025-07-19 오후 3.34.26.png&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 기간은 약 2주 정도로, 생각보다 짧은 기간이기에 처음부터 시간 배분을 철저히 하고 프로젝트에 임하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 매일 아침 데일리 미팅을 진행하여 각자가 맡은 부분에 대해 공유할 내용들을 공유하고, 다 같이 의논해야 할 사항에 대해서 회의를 하는 시간을 가졌다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트 시작한지 3일째인데, 생각보다 진행속도가 빨라서 기존에 예상했던 것 보다 기능을 더 많이 구현할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 현재까지 개발한 내용은 다음 포스트에서 자세히 다룰 예정이다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>Azure</category>
      <category>Microsoft</category>
      <category>msaischool</category>
      <category>PROJECT</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/84</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-2%EC%B0%A8-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1#entry84comment</comments>
      <pubDate>Sat, 19 Jul 2025 15:46:17 +0900</pubDate>
    </item>
    <item>
      <title>문서 작성 AI 서비스 만들기</title>
      <link>https://cases.tistory.com/entry/%EB%AC%B8%EC%84%9C-%EC%9E%91%EC%84%B1-AI-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;다들 가끔 공공기관 등에 제출해야 하는 서류(신청서, 증명서 등)를 작성할때, 매우 스트레스를 받았던 경험이 한 번씩은 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 전입 신고를 해야 한다고 가정을 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 먼저 전입 신고시 필요한 서류들이 어떤 것이 있는지 찾아야 할 것이고, 해당 서류들을 어디서 뗄 수 있는지, 양식을 받아와서 본인이 작성을 해야 하는 서류들도 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리도 이런 일들이 있을때 귀찮고 번거로운데, 우리나라에 사는 외국인, 또는 나이 드신 노인분들은 얼마나 힘드실까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 생각에 신청서, 문서 등을 작성할 수 있데 도와주는 AI 서비스를 만들어보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소규모로 간단하게 몇 시간 동안 초안 정도로만 만들어본거라 아직 엄청 거창한 서비스를 개발한 것은 아니지만, 추후에 충분히 확장이 가능한 주제라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure의 Document Intelligence는 문서, 이미지 등을 분석하고 처리할 수 있도록 해주는 클라우드 기반 서비스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Document Intelligence 와 이전 포스팅에서도 다뤘던 OpenAI를 함께 사용하여 개발했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Azure Document Intelligene 사용 방법&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 리소스를 만든 후 Studio로 이동하면 아래와 같은 여러 분석 모델들이 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-30 오후 11.15.49.png&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB1vsB/btsOY7CbEcH/jbepkyTKWi1yh0exRzuCf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB1vsB/btsOY7CbEcH/jbepkyTKWi1yh0exRzuCf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB1vsB/btsOY7CbEcH/jbepkyTKWi1yh0exRzuCf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB1vsB%2FbtsOY7CbEcH%2FjbepkyTKWi1yh0exRzuCf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1149&quot; height=&quot;1158&quot; data-filename=&quot;스크린샷 2025-06-30 오후 11.15.49.png&quot; data-origin-width=&quot;1149&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 모델별 사용법은 다 비슷해서, 대표적으로 OCR/Read 를 사용하여 로컬에서 구현해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 구현하기 전, 포털에서 기능을 먼저 사용해보면, 문서의 각 텍스트들을 추출하여 분석을 한 결과를 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-30 오후 11.23.49.png&quot; data-origin-width=&quot;2332&quot; data-origin-height=&quot;1097&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCKXQ/btsOY7hR938/MOhS3bV8n7btVF6pr17KdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCKXQ/btsOY7hR938/MOhS3bV8n7btVF6pr17KdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCKXQ/btsOY7hR938/MOhS3bV8n7btVF6pr17KdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCKXQ%2FbtsOY7hR938%2FMOhS3bV8n7btVF6pr17KdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2332&quot; height=&quot;1097&quot; data-filename=&quot;스크린샷 2025-06-30 오후 11.23.49.png&quot; data-origin-width=&quot;2332&quot; data-origin-height=&quot;1097&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 이미지는 물론이고, 여러장의 PDF를 분석하는 것도 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 모델을 로컬에서 호출하여 사용하려면 Endpoint와 API Key 등이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 방법은 Azure 공식 문서를 참고 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&amp;amp;pivots=programming-language-rest-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&amp;amp;pivots=programming-language-rest-api&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1751293550877&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;빠른 시작: 문서 인텔리전스 클라이언트 라이브러리 - Azure AI services&quot; data-og-description=&quot;문서 인텔리전스 SDK 또는 REST API를 사용하여 문서에서 주요 데이터 및 구조 요소를 추출하는 양식 처리 앱을 만듭니다.&quot; data-og-host=&quot;learn.microsoft.com&quot; data-og-source-url=&quot;https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&amp;amp;pivots=programming-language-rest-api&quot; data-og-url=&quot;https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bQpHKY/hyZcjUmmbN/KIlaOhPRIcXfZTOOoKhxe1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bo2d1G/hyZbvHiPuK/whK6KEsCdW78L81RoNPp9K/img.png?width=2048&amp;amp;height=1420&amp;amp;face=0_0_2048_1420,https://scrap.kakaocdn.net/dn/8ND03/hyZcqeTWZ4/lyfKgjnNzBKVoUBkBVXrS1/img.png?width=1262&amp;amp;height=805&amp;amp;face=0_0_1262_805&quot;&gt;&lt;a href=&quot;https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&amp;amp;pivots=programming-language-rest-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn.microsoft.com/ko-kr/azure/ai-services/document-intelligence/quickstarts/get-started-sdks-rest-api?view=doc-intel-4.0.0&amp;amp;pivots=programming-language-rest-api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bQpHKY/hyZcjUmmbN/KIlaOhPRIcXfZTOOoKhxe1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bo2d1G/hyZbvHiPuK/whK6KEsCdW78L81RoNPp9K/img.png?width=2048&amp;amp;height=1420&amp;amp;face=0_0_2048_1420,https://scrap.kakaocdn.net/dn/8ND03/hyZcqeTWZ4/lyfKgjnNzBKVoUBkBVXrS1/img.png?width=1262&amp;amp;height=805&amp;amp;face=0_0_1262_805');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;빠른 시작: 문서 인텔리전스 클라이언트 라이브러리 - Azure AI services&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;문서 인텔리전스 SDK 또는 REST API를 사용하여 문서에서 주요 데이터 및 구조 요소를 추출하는 양식 처리 앱을 만듭니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn.microsoft.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서를 보면 알 수 있듯이, Endpoint와 Key값은 Document Intelligence 리소스에 가면 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;1117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ye7TE/btsOXf2wBro/PiFgRjTqE5ZJla5YxvKL81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ye7TE/btsOXf2wBro/PiFgRjTqE5ZJla5YxvKL81/img.png&quot; data-alt=&quot;Document Intelligence 리소스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ye7TE/btsOXf2wBro/PiFgRjTqE5ZJla5YxvKL81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fye7TE%2FbtsOXf2wBro%2FPiFgRjTqE5ZJla5YxvKL81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1069&quot; height=&quot;1117&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;1117&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Document Intelligence 리소스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;OCR 결과와 PDF 필드를 GPT에 전송&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 만들려고 생각했던 것은 사용자가 &quot;OO신청서를 작성하려고 해&quot; 라고 요청을 하면, 보유하고 있는 여러 양식 파일들 중 요청에 맞는 파일을 불러와서 질의응답을 진행하는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금 당장은 간단한 서비스를 만들어보는 것이기에, 사용자가 작성해야 할 문서 파일을 업로드 하면, 해당 파일 OCR을 진행하고, 필요한 정보에 대한 질문을 사용자에게 하는 것으로 구현을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 파일을 업로드 하면, 해당 파일을 Document Intelligence의 OCR/Read 모델로 텍스트 추출을 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추출된 텍스트와 PDF 추출 필드를 모두 GPT에 전송하여, GPT는 사용자로 부터 받아야 하는 정보(예 : 이름, 주소, 전화번호 등)에 대한 질문 리스트를 생성하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1751464090618&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def handle_pdf_upload(file_path):
    global field_list, field_index, field_answers, inferred_answers, uploaded_pdf_path, pdf_fields
    
    if not file_path:
        return &quot;PDF 파일을 선택해주세요.&quot;, None
    
    print(f&quot;PDF 업로드됨: {file_path}&quot;)
    uploaded_pdf_path = file_path
    
    # PDF 필드명 추출
    pdf_fields = get_pdf_fields(file_path)
    print(f&quot;추출된 PDF 필드들: {pdf_fields}&quot;)
    
    # OCR 텍스트 추출
    ocr_text = extract_text_from_pdf(file_path)
    if not ocr_text:
        return &quot;OCR 텍스트 추출에 실패했습니다. 다른 PDF 파일을 시도해보세요.&quot;, None
    
    field_list = generate_questions_from_text(ocr_text, pdf_fields)
    if not field_list:
        return &quot;질문 생성에 실패했습니다.&quot;, None
    
    field_index = 0
    field_answers = {}
    inferred_answers = {} 
    
    fields_info = f&quot;발견된 PDF 필드: {len(pdf_fields)}개\n&quot; if pdf_fields else &quot;PDF 필드를 감지하지 못했습니다. 텍스트 기반으로 진행합니다.\n&quot;
    
    first_question_text, _ = field_list[0]
    
    return f&quot;문서 분석이 완료되었습니다.\n{fields_info}총 {len(field_list)}개의 질문이 있습니다.\n\n첫 번째 질문: {first_question_text}&quot;, None&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT가 사용자에게 질문을 할때, 기존에 답변할 때 처럼 장황하고 길게 한다면, 사용자가 질문을 제대로 이해할 수가 없거나, 올바른 답변을 할 수 없을 수 있기 때문에 최대한 간결하게 필요한 질문만 하도록 나의 의도를 Gemini에게 공유하여 함께 프롬프트를 작성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1751464801856&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; prompt = f&quot;&quot;&quot;
다음은 문서에서 OCR로 추출한 텍스트와 PDF의 실제 필드명들입니다.
사용자가 입력해야 할 항목들을 파악하고 질문을 생성해주세요.

문서 내용:
{ocr_text}

PDF 필드명들:
{fields_info}

요구사항:
1. 문서의 빈칸이나 입력 필드를 분석하여 적절한 질문을 생성하세요
2. 각 질문은 명확하고 이해하기 쉽게 작성하세요
3. 한 줄에 하나의 질문만 작성하세요
4. 질문 앞에 번호나 특수문자를 붙이지 마세요
5. **각 질문은 해당하는 PDF 필드명을 매핑해야 합니다. 만약 PDF 필드명에 직접적으로 매칭되는 정보가 없으면 'auto_infer'라고 필드명을 지정하세요.**
6. 형식: &quot;질문내용|필드명&quot; (예: &quot;이름을 입력해주세요|name_field&quot; 또는 &quot;생년월일을 입력해주세요|auto_infer&quot;)

질문들을 생성해주세요:
&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;답변에 대한 AI 모델 추론&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 양식의 모든 빈칸에 대해서 하나씩 다 질문을 한다면, 본인이 직접 작성하는 것이랑 별로 다를게 없지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 사용자가 답변한 정보들은 모델이 모두 저장을 하고, 추후에 또 새로운 문서를 작성하게 됐을때, 모델에 저장되어 있는 데이터에 대한 정보는 모델이 직접 다 입력을 하고, 더 나아가 추론을 통해 작성할 수 있는 란도 모두 AI가 다 작성을 할 수 있게 하는 것이 목표였다.( 예 : 사용자가 &quot;현재 직장명이 무엇인가요?&quot;라는 질문에 답을 하였음 -&amp;gt; 나중에 현재 재직 상태를 묻는 란이 있다면 그 칸은 당연히 &quot;재직중&quot; 이라고 답을 할 수 있을 것이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 내가 아무리 테스트를 해봐도, 비슷한 질문에 대해서 자동으로 기입을 해주질 않길래 뭐가 잘못 됐는지 찾아봤는데, 아직 정확한 원인은 찾지 못했다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 Azure 서비스의 토큰 제한 때문인걸로 추정을 하고 있지만, 정확한 원인은 추후 더 개발을 하면서 찾아보려고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1751465575345&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def handle_user_text(user_text):
    global field_index, field_list, field_answers, inferred_answers, uploaded_pdf_path, pdf_fields

    if not field_list and not pdf_fields:
        return &quot;먼저 PDF 파일을 업로드하고 분석을 시작해주세요.&quot;, None

    if field_index &amp;gt;= len(field_list):
        if uploaded_pdf_path:
            final_field_values = map_answers_to_fields(field_list, field_answers, inferred_answers)
            filled_pdf = generate_filled_pdf(uploaded_pdf_path, final_field_values)
            if filled_pdf:
                return &quot;모든 항목이 완료되었습니다! 작성된 문서를 다운로드하세요.&quot;, filled_pdf
            else:
                return &quot;문서 생성 중 오류가 발생했습니다.&quot;, None
        else:
            return &quot;원본 PDF 파일을 찾을 수 없습니다.&quot;, None

    current_question_info = field_list[field_index]
    current_question_text, current_field_name = current_question_info
    
    if not user_text or not user_text.strip():
        return f&quot;답변을 입력해주세요.\n현재 질문: {current_question_text}&quot;, None
    else:
        field_answers[current_question_text] = user_text.strip()
        field_index += 1
        print(f&quot;사용자 답변 저장: {current_question_text} -&amp;gt; {user_text.strip()}&quot;)
        print(f&quot;현재 진행상황 (사용자 질문): {field_index}/{len(field_list)}&quot;)


    inferred_count_this_turn = 0 
    if uploaded_pdf_path and pdf_fields:

        previous_inferred_keys = set(inferred_answers.keys())

        current_mapped_user_answers = map_answers_to_fields(field_list, field_answers, {}) 
        ocr_text_for_inference = extract_text_from_pdf(uploaded_pdf_path) 
        
        inferred_answers_new = request_gpt_inference(
            ocr_text_for_inference, 
            pdf_fields, 
            current_mapped_user_answers 
        )
        inferred_answers.update(inferred_answers_new) 
        print(f&quot;현재 AI 추론된 답변: {inferred_answers}&quot;)

        # 새로 추론된 필드 개수 계산
        current_inferred_keys = set(inferred_answers.keys())
        newly_inferred_keys = current_inferred_keys - previous_inferred_keys
        inferred_count_this_turn = len(newly_inferred_keys)
        print(f&quot;이번 턴에 새로 추론된 필드 개수: {inferred_count_this_turn}&quot;)


    # --- 다음 질문이 있는지 확인 및 AI 자동 건너뛰기 로직 ---
    response_message = &quot;답변이 저장되었습니다.&quot;
    if inferred_count_this_turn &amp;gt; 0:
        response_message += f&quot;\n\n  AI가 추론을 통해 {inferred_count_this_turn}개의 질문에 대한 답변을 추가로 작성하였습니다.&quot;

    while field_index &amp;lt; len(field_list):
        next_question_text, next_field_name = field_list[field_index]
        
        if next_question_text in field_answers or \
           (next_field_name in pdf_fields and next_field_name in inferred_answers):
            print(f&quot;질문 '{next_question_text}' (필드: {next_field_name})은(는) 이미 처리되었으므로 건너뜁니다.&quot;)
            field_index += 1
        else:
            # 아직 답변되지 않은 (사용자 질문이 필요한) 질문이 남아있음
            return f&quot;{response_message}\n\n다음 질문: {next_question_text}&quot;, None

    # 모든 질문이 완료되었거나 AI가 모두 채울 수 있어서 더 이상 사용자 질문이 필요 없는 경우
    if uploaded_pdf_path:
        final_field_values = map_answers_to_fields(field_list, field_answers, inferred_answers)
        filled_pdf = generate_filled_pdf(uploaded_pdf_path, final_field_values)
        if filled_pdf:
            return &quot;모든 항목이 완료되었습니다! 작성된 문서를 다운로드하세요.&quot;, filled_pdf
        else:
            return &quot;문서 생성 중 오류가 발생했습니다.&quot;, None
    else:
        return &quot;원본 PDF 파일을 찾을 수 없습니다.&quot;, None&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Gradio 화면 구성&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 하다보니까 기능 하나하나를 추가 할때마다 오류가 발생해서, 기존에는 STT, TTS 기능을 모두 탑재 했었지만, 현재는 빠져있는 상태이다...&lt;/p&gt;
&lt;pre id=&quot;code_1751465764805&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;with gr.Blocks(title=&quot;문서 작성 도우미&quot;) as demo:
    gr.Markdown(&quot;# 문서 자동 작성 도우미&quot;)
    gr.Markdown(&quot;PDF 파일을 업로드하면 AI가 빈칸을 파악하고 질문을 통해 문서를 작성해드립니다.&quot;)
    
    with gr.Row():
        with gr.Column():
            pdf_file = gr.File(label=&quot;PDF 업로드&quot;, file_types=[&quot;.pdf&quot;])
            upload_button = gr.Button(&quot;  분석 시작&quot;, variant=&quot;primary&quot;)
            
        with gr.Column():
            chatbot_text = gr.Textbox(
                label=&quot;AI 질문&quot;, 
                interactive=False, 
                lines=4,
                placeholder=&quot;PDF를 업로드하고 분석을 시작하면 질문이 표시됩니다.&quot;
            )
    
    with gr.Row():
        user_text_input = gr.Textbox(
            label=&quot;답변 입력&quot;, 
            placeholder=&quot;여기에 답변을 입력하고 Enter를 누르세요.&quot;,
            lines=2
        )
        submit_button = gr.Button(&quot;답변 제출&quot;, variant=&quot;secondary&quot;)
    
    with gr.Row():
        download_link = gr.File(label=&quot;  작성된 PDF 다운로드&quot;, interactive=False)
    
    # 이벤트 연결
    upload_button.click(
        handle_pdf_upload, 
        inputs=[pdf_file], 
        outputs=[chatbot_text, download_link]
    )
    
    user_text_input.submit(
        handle_user_text, 
        inputs=[user_text_input], 
        outputs=[chatbot_text, download_link]
    ).then(
        lambda: &quot;&quot;, 
        outputs=[user_text_input]
    )
    
    submit_button.click(
        handle_user_text, 
        inputs=[user_text_input], 
        outputs=[chatbot_text, download_link]
    ).then(
        lambda: &quot;&quot;, 
        outputs=[user_text_input]
    )

if __name__ == &quot;__main__&quot;:
    demo.launch(allowed_paths=[&quot;.&quot;])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-07-02 오전 9.21.14.png&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmyT5y/btsO08aQTYM/SkdXTzeKTBshVgnhb7Lob1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmyT5y/btsO08aQTYM/SkdXTzeKTBshVgnhb7Lob1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmyT5y/btsO08aQTYM/SkdXTzeKTBshVgnhb7Lob1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmyT5y%2FbtsO08aQTYM%2FSkdXTzeKTBshVgnhb7Lob1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1884&quot; height=&quot;659&quot; data-filename=&quot;스크린샷 2025-07-02 오전 9.21.14.png&quot; data-origin-width=&quot;1884&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 AI와 사용자는 질문을 주고 받고, 최종 완성된 양식의 파일을 다운받을 수 있게 제공해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발 중인 단계라 조금은(?) 삐걱 거리는 곳이 많고, 완성도도 현저히 떨어지지만, 내가 생각해낸 아이디어가 너무 좋아서 조금은 급하게 포스팅을 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 수정이 되고, 업그레이드 되어서 완전한 모습을 갖추게 된다면 다시 포스팅을 할 예정이다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>Azure</category>
      <category>문서작성ai</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/83</guid>
      <comments>https://cases.tistory.com/entry/%EB%AC%B8%EC%84%9C-%EC%9E%91%EC%84%B1-AI-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry83comment</comments>
      <pubDate>Wed, 2 Jul 2025 23:26:38 +0900</pubDate>
    </item>
    <item>
      <title>서울 맛집 챗봇 만들기 : Azure OpenAI + RAG</title>
      <link>https://cases.tistory.com/entry/%EC%84%9C%EC%9A%B8-%EB%A7%9B%EC%A7%91-%EC%B1%97%EB%B4%87-%EB%A7%8C%EB%93%A4%EA%B8%B0-Azure-OpenAI-RAG</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT나 Gemini, Perplexity 등의 챗봇을 사용하다 보면, 가끔 있지도 않은 사실을 진짜인 것 처럼 대답을 하거나, 특정 분야에 대한 지식을 보유하고 있는 모델이 있었으면 하는 생각이 들곤 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 LLM모델을 내가 직접 개발하기엔 어렵지만, 나에게 딱 맞춘 모델로 튜닝하는 것은 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Azure OpenAI로 여러가지 데이터를 활용하여 RAG를 적용시킨 챗봇을 개발하는 과정을 담아보았다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;RAG(검색 증강 생성)&lt;br /&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;RAG는 Retrieval-Augmented Generation의 약자로, 기존의 대규모 언어 모델(LLM)을 확장하여, 주어진 컨텍스트나 질문에 대해 더욱 정확하고 풍부한 정보를 제공하는 방법이다. 모델이 학습 데이터에 포함되지 않은 외부 데이터를 실시간으로 검색하고, 이를 바탕으로 답변을 생성하는 과정을 포함한다. 특히 환각(&lt;span style=&quot;background-color: #ffffff; color: #001d35; text-align: start;&quot;&gt;Hallucination)&lt;/span&gt;을 방지하고, 모델이 최신 정보를 반영하거나 더 넓은 지식을 활용할 수 있게 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 업로드&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure portal에 접속 후, 나의 리소스 그룹으로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;storage 리소스를 생성을 해주고, 좌측 사이드 바의 데이터 스토리지 &amp;gt; 컨테이너에 접속.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너를 생성해준 후, RAG에 활용할 데이터를 업로드 해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.00.33.png&quot; data-origin-width=&quot;2904&quot; data-origin-height=&quot;1402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/njcvB/btsOK5yJyXh/A24ouEoDxcEVyImuYGmjNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/njcvB/btsOK5yJyXh/A24ouEoDxcEVyImuYGmjNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/njcvB/btsOK5yJyXh/A24ouEoDxcEVyImuYGmjNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnjcvB%2FbtsOK5yJyXh%2FA24ouEoDxcEVyImuYGmjNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2904&quot; height=&quot;1402&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.00.33.png&quot; data-origin-width=&quot;2904&quot; data-origin-height=&quot;1402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.03.07.png&quot; data-origin-width=&quot;2380&quot; data-origin-height=&quot;820&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H0icZ/btsOLDaPCFT/Us6jfKC1bG6KQ29DC8Lqh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H0icZ/btsOLDaPCFT/Us6jfKC1bG6KQ29DC8Lqh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H0icZ/btsOLDaPCFT/Us6jfKC1bG6KQ29DC8Lqh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH0icZ%2FbtsOLDaPCFT%2FUs6jfKC1bG6KQ29DC8Lqh1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2380&quot; height=&quot;820&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.03.07.png&quot; data-origin-width=&quot;2380&quot; data-origin-height=&quot;820&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터는 JSON, CSV, 심지어 PDF 더라도 괜찮다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 모델에게 인덱싱을 해주기 전, 텍스트 임베딩 모델을 활용할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 활용한 데이터는 '서울 열린데이터 광장'에서 '서울 관광 음식' 데이터를 가져와 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://data.seoul.go.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://data.seoul.go.kr/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750486614025&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;열린데이터광장 메인&quot; data-og-description=&quot;데이터분류,데이터검색,데이터활용&quot; data-og-host=&quot;data.seoul.go.kr&quot; data-og-source-url=&quot;https://data.seoul.go.kr/&quot; data-og-url=&quot;https://data.seoul.go.kr&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bGggD0/hyZbANHTei/i0HhhJFQWKG6fqpkJMYma0/img.jpg?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291,https://scrap.kakaocdn.net/dn/ciznHg/hyZci7fKvS/Cc4vKXoexWxlojHM9jsgVk/img.png?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291&quot;&gt;&lt;a href=&quot;https://data.seoul.go.kr/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://data.seoul.go.kr/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bGggD0/hyZbANHTei/i0HhhJFQWKG6fqpkJMYma0/img.jpg?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291,https://scrap.kakaocdn.net/dn/ciznHg/hyZci7fKvS/Cc4vKXoexWxlojHM9jsgVk/img.png?width=740&amp;amp;height=291&amp;amp;face=0_0_740_291');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;열린데이터광장 메인&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;데이터분류,데이터검색,데이터활용&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;data.seoul.go.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 Search&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 스토리지에 업로드한 데이터를 인덱싱 해줄 단계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱싱을 하는 방법은 JSON 편집을 통해 내가 직접 인덱싱 방법을 설정해줄 수도 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 일일이 인덱싱 편집, 인덱서 생성 등의 과정을 거치지 않고, 임베딩 모델을 통해 모든 과정을 자동으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, PDF 파일 같은 경우에는 JSON 편집에 어려움이 있기 때문에 임베딩 모델을 활용하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에는 'Azure AI Search' 리소스를 사용해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.33.53.png&quot; data-origin-width=&quot;2514&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHFiDG/btsOLE1VvIU/FEIzMv5OKSSq2nlvANBHy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHFiDG/btsOLE1VvIU/FEIzMv5OKSSq2nlvANBHy1/img.png&quot; data-alt=&quot;리소스 그룹에서 검색 후 리소스 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHFiDG/btsOLE1VvIU/FEIzMv5OKSSq2nlvANBHy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHFiDG%2FbtsOLE1VvIU%2FFEIzMv5OKSSq2nlvANBHy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2514&quot; height=&quot;1156&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.33.53.png&quot; data-origin-width=&quot;2514&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;리소스 그룹에서 검색 후 리소스 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인이 임베딩 모델을 사용하지 않고, 직접 데이터 인덱싱 방법 등을 설정하고 싶으면 '인덱스 추가'를 클릭하여 진행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 내가 설명한 것 처럼 임베딩 모델을 사용하여 인덱싱 과정을 진행하려면 먼저 Azure AI Foundry에서 임베딩 모델을 생성해주도록 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 text-embedding-3-small 모델을 사용하였다. 해당 모델이 가격 대비 꽤 성능이 잘 나오는 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.42.31.png&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uscOT/btsOMdpa6GL/9bFALQqlknklUYb7dBx200/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uscOT/btsOMdpa6GL/9bFALQqlknklUYb7dBx200/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uscOT/btsOMdpa6GL/9bFALQqlknklUYb7dBx200/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuscOT%2FbtsOMdpa6GL%2F9bFALQqlknklUYb7dBx200%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2008&quot; height=&quot;1470&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.42.31.png&quot; data-origin-width=&quot;2008&quot; data-origin-height=&quot;1470&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.40.22.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;1474&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wG7Ed/btsOLC33kOV/nNQ4g6b6GWrIKUMDM6cuSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wG7Ed/btsOLC33kOV/nNQ4g6b6GWrIKUMDM6cuSk/img.png&quot; data-alt=&quot;텍스트 임베딩 모델 생성 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wG7Ed/btsOLC33kOV/nNQ4g6b6GWrIKUMDM6cuSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwG7Ed%2FbtsOLC33kOV%2FnNQ4g6b6GWrIKUMDM6cuSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2334&quot; height=&quot;1474&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.40.22.png&quot; data-origin-width=&quot;2334&quot; data-origin-height=&quot;1474&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;텍스트 임베딩 모델 생성 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩 모델을 생성했다면, 이전에 생성했던 AI Search 리소스로 접속하여, 데이터 가져오기 및 벡터화를 클릭.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2894&quot; data-origin-height=&quot;1408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SveQo/btsOK6dmQfS/T5rd9rztq07rOfNRTANQOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SveQo/btsOK6dmQfS/T5rd9rztq07rOfNRTANQOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SveQo/btsOK6dmQfS/T5rd9rztq07rOfNRTANQOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSveQo%2FbtsOK6dmQfS%2FT5rd9rztq07rOfNRTANQOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2894&quot; height=&quot;1408&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2894&quot; data-origin-height=&quot;1408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.47.39.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;614&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsaeKv/btsOMTRhqS9/UjLHLkSWA3tUsYH53N4yHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsaeKv/btsOMTRhqS9/UjLHLkSWA3tUsYH53N4yHk/img.png&quot; data-alt=&quot;RAG 클릭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsaeKv/btsOMTRhqS9/UjLHLkSWA3tUsYH53N4yHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsaeKv%2FbtsOMTRhqS9%2FUjLHLkSWA3tUsYH53N4yHk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1474&quot; height=&quot;614&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.47.39.png&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;614&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RAG 클릭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트 벡터화 부분에서 이전에 생성해둔 텍스트 임베딩 모델을 선택하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2198&quot; data-origin-height=&quot;930&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4XRj3/btsOMeO9tte/muH7AKBCLelVhVSowxskJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4XRj3/btsOMeO9tte/muH7AKBCLelVhVSowxskJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4XRj3/btsOMeO9tte/muH7AKBCLelVhVSowxskJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4XRj3%2FbtsOMeO9tte%2FmuH7AKBCLelVhVSowxskJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2198&quot; height=&quot;930&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2198&quot; data-origin-height=&quot;930&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터의 크기에 따라서 인덱싱 과정이 오래 걸릴 수도 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 꽤 큰 데이터를 사용했더니, 한 10분 가량 소요된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정이 완료되면 자동으로 인덱스와 인덱서가 생성이 된 걸 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.54.47.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BBCbO/btsOLhsqacw/XPVesHCxd4aKGNMQwEN6pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BBCbO/btsOLhsqacw/XPVesHCxd4aKGNMQwEN6pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BBCbO/btsOLhsqacw/XPVesHCxd4aKGNMQwEN6pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBBCbO%2FbtsOLhsqacw%2FXPVesHCxd4aKGNMQwEN6pk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2924&quot; height=&quot;826&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.54.47.png&quot; data-origin-width=&quot;2924&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.55.05.png&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXHtHk/btsOMaze4c5/JcyKco5Y4i42uGGUeG3JM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXHtHk/btsOMaze4c5/JcyKco5Y4i42uGGUeG3JM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXHtHk/btsOMaze4c5/JcyKco5Y4i42uGGUeG3JM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXHtHk%2FbtsOMaze4c5%2FJcyKco5Y4i42uGGUeG3JM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2938&quot; height=&quot;764&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.55.05.png&quot; data-origin-width=&quot;2938&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;RAG 모델 테스트&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 다시 AI Foundry로 접속하여 Add Data 를 클릭하여 생성해둔 인덱스로 데이터를 모델에게 인덱싱 시켜준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.59.59.png&quot; data-origin-width=&quot;2328&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzi1Me/btsOKIqj6aq/psgOUfLl6dKiSXGV4szAm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzi1Me/btsOKIqj6aq/psgOUfLl6dKiSXGV4szAm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzi1Me/btsOKIqj6aq/psgOUfLl6dKiSXGV4szAm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzi1Me%2FbtsOKIqj6aq%2FpsgOUfLl6dKiSXGV4szAm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2328&quot; height=&quot;1500&quot; data-filename=&quot;스크린샷 2025-06-21 오후 3.59.59.png&quot; data-origin-width=&quot;2328&quot; data-origin-height=&quot;1500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 데이터를 기반하여 대답을 하는지 확인하기 위해 도봉구 맛집을 추천해달라고 요청해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.09.00.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beDGpW/btsOLBjMym2/Y6FVUNS069VYLwM6hwPk20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beDGpW/btsOLBjMym2/Y6FVUNS069VYLwM6hwPk20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beDGpW/btsOLBjMym2/Y6FVUNS069VYLwM6hwPk20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeDGpW%2FbtsOLBjMym2%2FY6FVUNS069VYLwM6hwPk20%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1376&quot; height=&quot;936&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.09.00.png&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맛집들 추천과 함께 자세한 정보들을 제공해주고, 참고한 reference가 무엇인지도 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;로컬 환경으로 RAG 모델 호출하여 사용하기&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chat playground에서 'View code'를 클릭하면 로컬 환경으로 나의 모델을 호출할 수 있는 Sample Code가 제공된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 code를 그대로 가져와 endpoint, API Key 등을 나의 키로 입력해주고, 코드를 조금만 수정해주면 로컬에서 사용할 수 있는 코드를 금방 작성할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.19.40.png&quot; data-origin-width=&quot;2544&quot; data-origin-height=&quot;1492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crRAJA/btsOKGeVs1Q/dh1PCKTewMWlhPCuZh4nfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crRAJA/btsOKGeVs1Q/dh1PCKTewMWlhPCuZh4nfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crRAJA/btsOKGeVs1Q/dh1PCKTewMWlhPCuZh4nfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrRAJA%2FbtsOKGeVs1Q%2Fdh1PCKTewMWlhPCuZh4nfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2544&quot; height=&quot;1492&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.19.40.png&quot; data-origin-width=&quot;2544&quot; data-origin-height=&quot;1492&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Endpoint와 API Key를 입력해주고, prompt를 내 input을 받도록 수정해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;1282&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sFDpf/btsOLALYBAz/2YIPMtoVkH127GOkjK3hek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sFDpf/btsOLALYBAz/2YIPMtoVkH127GOkjK3hek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sFDpf/btsOLALYBAz/2YIPMtoVkH127GOkjK3hek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsFDpf%2FbtsOLALYBAz%2F2YIPMtoVkH127GOkjK3hek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2166&quot; height=&quot;1282&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2166&quot; data-origin-height=&quot;1282&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 기본 Sample Code에는 답변을 completition.to_json()으로 출력을 하게 되어있어서, 출력 결과를 확인한 후 내가 원하는 답변만 출력하도록 수정할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.29.30.png&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;888&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dI1Izj/btsOKQBVimP/3yXfVkwY4ZfszZ7q3KOUjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dI1Izj/btsOKQBVimP/3yXfVkwY4ZfszZ7q3KOUjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dI1Izj/btsOKQBVimP/3yXfVkwY4ZfszZ7q3KOUjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdI1Izj%2FbtsOKQBVimP%2F3yXfVkwY4ZfszZ7q3KOUjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2240&quot; height=&quot;888&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.29.30.png&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;888&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 필요한 답변은&amp;nbsp;&lt;b&gt;completion &amp;gt; choices[0] &amp;gt; message &amp;gt; content&amp;nbsp;&lt;/b&gt;경로임을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 참조한 reference도 함께 출력하게 코드를 수정해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.35.07.png&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1346&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVQMyj/btsOK29Q4Xx/20Ih60PvyGMIGK8a8k1Hc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVQMyj/btsOK29Q4Xx/20Ih60PvyGMIGK8a8k1Hc0/img.png&quot; data-alt=&quot;최종 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVQMyj/btsOK29Q4Xx/20Ih60PvyGMIGK8a8k1Hc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVQMyj%2FbtsOK29Q4Xx%2F20Ih60PvyGMIGK8a8k1Hc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2260&quot; height=&quot;1346&quot; data-filename=&quot;스크린샷 2025-06-21 오후 4.35.07.png&quot; data-origin-width=&quot;2260&quot; data-origin-height=&quot;1346&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;최종 완성 코드&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1750491462480&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import os
from openai import AzureOpenAI

# 환경변수 또는 직접 설정
endpoint = os.getenv(&quot;ENDPOINT_URL&quot;, &quot;YOUR_URL&quot;)
deployment = os.getenv(&quot;DEPLOYMENT_NAME&quot;, &quot;7ai010-gpt-4o-mini&quot;)
search_endpoint = os.getenv(&quot;SEARCH_ENDPOINT&quot;, &quot;YOUR_ENDPOINT&quot;)
search_key = os.getenv(&quot;SEARCH_KEY&quot;, &quot;YOUR_SEARCH_KEY&quot;)
search_index = os.getenv(&quot;SEARCH_INDEX_NAME&quot;, &quot;YOUR_INDEX&quot;)
subscription_key = os.getenv(&quot;AZURE_OPENAI_API_KEY&quot;, &quot;YOUR_API_KEY&quot;)

# Azure OpenAI 클라이언트 초기화
client = AzureOpenAI(
    azure_endpoint=endpoint,
    api_key=subscription_key,
    api_version=&quot;2025-01-01-preview&quot;,
)

# 사용자 프롬프트 입력
user_input = input(&quot;질문을 입력하세요: &quot;)

# Chat Prompt 생성
chat_prompt = [
    {
        &quot;role&quot;: &quot;user&quot;,
        &quot;content&quot;: user_input
    }
]

# Completion 요청
completion = client.chat.completions.create(
    model=deployment,
    messages=chat_prompt,
    max_tokens=800,
    temperature=0.7,
    top_p=0.95,
    frequency_penalty=0,
    presence_penalty=0,
    stream=False,
    extra_body={
        &quot;data_sources&quot;: [{
            &quot;type&quot;: &quot;azure_search&quot;,
            &quot;parameters&quot;: {
                &quot;endpoint&quot;: search_endpoint,
                &quot;index_name&quot;: search_index,
                &quot;semantic_configuration&quot;: &quot;matzip-index-semantic-configuration&quot;,
                &quot;query_type&quot;: &quot;semantic&quot;,
                &quot;strictness&quot;: 2,
                &quot;top_n_documents&quot;: 5,
                &quot;authentication&quot;: {
                    &quot;type&quot;: &quot;api_key&quot;,
                    &quot;key&quot;: search_key
                }
            }
        }]
    }
)

# 결과 출력
print(&quot;\n답변:&quot;)
print(completion.choices[0].message.content)

# 참고 문서 출력
citations = completion.choices[0].message.context.get(&quot;citations&quot;)
for citation in citations :
    if citations:
        print(citation['content'] + '\n')
    else:
        print(&quot;\n참조 문서 없음.&quot;)&lt;/code&gt;&lt;/pre&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>Azure</category>
      <category>dl</category>
      <category>ml</category>
      <category>msaischool</category>
      <category>OpenAI</category>
      <category>Rag</category>
      <category>개발</category>
      <category>챗봇</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/82</guid>
      <comments>https://cases.tistory.com/entry/%EC%84%9C%EC%9A%B8-%EB%A7%9B%EC%A7%91-%EC%B1%97%EB%B4%87-%EB%A7%8C%EB%93%A4%EA%B8%B0-Azure-OpenAI-RAG#entry82comment</comments>
      <pubDate>Sat, 21 Jun 2025 16:38:29 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 1차 프로젝트 Record - 최종</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Record-%EC%B5%9C%EC%A2%85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;API 구현까지 완료한 후 다음 과정으로는 다른 팀원분이 각자의 코드(모델, 백엔드, 프론트 등)들을 git으로 merge한 후 수정과 추가 작업을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 작업을 하며 중간 중간 코드 리뷰를 하고, 각 팀원들의 피드백 등을 반영하는 식으로 작업을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때가 프로젝트 마지막 날이어서, 조금 빠듯하게 하느라 다들 정신없이 했던 것 같다.(물론 나도...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 병합을 진행하신 팀원분께서 개발 경력이 있으신 분이라, Git사용법 부터 API 관련해서 많은 것을 알려주셨다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgxiWx/btsODL7Do0i/DKLKWamvYblHXOLlQRAaaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgxiWx/btsODL7Do0i/DKLKWamvYblHXOLlQRAaaK/img.png&quot; data-alt=&quot;GRU / Two-Tower 모델별 API&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgxiWx/btsODL7Do0i/DKLKWamvYblHXOLlQRAaaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgxiWx%2FbtsODL7Do0i%2FDKLKWamvYblHXOLlQRAaaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;342&quot; data-origin-width=&quot;860&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;GRU / Two-Tower 모델별 API&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 구현했던 Two-Tower 모델과 다른 팀원이 구현한 GRU 모델을 따로 구분하여, 모델별 추천 성능도 비교할 수 있게 해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트는 일주일 이라는 제한된 시간이었기에 많은 공을 들여 구현을 하진 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제의 중심은 결국 머신러닝 / 딥러닝이기 때문에 모델을 구현하는 것에 더 높은 비중을 두고 프로젝트를 진행하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;프로젝트 Preview&lt;/b&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.51.08.png&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;1234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dgUwY3/btsOCQIko35/R6ODc1IFDbQdskj7jD1821/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dgUwY3/btsOCQIko35/R6ODc1IFDbQdskj7jD1821/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dgUwY3/btsOCQIko35/R6ODc1IFDbQdskj7jD1821/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdgUwY3%2FbtsOCQIko35%2FR6ODc1IFDbQdskj7jD1821%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2542&quot; height=&quot;1234&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.51.08.png&quot; data-origin-width=&quot;2542&quot; data-origin-height=&quot;1234&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 사이트에 접속을 하면 해당 사용자는 데이터가 없기 때문에 랜덤으로 아이템이 뜨게 되어있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.51.41.png&quot; data-origin-width=&quot;2594&quot; data-origin-height=&quot;1240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kL0eE/btsOCCcKwB5/j6RppNoQ4itPm4t7tQFsck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kL0eE/btsOCCcKwB5/j6RppNoQ4itPm4t7tQFsck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kL0eE/btsOCCcKwB5/j6RppNoQ4itPm4t7tQFsck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkL0eE%2FbtsOCCcKwB5%2Fj6RppNoQ4itPm4t7tQFsck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2594&quot; height=&quot;1240&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.51.41.png&quot; data-origin-width=&quot;2594&quot; data-origin-height=&quot;1240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;스마트폰 케이스&quot;를 검색하는 이벤트를 발생 시킨 후 메인으로 다시 이동을 해보면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.52.13.png&quot; data-origin-width=&quot;2598&quot; data-origin-height=&quot;1242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C29FI/btsODMFtQzm/813pLOEdRkMzAMpSqLhLek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C29FI/btsODMFtQzm/813pLOEdRkMzAMpSqLhLek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C29FI/btsODMFtQzm/813pLOEdRkMzAMpSqLhLek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC29FI%2FbtsODMFtQzm%2F813pLOEdRkMzAMpSqLhLek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2598&quot; height=&quot;1242&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.52.13.png&quot; data-origin-width=&quot;2598&quot; data-origin-height=&quot;1242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 리스트에 스마트폰 케이스 제품들이 섞여서 추천되고 있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 두 개의 키워드를 더 검색한 후의 결과를 살펴보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.52.32.png&quot; data-origin-width=&quot;2584&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbXI4W/btsODXmoTQa/UAowL8lAvtoZW9hbZuRkR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbXI4W/btsODXmoTQa/UAowL8lAvtoZW9hbZuRkR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbXI4W/btsODXmoTQa/UAowL8lAvtoZW9hbZuRkR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbXI4W%2FbtsODXmoTQa%2FUAowL8lAvtoZW9hbZuRkR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2584&quot; height=&quot;1232&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.52.32.png&quot; data-origin-width=&quot;2584&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.53.46.png&quot; data-origin-width=&quot;2574&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1cuz8/btsODLs1YvE/3xKlrRnZErWUk8eIBE6Hl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1cuz8/btsODLs1YvE/3xKlrRnZErWUk8eIBE6Hl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1cuz8/btsODLs1YvE/3xKlrRnZErWUk8eIBE6Hl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1cuz8%2FbtsODLs1YvE%2F3xKlrRnZErWUk8eIBE6Hl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2574&quot; height=&quot;1226&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.53.46.png&quot; data-origin-width=&quot;2574&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.54.07.png&quot; data-origin-width=&quot;2588&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOjnsf/btsOD2gZtIz/mgLKmZdAyjLCdVRVDoq7b1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOjnsf/btsOD2gZtIz/mgLKmZdAyjLCdVRVDoq7b1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOjnsf/btsOD2gZtIz/mgLKmZdAyjLCdVRVDoq7b1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOjnsf%2FbtsOD2gZtIz%2FmgLKmZdAyjLCdVRVDoq7b1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2588&quot; height=&quot;1244&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.54.07.png&quot; data-origin-width=&quot;2588&quot; data-origin-height=&quot;1244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;여름 캠핑 장비&quot;와 &quot;컴퓨터 메모리 32GB&quot;를 검색한 후 메인으로 이동을 하면 이때까지의 모든 데이터가 반영된 추천이 이루어지고 있는 모습을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;프로젝트 리뷰&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자이신 팀원 덕분에 원래 Github을 사용할 줄은 알았지만, 이 정도로 디테일하게 Git을 사용하고, 협업을 진행한건 처음이었던 것 같아서 나에게 좋은 경험이 된 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.37.18.png&quot; data-origin-width=&quot;2478&quot; data-origin-height=&quot;1616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI5po7/btsOCOqfIF8/I2OrkW7H8jeFDOxjERBQUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI5po7/btsOCOqfIF8/I2OrkW7H8jeFDOxjERBQUk/img.png&quot; data-alt=&quot;Git으로 작업한 흔적들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI5po7/btsOCOqfIF8/I2OrkW7H8jeFDOxjERBQUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI5po7%2FbtsOCOqfIF8%2FI2OrkW7H8jeFDOxjERBQUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2478&quot; height=&quot;1616&quot; data-filename=&quot;스크린샷 2025-06-17 오전 12.37.18.png&quot; data-origin-width=&quot;2478&quot; data-origin-height=&quot;1616&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Git으로 작업한 흔적들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 우리 팀은 매일 아침 시작 전 스탠드업 회의를 통해 당일에 진행할 각자의 역할에 대해 확실히 인지를 시켰다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 1.12.34.png&quot; data-origin-width=&quot;2436&quot; data-origin-height=&quot;1294&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oVLsC/btsOCK9ucZi/E20MVHsXxwBEd7BWO93tu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oVLsC/btsOCK9ucZi/E20MVHsXxwBEd7BWO93tu1/img.png&quot; data-alt=&quot;회의록 리스트&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oVLsC/btsOCK9ucZi/E20MVHsXxwBEd7BWO93tu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoVLsC%2FbtsOCK9ucZi%2FE20MVHsXxwBEd7BWO93tu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2436&quot; height=&quot;1294&quot; data-filename=&quot;스크린샷 2025-06-17 오전 1.12.34.png&quot; data-origin-width=&quot;2436&quot; data-origin-height=&quot;1294&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회의록 리스트&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매일 아침 진행한 회의록들을 노션으로 관리하여, 필요할때 바로 찾아서 볼 수 있도록 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회의록 외에도, 여러 관련 문서, 논문, 데이터 등을 노션으로 공유, 관리하여 팀원들과의 커뮤니케이션을 원활히 할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 1.16.00.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;1276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LjUOU/btsOCEItBF4/D6a6PKCBN2Nhx5Xm6c0IJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LjUOU/btsOCEItBF4/D6a6PKCBN2Nhx5Xm6c0IJk/img.png&quot; data-alt=&quot;초반 주제 선정을 위한 아이디어 회의&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LjUOU/btsOCEItBF4/D6a6PKCBN2Nhx5Xm6c0IJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLjUOU%2FbtsOCEItBF4%2FD6a6PKCBN2Nhx5Xm6c0IJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;1276&quot; data-filename=&quot;스크린샷 2025-06-17 오전 1.16.00.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;1276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;초반 주제 선정을 위한 아이디어 회의&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-06-17 오전 1.17.47.png&quot; data-origin-width=&quot;2354&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ShCTT/btsOEl8rOKt/1eJsrp0gLdjKSr03gynGW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ShCTT/btsOEl8rOKt/1eJsrp0gLdjKSr03gynGW0/img.png&quot; data-alt=&quot;관련 문서 허브&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ShCTT/btsOEl8rOKt/1eJsrp0gLdjKSr03gynGW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FShCTT%2FbtsOEl8rOKt%2F1eJsrp0gLdjKSr03gynGW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2354&quot; height=&quot;964&quot; data-filename=&quot;스크린샷 2025-06-17 오전 1.17.47.png&quot; data-origin-width=&quot;2354&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;관련 문서 허브&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 뿐만 아니라 발표 자료 PPT 제작, 발표 대본 스크립트 작성 등을 경험 해보며 개발 외에 여러모로 많은 것들을 배울 수 있었던 프로젝트였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 일주일 가량 진행된 팀 프로젝트 였지만, 체감상 2주 정도는 된 것 같은 느낌이 들 정도로 많은 것을 얻어가는 시간이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두가 완벽하진 않았지만, 모두가 열정적이어서 더 좋았던.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 것을 배울 수 있었던 뿌듯한 시간이었음은 확신할 수 있다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>msaischool</category>
      <category>딥러닝</category>
      <category>머신러닝</category>
      <category>프로젝트</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/81</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Record-%EC%B5%9C%EC%A2%85#entry81comment</comments>
      <pubDate>Tue, 17 Jun 2025 01:28:48 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 1차 프로젝트 Record - 3</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Record-3FastAPI</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번 two-tower 모델 구현에 이어, 백엔드 구현을 어떤 식으로 하게 되었는지 작성해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재의 가장 큰 문제는 저번 포스트에서도 언급 했다시피, 모델은 데이터셋에 존재하는 데이터만을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기반으로 하여 추천을 하게 되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 사용자는 새로운 행동 데이터가 추가가 될 것이고, 그에 따른 모델의 추천 결과도 달라져야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 도대체 어떻게 구현을 해야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 백엔드 구현은 빠른 API 구축이 가능한 FastAPI를 채택하여 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI는 한 번도 사용해본 적이 없었지만, 요즘 코딩 성능이 가장 좋다는 평까지 나오고 있는,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini의 도움을 받아 조금씩 배워가며 코드를 작성할 수 있었다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 나는 사용자의 새로운 행동 데이터가 생길때 마다 실시간으로 모델 재학습을 진행하는 방법을 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 말도 안되는 소리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 현재 모델을 학습하는 시간도 거의 1시간 가까이 걸렸는데, 이 과정을 수많은 사용자들의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 생성될때마다 진행을 한다? 이건 사실상 불가능한 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러가지 방법을 시도해본 후, 하나의 방법을 고안해내게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 데이터를 단기 데이터 / 장기 데이터로 나눈 후, 사용자의 기존 데이터는 장기 데이터, 새롭게 추가된 데이터는 단기 데이터로 저장하여 두 개의 데이터를 모두 고려한 상품을 추천하게 구현해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1750069317149&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.post(&quot;/events&quot;, status_code=202)
def track_event(event: Event):
    # 사용자의 행동 (클릭, 검색) 이벤트를 기록합니다.
    if not SERVER_IS_READY:
        raise HTTPException(status_code=503, detail=&quot;Server is not ready.&quot;)
    
    # 1. 모든 이벤트를 전체 히스토리에 기록 (이력 조회를 위함)
    USER_INTERACTION_HISTORY[event.session_id].append(
        {&quot;event_type&quot;: event.event_type, &quot;value&quot;: event.value}
    )

    # 2. 클릭/검색 이벤트를 단기 행동 기록에 추가
    if event.event_type == 'click' or event.event_type == 'search':
        # 벡터를 바로 업데이트하는 대신, 행동 자체를 기록
        USER_RECENT_ACTIONS[event.session_id].append(event.value) # &amp;lt;--- 여기에 최근 행동이 저장됨.
        print(f&quot;Action '{event.value}' added to recent history for session: {event.session_id}&quot;)
        
    return {&quot;message&quot;: &quot;Event tracked&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 웹 서비스에서 사용자의 클릭, 검색과 같은 이벤트가 발생할 때 호출되어, 해당 이벤트를 USER_RECENT_ACTIONS라는 메모리 상의 큐에 저장한다. 이 큐에 저장된 정보가 나중에 단기 추천에 활용된다.&lt;/p&gt;
&lt;pre id=&quot;code_1750070987509&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# --- 유틸리티 함수 ---
def find_internal_item_vector_by_title(title: str) -&amp;gt; Optional[torch.Tensor]:
    &quot;&quot;&quot;외부 상품 제목과 가장 유사한 내부 아이템을 찾아 그 벡터를 반환합니다.&quot;&quot;&quot;
    if not SERVER_IS_READY: return None
    try:
        title_tfidf = tfidf_vectorizer.transform([title])
        similarities = cosine_similarity(title_tfidf, tfidf_matrix).flatten()
        most_similar_item_idx = similarities.argmax()
        
        # 유사도가 매우 낮으면 (e.g., 0.1 미만) 관련 없는 아이템으로 간주하고 무시
        if similarities[most_similar_item_idx] &amp;lt; 0.1:
            print(f&quot;No relevant internal item found for title: '{title}'&quot;)
            return None
            
        print(f&quot;Found internal item '{all_titles[most_similar_item_idx]}' for external title '{title}'&quot;)
        return ALL_ITEM_VECTORS[most_similar_item_idx]
    except Exception as e:
        print(f&quot;Error finding internal item by title: {e}&quot;)
        return None

def get_base_user_vector(session_id: str) -&amp;gt; Optional[torch.Tensor]:
    &quot;&quot;&quot;모델에서 사용자의 기본(장기) 벡터를 가져옵니다.&quot;&quot;&quot;
    user_idx = SESSION_TO_IDX.get(session_id)
    if user_idx is None: return None
    with torch.no_grad():
        return model.get_user_vector(torch.LongTensor([user_idx]).to(device))

def get_user_vector(session_id: str) -&amp;gt; Optional[torch.Tensor]:
    &quot;&quot;&quot;
    세션의 단기 사용자 벡터를 계산합니다.
    최근 행동(최대 10개)의 평균 벡터를 반환합니다.
    최근 행동이 없으면 None을 반환합니다.
    &quot;&quot;&quot;
    recent_actions = USER_RECENT_ACTIONS.get(session_id)
    if not recent_actions:
        return None # 최근 행동이 없음

    # 최근 행동들의 아이템 벡터를 수집
    recent_item_vectors = []
    print(f&quot;Calculating short-term vector from actions: {list(recent_actions)}&quot;)
    for action_title in recent_actions:
        item_vector = find_internal_item_vector_by_title(action_title)
        if item_vector is not None:
            recent_item_vectors.append(item_vector)

    if not recent_item_vectors:
        return None # 유효한 아이템 벡터가 없음

    # 아이템 벡터들의 평균을 계산하여 순수 단기 벡터 생성
    short_term_vector = torch.mean(torch.stack(recent_item_vectors), dim=0)
    return short_term_vector&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;8:1-8:55&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;find_internal_item_vector_by_title(title: str)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;10:1-10:175&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 클릭하거나 검색한 &lt;b&gt;상품 제목&lt;/b&gt;이 주어졌을 때, 시스템이 알고 있는 수많은 내부 상품들 중에서 가장 비슷한 상품을 찾아 그 상품의 벡터(임베딩)를 가져오는 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*TF-IDF&lt;/b&gt; 와 코사인 유사도를 사용해서 제목 간의 유사도를 측정하고, 가장 유사한 내부 아이템의 미리 계산된 임베딩 벡터를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;10:1-10:175&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;*TF-IDF&amp;nbsp; : &quot;Term Frequency-Inverse Document Frequency&quot;의 약자로, 특정 단어가 문서 집합(코퍼스) 내에서 얼마나 중요한지를 측정하는 통계적 가중치 방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;10:1-10:175&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 쉽게 말해, 어떤 단어가 특정 문서에서 자주 나오면서도 다른 문서에서는 잘 나오지 않을수록 그 단어가 해당 문서를 대표하는 중요한 단어라고 판단하는 방식이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;10:1-10:175&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;10:1-10:175&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt; get_base_user_vector(session_id: str) &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수는 특정 &lt;b&gt;사용자의 고유한 취향(장기 선호도)을 나타내는 벡터&lt;/b&gt;를 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 벡터는 모델이 처음 학습될 때 사용자별로 고정적으로 생성된 값이며, 사용자의 전반적인 관심사를 대변한다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 ID를 내부 인덱스로 변환한 다음, 사전 학습된 모델에서 해당 인덱스에 매핑된 사용자 임베딩 벡터를 직접 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;get_user_vector(session_id : str)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;track_event 함수를 통해 기록된 사용자의 &lt;b&gt;최근 10개 행동&lt;/b&gt; (상품 제목)을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 제목들 각각에 대해 위에서 설명한 find_internal_item_vector_by_title 함수를 써서 해당 상품의 임베딩 벡터를 찾는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 찾은 &lt;b&gt;모든 벡터들의 평균&lt;/b&gt;을 내서 단기 사용자 벡터가 완성된다.&lt;/p&gt;
&lt;pre id=&quot;code_1750074183730&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@app.get(&quot;/recommend/{session_id}&quot;, response_model=RecommendationResponse)
def get_recommendations(session_id: str, long_term_ratio: float = 0.7, num_recs: int = 30):
    if not SERVER_IS_READY: raise HTTPException(status_code=503, detail=&quot;Server not ready&quot;)

    long_term_vector = get_base_user_vector(session_id)
    if long_term_vector is None:
        raise HTTPException(status_code=404, detail=f&quot;Session ID '{session_id}' not found.&quot;)

    short_term_vector = get_user_vector(session_id) # 최신 단기 벡터

    with torch.no_grad():
        # 장기 벡터는 항상 존재
        long_term_scores = torch.matmul(ALL_ITEM_VECTORS, long_term_vector.T).squeeze()

        if short_term_vector is not None:
            # 단기 벡터가 있으면 단기 점수도 계산
            short_term_scores = torch.matmul(ALL_ITEM_VECTORS, short_term_vector.T).squeeze()
            num_long_term_recs = int(num_recs * long_term_ratio)
            num_short_term_recs = num_recs - num_long_term_recs
        else:
            # 단기 벡터가 없으면 장기 추천만 제공
            print(f&quot;No short-term vector for {session_id}, returning long-term recs only.&quot;)
            short_term_scores = None
            num_long_term_recs = num_recs
            num_short_term_recs = 0

        long_term_indices = torch.topk(long_term_scores, k=num_long_term_recs * 2).indices.tolist()
        
        if short_term_scores is not None:
            short_term_indices = torch.topk(short_term_scores, k=num_short_term_recs * 2).indices.tolist()
        else:
            short_term_indices = []

    # 2. 두 추천 목록을 조합
    combined_indices = []
    seen_indices = set()

    # 단기 추천(최신 관심사)을 먼저 일부 추가
    for idx in short_term_indices:
        if idx not in seen_indices:
            combined_indices.append(idx)
            seen_indices.add(idx)
        if len(seen_indices) &amp;gt;= num_short_term_recs:
            break
            
    # 장기 추천으로 나머지 채우기
    for idx in long_term_indices:
        if idx not in seen_indices:
            combined_indices.append(idx)
            seen_indices.add(idx)
        if len(combined_indices) &amp;gt;= num_recs:
            break

    # 3. 조합된 인덱스로부터 최종 추천 목록 생성 (내부 데이터 사용)
    final_recommendations = []
    for idx in combined_indices:
        title = mappings['item_titles'].get(idx)
        if title:
            final_recommendations.append(Product(
                title=title,
                link=&quot;#&quot;, # 내부 아이템은 링크가 없음
                price=mappings['item_prices'].get(idx),
                thumbnail=None,
                source=&quot;Internal DB&quot;
            ))

    internal_titles = [rec.title for rec in final_recommendations]
    print(f&quot;Blended internal recommendations for {session_id}: {internal_titles}&quot;)

    return RecommendationResponse(session_id=session_id, recommendations=final_recommendations)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-sourcepos=&quot;10:1-24:0&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-sourcepos=&quot;10:1-13:0&quot;&gt;&lt;b&gt;사용자 취향 벡터 준비:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;10:1-13:0&quot;&gt;&amp;nbsp;long_term_vector를 통해 사용자의 &lt;b&gt;장기기억 데이터&lt;/b&gt;를 가져오고, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;short_term_vector를 통해 사용자의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;최근 행동(클릭, 검색)으로 파악된 단기적인 관심사&lt;/b&gt;를 동적으로 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sourcepos=&quot;14:1-17:0&quot;&gt;&lt;b&gt;아이템 추천 점수 계산:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;15:5-17:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;15:5-15:86&quot;&gt;ALL_ITEM_VECTORS (모든 상품의 임베딩 벡터)와 &lt;b&gt;장기 데이터 벡터&lt;/b&gt;의 유사도를 계산하여 &lt;b&gt;장기 추천 점수&lt;/b&gt;를 매김.&lt;/li&gt;
&lt;li data-sourcepos=&quot;16:5-17:0&quot;&gt;&lt;b&gt;단기 데이터 벡터&lt;/b&gt;가 있다면, 이 벡터와 ALL_ITEM_VECTORS의 유사도를 계산하여 &lt;b&gt;단기 추천 점수&lt;/b&gt;를 매김.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sourcepos=&quot;18:1-21:0&quot;&gt;&lt;b&gt;장기/단기 추천 조합 (블렌딩):&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;19:5-21:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;19:5-19:77&quot;&gt;long_term_ratio 에 따라 최종 추천 목록에서 장기/단기 추천이 차지할 &lt;b&gt;비율&lt;/b&gt;을 결정.&lt;/li&gt;
&lt;li data-sourcepos=&quot;20:5-21:0&quot;&gt;단기 추천 점수가 높은 상품들을 &lt;b&gt;우선적으로&lt;/b&gt; 목록에 채우고, 나머지 자리를 장기 추천 점수가 높은 상품들로 채운다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;20:5-21:0&quot;&gt;이때 &lt;b&gt;중복된 상품은 제거&lt;/b&gt;하여 최종 추천 목록을 만든다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;20:5-21:0&quot;&gt;이 블렌딩 과정을 통해 사용자의 최신 관심사를 빠르게 반영하면서도, 기존의 선호도도 잃지 않도록 균형을 맞춘다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sourcepos=&quot;22:1-24:0&quot;&gt;&lt;b&gt;최종 추천 목록 반환:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;23:5-24:0&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;23:5-24:0&quot;&gt;조합된 상품 인덱스를 바탕으로 실제 상품 정보(제목, 가격 등)를 가져와 Product 객체 리스트 형태로 사용자에게 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>dl</category>
      <category>fastapi</category>
      <category>ml</category>
      <category>msaischool</category>
      <category>pytorch</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/80</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Record-3FastAPI#entry80comment</comments>
      <pubDate>Mon, 16 Jun 2025 21:07:22 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 1차 프로젝트 Record - 2</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Record-2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;다른 팀원분은 GRU 모델을 사용하여 모델 학습 후 백엔드 진행을 맡았고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 Two-Tower 모델을 구현을 해보기로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 모델로 구현 후, 코드 리뷰를 해보면서 기능을 테스트 해보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 오늘은 내가 이틀 동안 구현해 본 추천 시스템을 위한 Two-Tower 모델 학습 파이프라인을 설명해보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 전처리&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749612591400&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; # --- 1. 데이터 로드 및 전처리 ---
    print(&quot;\n--- 1. Loading and Preprocessing Data ---&quot;)
    
    parquet_files = [f&quot;{data_folder_path}/{i:012d}.parquet&quot; for i in range(4)]

    print(f&quot;Attempting to load {len(parquet_files)} parquet files...&quot;)
    try:
        # 필요한 컬럼만 불러오기
        columns_to_read = ['session_id', 'item_id', 'name', 'price', 'c1_name', 'c2_name', 'brand_name']
        dataset = load_dataset('parquet', data_files={'train': parquet_files}, split='train', columns=columns_to_read)
        df = dataset.to_pandas()
        print(f&quot;Successfully loaded {len(df)} rows from {len(parquet_files)} Parquet files.&quot;)
        
        #  결측치 처리
        for col in ['c1_name', 'c2_name', 'brand_name', 'item_condition_name']:
            df[col] = df[col].fillna('Unknown')
        
        # 사용자/아이템 인덱스 생성
        df['user_idx'] = df['session_id'].astype('category').cat.codes
        df['item_idx'] = df['item_id'].astype('category').cat.codes
        
        num_users = len(df['user_idx'].unique())
        num_items = len(df['item_idx'].unique())
        print(f&quot;Unique Users: {num_users}, Unique Items: {num_items}&quot;)

    except Exception as e:
        print(f&quot;An error occurred during data loading: {e}&quot;)
        exit()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 10개의 parquet 데이터를 다운 받아서 학습을 시키려고 했지만, GPU 성능의 이슈로 인해 학습 시간이 너무 오래 걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 타협을 본 후 총 4개의 파일만 불러와서 학습을 시키게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습에 사용 될 column은 &lt;b&gt;'session_id', 'item_id', 'name', 'price', 'c1_name', 'c2_name', 'brand_name'&amp;nbsp;&lt;/b&gt;총 7개 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;session_id는 각 사용자를 구분하는 user_idx로 사용될 것이고, item_id, name은 각 상품을 뜻하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Feature Engineering&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749613076285&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 2-1. 가격(Price) 피처 처리
print(&quot;Processing price feature...&quot;)
# 가격이 0 이하인 경우 이상치로 간주하고 1로 처리 후 로그 변환
df['price'] = df['price'].apply(lambda x: max(x, 1.0))
df['log_price'] = np.log1p(df['price'])

price_scaler = StandardScaler()
df['scaled_price'] = price_scaler.fit_transform(df[['log_price']])

# 아이템별 가격 정보 저장
item_prices = df.drop_duplicates('item_idx').set_index('item_idx')['scaled_price'].to_dict()

# 2-2. 텍스트(Text) 임베딩 생성 
print(&quot;Creating enhanced item embeddings...&quot;)
text_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)

# 아이템별 메타데이터 딕셔너리 생성
item_meta = df.drop_duplicates('item_idx').set_index('item_idx')
item_titles = item_meta['name'].to_dict()
item_idx_to_c1 = item_meta['c1_name'].to_dict()
item_idx_to_c2 = item_meta['c2_name'].to_dict()
item_idx_to_brand = item_meta['brand_name'].to_dict()
item_idx_to_condition = item_meta['item_condition_name'].to_dict()

# 모든 피처를 결합한 상세 설명 생성
item_descriptions = []
for i in range(num_items):
    name = item_titles.get(i, 'N/A')
    c1 = item_idx_to_c1.get(i, 'N/A')
    c2 = item_idx_to_c2.get(i, 'N/A')
    brand = item_idx_to_brand.get(i, 'N/A')
    condition = item_idx_to_condition.get(i, 'N/A')
    description = (f&quot;Name: {name}. Category: {c1}, {c2}. &quot;
                   f&quot;Brand: {brand}. Condition: {condition}.&quot;)
    item_descriptions.append(description)

# 텍스트 임베딩 생성
item_text_embeddings = text_model.encode(item_descriptions, show_progress_bar=True, convert_to_tensor=True, device=device)
text_embedding_dim = item_text_embeddings.shape[1]
print(&quot;Item embeddings generated.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 가격에 대해서는 우선 0 이하의 값들(이상치)을 1로 보정 해주었고, &lt;b&gt;right-skewed 분포를 모델이 학습하기 수월한 정규 분포에 가깝게 처리&lt;/b&gt;해주기 위해 로그 변환을 적용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 Feature들을 결합하여, 텍스트 임베딩을 해주었는데, 텍스트 임베딩 생성 모델에는 &lt;b&gt;'all-MiniLM-L6-v2'&amp;nbsp;&lt;/b&gt;라는 SentenceTransformer 모델을 사용하였다.&amp;nbsp;이 모델은 Sentence Embaddings를 생성하는데에 특화된 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문장이나 단락의 의미를 숫자 벡터로 변환하고, 이를 통해 컴퓨터가 텍스트의 의미를 이해하고 다양한 자연어 처리(NLP) 작업에 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;모델 및 데이터셋 정의&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1749627748451&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 3-1. 모델 아키텍처 (가격 피처 결합)
class TwoTowerModel(nn.Module):
    def __init__(self, num_users, precomputed_item_embeddings, text_embedding_dim, final_embedding_dim=64):
        super(TwoTowerModel, self).__init__()
        self.final_embedding_dim = final_embedding_dim

        # User Tower
        self.user_embedding = nn.Embedding(num_users, final_embedding_dim)

        # Item Tower
        self.item_text_embedding = nn.Embedding.from_pretrained(precomputed_item_embeddings, freeze=True)
        # 텍스트 임베딩과 가격 피처(1차원)를 합친 후 최종 임베딩 차원으로 매핑하는 MLP
        self.item_mlp = nn.Sequential(
            nn.Linear(text_embedding_dim + 1, (text_embedding_dim + 1) // 2),
            nn.ReLU(),
            nn.Linear((text_embedding_dim + 1) // 2, final_embedding_dim)
        )

    def get_user_vector(self, user_ids):
        return self.user_embedding(user_ids)

    def get_item_vector(self, item_ids, item_prices):
        text_vecs = self.item_text_embedding(item_ids)
        # 가격 피처를 [batch_size, 1] 형태로 만들어 텍스트 임베딩과 결합
        combined_vec = torch.cat([text_vecs, item_prices.unsqueeze(1)], dim=1)
        return self.item_mlp(combined_vec)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&amp;lt; User Tower &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Tower에는 사용자 ID를 임베딩 벡터로 변환하는 nn.Embedding 레이어를 포함하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;num_users는 임베딩을 할 고유한 사용자 ID의 개수이다. final_embedding_dim은 각 사용자 ID에 해당하는 임베딩 벡터의 차원수이다. 예를 들어서, num_users = 1000, final_embedding_dim = 128 이라면, 이 레이어는 1000 x 128 크기의 임베딩 행렬을 가지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt; Item Tower &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nn.Embedding.from_pretrained() : 미리 계산된(Sentence Transformer로 생성한 아이템 텍스트 임베딩)을 사용하여 임베딩 레이어를 초기화 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;precomputed_item_embeddings : item_text_embeddings 텐서가 여기에 전달된다. 이 텐서의 각 행이 특정 아이템의 텍스트 임베딩 벡터가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 freeze=True 를 써주어, 임베딩 레이어의 가중치를 학습 과정에서 업데이트 하지 않게 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다층 퍼셉트론(MLP)로 텍스트 임베딩과 가격 피처를 결합하여 최종 아이템 임베딩을 생성해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; get_item_vector 메서드&lt;/b&gt;에서 기존 1차원 배열 형태의 가격 feature를 torch.cat을 위해 텍스트 임베딩과 차원을 맞춰주었고, 각 아이템 임베딩 벡터의 차원 방향으로 결합해주었다. 최종적으로 final_embedding_dim 차원의 아이템 임베딩을 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;네거티브 샘플링을 포함한 데이터셋 클래스&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749631565079&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class TripletDataset(Dataset):
    def __init__(self, interactions_df, item_prices_dict, all_item_indices):
        self.users = torch.LongTensor(interactions_df['user_idx'].values)
        self.pos_items = torch.LongTensor(interactions_df['item_idx'].values)
        self.item_prices = item_prices_dict
        self.all_item_indices = all_item_indices
        self.num_items = len(all_item_indices)

    def __len__(self):
        return len(self.users)

    def __getitem__(self, idx):
        user_id = self.users[idx]
        pos_item_id = self.pos_items[idx]

        # 네거티브 샘플링
        while True:
            neg_item_id = np.random.randint(0, self.num_items)
            # 여기서는 단순 랜덤 샘플링을 사용 (개선 가능)
            if neg_item_id != pos_item_id.item():
                break
        
        pos_item_price = torch.tensor(self.item_prices.get(pos_item_id.item(), 0.0), dtype=torch.float)
        neg_item_price = torch.tensor(self.item_prices.get(neg_item_id, 0.0), dtype=torch.float)
        
        return user_id, pos_item_id, neg_item_id, pos_item_price, neg_item_price&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 nn.TripletMarginLoss와 같은 손실 함수에 필요한 &lt;b&gt;트리플릿(Anchor, Positive, Negative)&lt;/b&gt; 형태로 데이터를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;118:1-118:30&quot;&gt;&lt;b&gt;Anchor (앵커)&lt;/b&gt;: 사용자의 임베딩 벡터&lt;/li&gt;
&lt;li data-sourcepos=&quot;119:1-119:61&quot;&gt;&lt;b&gt;Positive (긍정 샘플)&lt;/b&gt;: 사용자가 실제로 상호작용(예: 구매, 클릭)한 아이템의 임베딩 벡터&lt;/li&gt;
&lt;li data-sourcepos=&quot;120:1-121:0&quot;&gt;&lt;b&gt;Negative (부정 샘플)&lt;/b&gt;: 사용자가 상호작용하지 않은 아이템 중 임의로 선택된 아이템의 임베딩 벡터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앵커와 긍정 샘플은 가깝게, 부정 샘플과는 멀게 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;학습 및 검증&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1749634962402&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 데이터 분할
df_interactions = df[['user_idx', 'item_idx']].drop_duplicates().reset_index(drop=True)
train_val_df, test_df = train_test_split(df_interactions, test_size=0.1, random_state=42)
train_df, val_df = train_test_split(train_val_df, test_size=0.15, random_state=42)

print(f&quot;Train size: {len(train_df)}, Validation size: {len(val_df)}, Test size: {len(test_df)}&quot;)

all_items = np.arange(num_items)
train_dataset = TripletDataset(train_df, item_prices, all_items)
val_dataset = TripletDataset(val_df, item_prices, all_items)

train_loader = DataLoader(train_dataset, batch_size=512, shuffle=True, num_workers=4, pin_memory=True)
val_loader = DataLoader(val_dataset, batch_size=512, shuffle=False, num_workers=4, pin_memory=True)

# 4-2. 모델 학습 루프 (검증 및 조기 종료 포함)
final_embedding_dim = 128
model = TwoTowerModel(num_users, item_text_embeddings, text_embedding_dim, final_embedding_dim).to(device)
optimizer = optim.Adam(model.parameters(), lr=0.001)
# TripletLoss: anchor와 positive는 가깝게, anchor와 negative는 멀게 만듦
criterion = nn.TripletMarginLoss(margin=1.0)

num_epochs = 20
best_val_loss = float('inf')
patience = 3
patience_counter = 0

print(&quot;Starting model training...&quot;)
for epoch in range(num_epochs):
    model.train()
    total_train_loss = 0

    for batch in tqdm(train_loader, desc=f&quot;Epoch {epoch+1}/{num_epochs} [Training]&quot;):
        user_ids, pos_item_ids, neg_item_ids, pos_prices, neg_prices = [b.to(device) for b in batch]

        optimizer.zero_grad()

        user_vec = model.get_user_vector(user_ids)
        pos_item_vec = model.get_item_vector(pos_item_ids, pos_prices)
        neg_item_vec = model.get_item_vector(neg_item_ids, neg_prices)

        loss = criterion(user_vec, pos_item_vec, neg_item_vec)
        loss.backward()
        optimizer.step()
        total_train_loss += loss.item()

    avg_train_loss = total_train_loss / len(train_loader)

    # 검증
    model.eval()
    total_val_loss = 0
    with torch.no_grad():
        for batch in tqdm(val_loader, desc=f&quot;Epoch {epoch+1}/{num_epochs} [Validation]&quot;):
            user_ids, pos_item_ids, neg_item_ids, pos_prices, neg_prices = [b.to(device) for b in batch]
            user_vec = model.get_user_vector(user_ids)
            pos_item_vec = model.get_item_vector(pos_item_ids, pos_prices)
            neg_item_vec = model.get_item_vector(neg_item_ids, neg_prices)
            loss = criterion(user_vec, pos_item_vec, neg_item_vec)
            total_val_loss += loss.item()

    avg_val_loss = total_val_loss / len(val_loader)
    print(f&quot;Epoch {epoch+1}, Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}&quot;)

    # 조기 종료 및 모델 저장
    if avg_val_loss &amp;lt; best_val_loss:
        best_val_loss = avg_val_loss
        torch.save(model.state_dict(), os.path.join(artifacts_folder, &quot;best_model.pth&quot;))
        print(f&quot;Validation loss improved. Saved best model to '{artifacts_folder}/best_model.pth'&quot;)
        patience_counter = 0
    else:
        patience_counter += 1
        print(f&quot;Validation loss did not improve. Patience: {patience_counter}/{patience}&quot;)
        if patience_counter &amp;gt;= patience:
            print(&quot;Early stopping triggered.&quot;)
            break&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 상호작용 데이터 준비&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;User - Item 쌍이 한 번만 학습 데이터에 포함되도록 하기 위해서, drop_duplicates()&lt;/b&gt;를 사용하여 중복을 제거하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 데이터셋 분할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 상호작용 데이터(df_interactions) 를 Train, Validation, Test 세트로 각각 76.5%, 13.5%, 10% 비율로 나눴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. TripletDataset 인스턴스 생성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;all_items = np.arange(num_items) : 전체 아이템의 인덱스 배열을 생성 -&amp;gt; 부정 샘플링을 위한 준비&lt;/li&gt;
&lt;li&gt;train_dataset = TripletDataset(train_df, item_prices, all_items) : 학습용 상호작용 데이터(train_df), 아이템 가격 정보(item_prices), 전체 아이템 인덱스(all_items)를 사용하여 학습용 TripletDataset을 만든다.&lt;/li&gt;
&lt;li&gt;검증용 TripletDataset도 위와 동일한 방법으로 만들어주었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. DataLoader 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 데이터는 Epoch마다 섞어서 모델이 데이터의 순서에 의존하지 않고 일반화 능력을 향상시키도록 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;num_workers&lt;/b&gt;는 &lt;b&gt;데이터를 로드하고 전처리하는 데 사용할 서브프로세스의 수&lt;/b&gt;를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 한 batch에 대한 학습을 진행하는 동안,&lt;b&gt; num_workers는 다음 배치를 미리 준비&lt;/b&gt;해 둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pin_memory=True&lt;/b&gt; 옵션은 DataLoader가 데이터를 CPU 메모리에 미리 고정된 상태로 로드하여, &lt;b&gt;GPU 데이터를 복사할 때 더 효율적으로 전송&lt;/b&gt; 되도록 돕는다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 모델 학습 루프&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 단계에서는 train_loader 에서 배치 데이터를 가져와 &lt;b&gt;forward pass, 손실 계산, backward pass, 옵티마이저 스텝을 수행&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 단계에서는 각 Epoch이 끝날 때마다 &lt;b&gt;model.eval() 로 설정하고, val_loader 를 사용하여 모델 성능을 검증&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 손실이 더 이상 개선되지 않으면 학습을 &lt;b&gt;조기 중단하여 과적합을 방지&lt;/b&gt;하고, &lt;b&gt;가장 좋은 성능을 보였던 모델의 가중치를 저장&lt;/b&gt;하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Artifacts 저장&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1749649132577&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;print(&quot;\n--- 5. Saving Artifacts ---&quot;)
# 1. 가장 성능이 좋았던 모델 가중치 이름 변경
if os.path.exists(os.path.join(artifacts_folder, &quot;best_model.pth&quot;)):
    os.rename(os.path.join(artifacts_folder, &quot;best_model.pth&quot;), os.path.join(artifacts_folder, &quot;two_tower_model.pth&quot;))
    print(&quot;Saved best model as 'two_tower_model.pth'&quot;)

# 2. 아이템 텍스트 임베딩 저장
torch.save(item_text_embeddings, os.path.join(artifacts_folder, &quot;item_text_embeddings.pt&quot;))

# 3. 가격 스케일러 저장
with open(os.path.join(artifacts_folder, &quot;price_scaler.pkl&quot;), 'wb') as f:
    pickle.dump(price_scaler, f)

# 4. 매핑 및 데이터 저장
mappings = {
    'user_categories': df['session_id'].astype('category').cat.categories,
    'item_categories': df['item_id'].astype('category').cat.categories,
    'idx_to_item_original': {idx: original_id for idx, original_id in enumerate(df['item_id'].astype('category').cat.categories)},
    'item_titles': item_titles,
    'item_idx_to_c1': item_idx_to_c1,
    'item_idx_to_c2': item_idx_to_c2,
    'item_idx_to_brand': item_idx_to_brand,
    'item_idx_to_condition': item_idx_to_condition,
    'item_prices': item_prices,
    'final_embedding_dim': final_embedding_dim,
    'text_embedding_dim': text_embedding_dim
}
with open(os.path.join(artifacts_folder, &quot;mappings.pkl&quot;), 'wb') as f:
    pickle.dump(mappings, f)

# 5. 테스트 데이터 저장
test_df.to_pickle(os.path.join(artifacts_folder, &quot;test_df.pkl&quot;))

print(f&quot;Artifacts saved to '{artifacts_folder}' folder.&quot;)
print(&quot;\n--- Training Script Finished ---&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 완료된 후, 추후 백엔드와 연동하여 활용할 수 있게 Artifacts를 저장해주었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;two_tower_model.pth : 최종 학습된 모델의 가중치&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;item_text_embeddings.pt : 모든 아이템의 사전 계산된 텍스트 임베딩&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;price_scaler.pkl : 가격 스케일링에 사용된 StandardScaler 객체 (새로운 아이템 가격을 처리할 때 사용)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mappings.pkl : 사용자 및 아이템의 원본 ID와 인덱스 간의 매핑 정보, 아이템의 메타데이터(이름, 카테고리, 브랜드, 상태), 그리고 최종 임베딩 차원 및 텍스트 임베딩 차원 등의 필요한 정보들을 딕셔너리 형태로 저장&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 까지 하고 &quot;사용자의 실시간으로 업데이트 되는 행동 데이터들은 어떡하지?&quot;라는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 처음엔 막연히 모델에게 그때 그때 재학습을 시켜서 구현을 해야하나 싶었지만, 유저 한 명, 한 명의 실시간으로 쌓여가는 수많은 행동데이터를 그때마다 모델에게 학습을 시킨다는 것은 거의 불가능한 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 방법을 찾고 찾은 결과, 이와 관련된 코드는 백엔드로 구현을 하게 되었는데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후로 작업했던 백엔드와 간단한 테스트용 프론트 작업에 대해서는 다음 포스팅에서 다룰 예정이다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>msaischool</category>
      <category>two-tower model</category>
      <category>딥러닝</category>
      <category>머신러닝</category>
      <category>추천시스템</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/79</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Record-2#entry79comment</comments>
      <pubDate>Wed, 11 Jun 2025 22:53:46 +0900</pubDate>
    </item>
    <item>
      <title>[MS AI School] 1차 프로젝트 Recorde - 1</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Recorde-1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 팀 프로젝트의 주제는 &lt;b&gt;&quot;고객별 상품 추천 시스템&quot;&amp;nbsp;&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해서, 쿠팡, 아마존, 무신사, 넷플릭스 등과 같이 고객별로 취향, 행동 등을 반영하여 그에 맞는 상품을 추천해주는 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제는 쿠팡, 아마존 같은 시스템을 구현 해보고 싶다는 나의 의견 제시를 시작으로 최종 선정이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 주제를 선정한 이유는, 이번 기수들을 포함하여 이전 기수들도 한 번도 하지 않은, 그리고 실무에서도 많은 기업들에서도 사용 중인, 또는 사용 할 수 있는 주제라고 생각해서 선정하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 내가 생각했을 때는, 군집 모델을 활용하여, 고객별로 특성이 비슷한 군집을 생성하게 한 후 상품도 군집화를 진행 -&amp;gt; 각 고객 군집별 군집의 특성에 맞는 상품 추천. 이러한 흐름을 생각했었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 프로젝트 첫 날, 프로젝트에 사용할 데이터셋을 조사 하면서 여러 알고리즘과 모델들을 서칭해보았는데, 그렇게 군집화 모델 하나로 띡 하고 해결 될 단순한 문제가 아니었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 어떤 알고리즘으로 구성되어 있는지 내부를 다 알 수는 없지만, 여러 논문과 kaggle을 뒤져가며 추천 시스템을 조사해본 결과 여러가지 모델 / 알고리즘이 있었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 조사하면서 알게된 내용들은 따로 포스팅 하여 자세히 다룰 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 데이터셋을 엄청 찾아봤는데, 우리에게 딱 적합한 데이터셋은 찾기 힘들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 행동패턴에 대한 정보, 상품명, 카테고리명 등이 다 같이 포함되어 있는 데이터가 필요했는데, 꼭 하나씩 빠져있는 데이터셋만 있어서 골치가 좀 아팠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.kaggle.com/retailrocket/ecommerce-dataset&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.kaggle.com/retailrocket/ecommerce-dataset&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749041710071&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Retailrocket recommender system dataset&quot; data-og-description=&quot;Ecommerce data: web events, item properties (with texts), category tree&quot; data-og-host=&quot;www.kaggle.com&quot; data-og-source-url=&quot;https://www.kaggle.com/retailrocket/ecommerce-dataset&quot; data-og-url=&quot;https://www.kaggle.com/datasets/retailrocket/ecommerce-dataset&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/WY3ub/hyY5a2wLjh/WCRLKSMcbvBrJmO2OHkoE0/img.jpg?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://www.kaggle.com/retailrocket/ecommerce-dataset&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.kaggle.com/retailrocket/ecommerce-dataset&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/WY3ub/hyY5a2wLjh/WCRLKSMcbvBrJmO2OHkoE0/img.jpg?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Retailrocket recommender system dataset&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ecommerce data: web events, item properties (with texts), category tree&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.kaggle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.kaggle.com/datasets/karkavelrajaj/amazon-sales-dataset/data&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.kaggle.com/datasets/karkavelrajaj/amazon-sales-dataset/data&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749041755042&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Amazon Sales Dataset&quot; data-og-description=&quot;This dataset is having the data of 1K+ Amazon Product's Ratings and Reviews&quot; data-og-host=&quot;www.kaggle.com&quot; data-og-source-url=&quot;https://www.kaggle.com/datasets/karkavelrajaj/amazon-sales-dataset/data&quot; data-og-url=&quot;https://www.kaggle.com/datasets/karkavelrajaj/amazon-sales-dataset&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cbrVus/hyY1lxUMLe/EoOAWmPJGeMkabIob8ETE1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200&quot;&gt;&lt;a href=&quot;https://www.kaggle.com/datasets/karkavelrajaj/amazon-sales-dataset/data&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.kaggle.com/datasets/karkavelrajaj/amazon-sales-dataset/data&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cbrVus/hyY1lxUMLe/EoOAWmPJGeMkabIob8ETE1/img.png?width=1200&amp;amp;height=1200&amp;amp;face=0_0_1200_1200');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Amazon Sales Dataset&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;This dataset is having the data of 1K+ Amazon Product's Ratings and Reviews&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.kaggle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 한 가지 방법을 팀원들에게 제안했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 두 데이터에서 필요한 부분만 추출하여 병합을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retailrocket 같은 경우에는 모두 다 갖추고 있지만, 상품명이 존재하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, Amazon의 데이터는 상품명은 갖추고 있었지만, 사용자 행동에 대한 데이터가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 팀원들에게 이 의견을 제시한 후, 팀원들도 모두 동의하여 데이터 병합을 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;Trouble Shooting&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 여기서 한가지 문제가 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retailrocket 데이터의 item_id에 임의로 Amazon의 product_name을 부여하려고 했는데, 이렇게 되면 사용자 군집화를 진행하였을때, 군집 왜곡이 발생할 수 있다는 것을 알게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;군집 왜곡이 왜 발생하느냐고 설명을 하자면, 예를 들어, A라는 고객 데이터가 있다고 치자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retailrocket 데이터셋에서 A고객의 행동 데이터에는 화장품과 관련된 상품을 클릭, 조회 등을 했다고 기록되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 이 상품을 무작위로 지정하게 되면, 음식, 옷, 전자기기 등 A고객은 이것 저것 무작위로 상품을 조회 한 것으로 되어, 이 데이터를 바탕으로 군집을 진행한다면, 당연히 군집이 제대로 될리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 고민끝에 또 한가지 방법을 고안해냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 Retailrocket과 Amazon의 Category 컬럼을 이용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Retailrocket에는 categoryid 컬럼이, Amazon에는 Category_name이 들어있어서, Retailrocket의 item_properties.csv의 property 컬럼의 value가 category인 데이터만 뽑은 다음, 필터링된 데이터들의 value컬럼에서 같은 categoryid를 가진것끼리 그룹을 만든 후 출력을 해보면, 각 카테고리별 아이템의 수를 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250604_223112789.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;1140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWGc7e/btsOraMkzWD/ZvzxVFrX8OM6hdsxCKfik0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWGc7e/btsOraMkzWD/ZvzxVFrX8OM6hdsxCKfik0/img.png&quot; data-alt=&quot;categoryid별로 상품 count&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWGc7e/btsOraMkzWD/ZvzxVFrX8OM6hdsxCKfik0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWGc7e%2FbtsOraMkzWD%2FZvzxVFrX8OM6hdsxCKfik0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;1140&quot; data-filename=&quot;KakaoTalk_20250604_223112789.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;1140&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;categoryid별로 상품 count&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 하나 짚고 넘어갈 것이, 저 categoryid는 대분류인지, 중분류, 소분류인지 알 수가 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 데이터를 분석해보았는데, category_tree.csv 파일을 이용하면 됐었다.(왜 이 파일을 볼 생각을 못 했는가...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일에는 categoryid, parentid 두 컬럼이 존재하는데, 우리는 대분류로 category를 나누기로 결정을 해서, parentid가 Nan인 categoryid만 출력을 시켜보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250604_223112789_01.png&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wx1I0/btsOoRarWh7/8o311ttQKUtYkVh5Pjqyv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wx1I0/btsOoRarWh7/8o311ttQKUtYkVh5Pjqyv1/img.png&quot; data-alt=&quot;대분류 카테고리만 출력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wx1I0/btsOoRarWh7/8o311ttQKUtYkVh5Pjqyv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwx1I0%2FbtsOoRarWh7%2F8o311ttQKUtYkVh5Pjqyv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;632&quot; height=&quot;964&quot; data-filename=&quot;KakaoTalk_20250604_223112789_01.png&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대분류 카테고리만 출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하여 어찌저찌 우리가 필요한 데이터셋을 구성할 수 있게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 바로 다음 날, 팀원 중 한 분이 프로젝트에 필요한 데이터(사용자 행동 데이터, 상품명, 카테고리명 등), 방대한 데이터 수(5백만 명 이상의 사용자, 8천만 개 이상의 상품, 10억 건 이상의 사용자-상품 상호작용 이벤트)를 찾아오셔서... 나의 반나절 동안의 수고는 물거품이 되어버렸지만... 그래도 이를 통해 얻어가는 것이 크다고 생각하며, 스스로 위로 했다 하하...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://huggingface.co/datasets/mercari-us/merrec&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://huggingface.co/datasets/mercari-us/merrec&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749044661931&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;mercari-us/merrec &amp;middot; Datasets at Hugging Face&quot; data-og-description=&quot;MerRec: A Large-scale Multipurpose Mercari Dataset for Consumer-to-Consumer Recommendation Systems This repository contains the dataset accompanying the paper MerRec: A Large-scale Multipurpose Mercari Dataset for Consumer-to-Consumer Recommendation System&quot; data-og-host=&quot;huggingface.co&quot; data-og-source-url=&quot;https://huggingface.co/datasets/mercari-us/merrec&quot; data-og-url=&quot;https://huggingface.co/datasets/mercari-us/merrec&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/jDbNr/hyY1jNEkrx/cQEBI3xg6BDKuBJYA9XwY1/img.png?width=1200&amp;amp;height=648&amp;amp;face=0_0_1200_648,https://scrap.kakaocdn.net/dn/c1nXzU/hyY32jmKXo/OIaKWMH6X7X48BlzlmjGc1/img.png?width=1200&amp;amp;height=648&amp;amp;face=0_0_1200_648&quot;&gt;&lt;a href=&quot;https://huggingface.co/datasets/mercari-us/merrec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://huggingface.co/datasets/mercari-us/merrec&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/jDbNr/hyY1jNEkrx/cQEBI3xg6BDKuBJYA9XwY1/img.png?width=1200&amp;amp;height=648&amp;amp;face=0_0_1200_648,https://scrap.kakaocdn.net/dn/c1nXzU/hyY32jmKXo/OIaKWMH6X7X48BlzlmjGc1/img.png?width=1200&amp;amp;height=648&amp;amp;face=0_0_1200_648');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;mercari-us/merrec &amp;middot; Datasets at Hugging Face&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;MerRec: A Large-scale Multipurpose Mercari Dataset for Consumer-to-Consumer Recommendation Systems This repository contains the dataset accompanying the paper MerRec: A Large-scale Multipurpose Mercari Dataset for Consumer-to-Consumer Recommendation System&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;huggingface.co&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 이 데이터셋이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용량이 아주 크기 때문에, 웬만하면 일부 데이터만 다운 받아 사용하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추천 시스템에 사용되는 알고리즘 / 모델들에 대해서 조사를 좀 해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Collaborative Filtering, Sequential Recommendation, Hybrid System 등 여러 알고리즘이 있었고, 그 모델들을 사용하여 직접 PoC를 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 Two-Tower 모델 PoC를 진행하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Two-Tower 모델은 두 개의 타워를 세워 벡터 임베딩 후 학습을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 데이터에 적용 시키면,&amp;nbsp;&lt;b&gt;User Tower, Item Tower&lt;/b&gt;를 구성한 후 두 타워에서 나온 벡터들을 서로 비교하여 사용자가 어떤 아이템을 좋아할지 예측을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 name 데이터만을 feature로 사용하여 모델 학습을 진행하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1749049580715&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;text_model = SentenceTransformer('all-MiniLM-L6-v2', device=device)
item_titles_list = [item_titles[i] for i in range(num_items)]
item_embeddings = text_model.encode(item_titles_list, show_progress_bar=True, convert_to_tensor=True, device=device)
print(&quot;Item embeddings generated.&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;all-MiniLM 이라는 미리 학습된 SentenceTransformer모델을 사용하여, 아이템의 의미론적 특징을 포착하게 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과는 아래와 같이 나왔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;two-tower(before)2025-06-04 153840.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTxHqS/btsOpQuy6fb/Q4ycOxKnCJYtzoxTFhkEYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTxHqS/btsOpQuy6fb/Q4ycOxKnCJYtzoxTFhkEYK/img.png&quot; data-alt=&quot;two-tower model&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTxHqS/btsOpQuy6fb/Q4ycOxKnCJYtzoxTFhkEYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTxHqS%2FbtsOpQuy6fb%2FQ4ycOxKnCJYtzoxTFhkEYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1500&quot; height=&quot;617&quot; data-filename=&quot;two-tower(before)2025-06-04 153840.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;two-tower model&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과를 보면, 사용자 행동 패턴과 관련 있는 상품도 보이지만, 관련 없는 상품도 상당히 많이 추천이 되고 있는것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;feature로 사용할 데이터를 더 많이 넘겨주어야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 c1_name(category), brand_name을 추가적으로 학습에 사용될 feature로 넘겨주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;two-tower_2025-06-04_153657.png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;722&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AwD6L/btsOraewnwL/3EGdFaTwIVeO3SkQe8FiP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AwD6L/btsOraewnwL/3EGdFaTwIVeO3SkQe8FiP0/img.png&quot; data-alt=&quot;two-tower after&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AwD6L/btsOraewnwL/3EGdFaTwIVeO3SkQe8FiP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAwD6L%2FbtsOraewnwL%2F3EGdFaTwIVeO3SkQe8FiP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;722&quot; data-filename=&quot;two-tower_2025-06-04_153657.png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;722&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;two-tower after&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드라마틱하게 결과가 좋아진 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 과거 관심을 보였던 상품들과 유사하거나, 같은 브랜드의 상품을 추천하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도만 해도 충분히 좋은 결과를 도출했다고 볼 수 있지만, 우리는 사용자의 행동 순서(Sequential) 데이터를 보유하고 있기에 그 데이터를 활용하여 성능을 조금 더 좋게 나올 수 있게 하는 방법을 고민해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 나는 two-tower 와 GRU(Sequential Model) 를 결합하여, Hybrid System을 구축해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, two-tower로 이 수많은 상품들 중 사용자와 연관된 상품들로 필터링 한 후 GRU 모델에게 넘겨준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GRU 모델은 사용자의 행동 패턴, 순서를 기반으로 하여 다음 행동을 예측하고, 그에 따른 추천 상품의 순위를 매겨 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구현한 결과는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2025-06-04 181816.png&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;1120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjNhMW/btsOoVXIn91/TxFu1FZphoe9HIJId0DkeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjNhMW/btsOoVXIn91/TxFu1FZphoe9HIJId0DkeK/img.png&quot; data-alt=&quot;two-tower + GRU&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjNhMW/btsOoVXIn91/TxFu1FZphoe9HIJId0DkeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjNhMW%2FbtsOoVXIn91%2FTxFu1FZphoe9HIJId0DkeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1023&quot; height=&quot;1120&quot; data-filename=&quot;화면 캡처 2025-06-04 181816.png&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;1120&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;two-tower + GRU&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 결과와 뭐가 크게 달라졌는지 잘 모르겠다고 할 수 있겠지만, 이 코드를 잘 다듬어서 기능 별로 적절한 알고리즘을 선택하여 각각 다르게 적용시킬 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>GRU</category>
      <category>msaischool</category>
      <category>PROJECT</category>
      <category>two-tower</category>
      <category>기록</category>
      <category>추천알고리즘</category>
      <category>프로젝트</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/78</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-1%EC%B0%A8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-Recorde-1#entry78comment</comments>
      <pubDate>Thu, 5 Jun 2025 00:19:20 +0900</pubDate>
    </item>
    <item>
      <title>MS AI School 7기 - 우수생 선정 되다 !</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-7%EA%B8%B0-%EC%9A%B0%EC%88%98%EC%83%9D-%EC%84%A0%EC%A0%95-%EB%90%98%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;일주일 정도 전, 1차 프로젝트 시작을 알리는 두번 째 타운홀 미팅이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250604_182853496_02.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d93fSp/btsOoSmdwoG/jfbmqwwBjKwekamdzm58N0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d93fSp/btsOoSmdwoG/jfbmqwwBjKwekamdzm58N0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d93fSp/btsOoSmdwoG/jfbmqwwBjKwekamdzm58N0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd93fSp%2FbtsOoSmdwoG%2FjfbmqwwBjKwekamdzm58N0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; data-filename=&quot;KakaoTalk_20250604_182853496_02.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 미팅도 저번처럼 한국 마이크로소프트 건물에서 진행되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갈때마다 느끼지만, 경복궁이 보이는 이 동네는 참 분위기가 좋은 것 같습니다.(저런 건물에서 일 한다면 매일 야근도 할 수 있을 것 같은...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 미팅때는 바로 다음 날 부터 시작될 1차 프로젝트에 관한 설명, 그리고 팀원들과의 첫 대면 회의를 할 수 있는 시간이 대부분으로 구성되어 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주제 선정부터 시작해서 팀원들과 여러 의견을 주고 받고, 여러 멘토님들에게도 코칭을 받으며 반나절 가까이 회의를 진행했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미팅이 끝나기 한 시간 전, 갑작스러운 중간 평가를 진행한다고 하여 적잖이(사실 아주 많이...) 당황했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 알고보니, 사전에 미리 중간 평가를 진행한다고 공지에 기재가 되어 있었습니다. 공지를 제대로 읽지 않은 저는 그 평가가 우리를 평가하는 것인줄은 꿈에도 몰랐던거죠...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 된거 그냥 그 동안 열심히 공부했던 만큼만 해보자는 마음가짐으로 시험에 임했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 웬걸... 생각보다 점수가 엄청 높게 나와서 저도 놀랐습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 며칠 뒤, 저는 이번 중간 평가 우수 학생으로 선정 되었다는 소식을 듣게 되었습니다. 하하&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250604_182853496.jpg&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vn8Gx/btsOqGEAE7v/ceh8K37oAqzKiqVsMfo5I0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vn8Gx/btsOqGEAE7v/ceh8K37oAqzKiqVsMfo5I0/img.jpg&quot; data-alt=&quot;열심히 했던 보람이랄까..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vn8Gx/btsOqGEAE7v/ceh8K37oAqzKiqVsMfo5I0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVn8Gx%2FbtsOqGEAE7v%2Fceh8K37oAqzKiqVsMfo5I0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1378&quot; height=&quot;216&quot; data-filename=&quot;KakaoTalk_20250604_182853496.jpg&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;열심히 했던 보람이랄까..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수가 높게 나오긴 했지만, 수강생들 중 가장 높을거라곤 전혀 예상도 못했어서 그런지 기분이 정말 좋더군요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 1차 프로젝트를 진행 중입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_KakaoTalk_20250604_182853496_01.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tYPn2/btsOpE8SdmC/Do7MbGSu49KDIdyuVWVGSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tYPn2/btsOpE8SdmC/Do7MbGSu49KDIdyuVWVGSK/img.png&quot; data-alt=&quot;미팅 후 팀 결성 기념 사진(?)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tYPn2/btsOpE8SdmC/Do7MbGSu49KDIdyuVWVGSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtYPn2%2FbtsOpE8SdmC%2FDo7MbGSu49KDIdyuVWVGSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;2250&quot; data-filename=&quot;edited_KakaoTalk_20250604_182853496_01.jpg&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;미팅 후 팀 결성 기념 사진(?)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 포스트 부터는 현재 진행 중인 프로젝트에 관한 내용에 대해서 중간 중간 포스팅 할 예정입니다.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>AI</category>
      <category>Microsoft</category>
      <category>msaischool</category>
      <category>부트캠프</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/77</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-7%EA%B8%B0-%EC%9A%B0%EC%88%98%EC%83%9D-%EC%84%A0%EC%A0%95-%EB%90%98%EB%8B%A4#entry77comment</comments>
      <pubDate>Wed, 4 Jun 2025 18:52:06 +0900</pubDate>
    </item>
    <item>
      <title>[ML] Linear Regression 정리</title>
      <link>https://cases.tistory.com/entry/ML-Linear-Regression-%EC%A0%95%EB%A6%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Linear Regression(선형 회귀)는 독립 변수와 종속 변수 사이의 관계를 직선 형태로 모델링 하는 기법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;y = wx + b&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;w&lt;/span&gt;&lt;span&gt;: (weight)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;b&lt;/span&gt;&lt;span&gt;: (bias)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;기본 식은 위와 같다고 볼 수 있지만, feature의 수가 늘어나면 다차원 형태의 식이 될 수도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;weight는 각각의 feature의 importance에 따라 값이 달라지기 때문에 feature의 값 만큼 weight의 개수도 달라진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;여기서 우리는 최적의 weight 값을 찾는 것을 목표로 모델 학습을 진행한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Linear Regression을 어디에 쓸 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 공부 시간과 내 시험 점수, 집 평수에 따른 월세 가격, 나이에 따른 실업률 추정 등등이 Linear Regression의 예시가 될 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-26 오후 9.04.30.png&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctWRu6/btsOcoFWAuy/TAoxKl4D0dAeJqhDKnJRpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctWRu6/btsOcoFWAuy/TAoxKl4D0dAeJqhDKnJRpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctWRu6/btsOcoFWAuy/TAoxKl4D0dAeJqhDKnJRpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctWRu6%2FbtsOcoFWAuy%2FTAoxKl4D0dAeJqhDKnJRpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;521&quot; height=&quot;320&quot; data-filename=&quot;스크린샷 2025-05-26 오후 9.04.30.png&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프로 보았을때, 저 빨간점들과 파란선(예측선)의 차이가 최소가 되는 직선을 찾는것이 Linear Regression 모델 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 예측선은 w(weight)값을 조정하여 변화시킬 수 있고, 최적의 w값을 찾는것이 모델 학습의 목적이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 최적의 weight값을 어떻게 찾을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 우리는 RSS와 MSE에 대해서 알고 넘어가야 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;RSS&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RSS는 잔차 제곱 합을 뜻하며, 쉽게 말해 오류 값의 제곱을 구한 다음, 더해주는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 RSS 값이 최소가 되는 방향으로 weight 값을 찾는게 모델링의 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;MSE&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSE는 평균제곱오차 를 뜻하며, RSS를 학습 데이터의 수로 나눠준 것을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회귀에서는 MSE를 비용 함수(Cost function) 또는 손실 함수(Loss function)이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 비용함수를 최소화 하는 것을 목표로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 MSE를 최소화 하는 방법이 뭘까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그건 바로 미분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형 회귀에서의 weight(가중치)는 feature(입력 변수)의 수에 따라 &lt;span&gt;w1, w2, w3, ...&lt;/span&gt; 형태로 벡터로 존재하게&lt;span&gt;&amp;nbsp;된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;MSE를 최소화하기 위해서는, 이 각각의 가중치 w에 대해 MSE를 편미분한 값이 0이 되도록 만드는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;즉, 전체 MSE 함수에 대해 모든 가중치에 대한 gradient(기울기)가 0이 되는 지점을 찾는 것이 곧, MSE를 최소화하는 최적의 weight 값을 찾는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 방법 중 가장 널리 쓰이는게 바로 &lt;b&gt;Gradient Descent Method(경사 하강법)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-26 오후 11.51.07.png&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ebgpkU/btsOcbGSEWn/sqdHfOzdbkcvriPTrXUHNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ebgpkU/btsOcbGSEWn/sqdHfOzdbkcvriPTrXUHNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ebgpkU/btsOcbGSEWn/sqdHfOzdbkcvriPTrXUHNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FebgpkU%2FbtsOcbGSEWn%2FsqdHfOzdbkcvriPTrXUHNK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1426&quot; height=&quot;245&quot; data-filename=&quot;스크린샷 2025-05-26 오후 11.51.07.png&quot; data-origin-width=&quot;1426&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSE가 크면 클수록 다음 weight는 작은 쪽으로 보내야 하고, 작으면 작을 수록 weight는 큰 쪽으로 보내야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, learning rate는 가중치를 얼마나 크게 또는 작게 업데이트 할지를 결정하는 파라미터 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값이 클 수록 변화폭은 더 커지게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 learning rate의 값을 처음부터 크게 줘서 바로 0을 찾아가게 만들면 안되나? 라는 생각이 들 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 너무 큰 값을 주게 되면 최솟값을 지나쳐서 멀리 가버릴 수도 있다. 그렇게 되면 Loss가 오히려 증가하는 경우도 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로, 너무 작은 값을 주게 되면 학습 속도가 매우 느려서 오랜 시간 학습을 했지만, 최솟값에 도달하지 않는 결과를 초래할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-26 오후 11.55.28.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4pVGo/btsOdD3f1pU/oP66Be2tVgKn9lin6iyK2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4pVGo/btsOdD3f1pU/oP66Be2tVgKn9lin6iyK2k/img.png&quot; data-alt=&quot;learning rate 잘못된 예&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4pVGo/btsOdD3f1pU/oP66Be2tVgKn9lin6iyK2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4pVGo%2FbtsOdD3f1pU%2FoP66Be2tVgKn9lin6iyK2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1246&quot; height=&quot;613&quot; data-filename=&quot;스크린샷 2025-05-26 오후 11.55.28.png&quot; data-origin-width=&quot;1246&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;learning rate 잘못된 예&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 우리는 적절한 learning rate의 값을 잘 찾아서 입력을 해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파라미터가 매우 중요하기 때문에 learning rate의 값을 찾기 위한 여러가지 방법들이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로는 큰 값부터 넣은 후, 값을 조금씩 감소 시키면서 실험을 해보는 방법을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;Cyclical Learning Rates for Training Neural Networks&quot; href=&quot;https://arxiv.org/abs/1506.01186&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://arxiv.org/abs/1506.01186&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1748272149651&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Cyclical Learning Rates for Training Neural Networks&quot; data-og-description=&quot;It is known that the learning rate is the most important hyper-parameter to tune for training deep neural networks. This paper describes a new method for setting the learning rate, named cyclical learning rates, which practically eliminates the need to exp&quot; data-og-host=&quot;arxiv.org&quot; data-og-source-url=&quot;https://arxiv.org/abs/1506.01186&quot; data-og-url=&quot;https://arxiv.org/abs/1506.01186v6&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lqCQv/hyYYzJxirn/DSjuZlTANbgD2kmo733gIk/img.png?width=1200&amp;amp;height=700&amp;amp;face=0_0_1200_700,https://scrap.kakaocdn.net/dn/fCX1T/hyY0tnE6h1/S0gbSJWljXK2EtLDZBVUkK/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/1506.01186&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://arxiv.org/abs/1506.01186&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lqCQv/hyYYzJxirn/DSjuZlTANbgD2kmo733gIk/img.png?width=1200&amp;amp;height=700&amp;amp;face=0_0_1200_700,https://scrap.kakaocdn.net/dn/fCX1T/hyY0tnE6h1/S0gbSJWljXK2EtLDZBVUkK/img.png?width=1000&amp;amp;height=1000&amp;amp;face=0_0_1000_1000');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Cyclical Learning Rates for Training Neural Networks&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;It is known that the learning rate is the most important hyper-parameter to tune for training deep neural networks. This paper describes a new method for setting the learning rate, named cyclical learning rates, which practically eliminates the need to exp&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;arxiv.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 논문에서는 &lt;span data-token-index=&quot;0&quot;&gt;Cyclical learning rates (순환학습률, CLR) 이라는 방법을 소개하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5f656c; text-align: start;&quot;&gt;최대 학습률과 최저 학습률 사이 값을 순환하게 하는 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #5f656c; text-align: start;&quot;&gt;대표적인 3가지 모드를 소개하자면,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1.&lt;span&gt; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;triangular&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 기본적인 형태: learning rate가 base ↗ max ↘ base 형태로 반복&lt;/li&gt;
&lt;li&gt;일정한 높이의 삼각형 패턴을 반복함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2.&lt;span&gt; &amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;triangular2&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;triangular&lt;/span&gt;&lt;span&gt;와 동일하지만, &lt;/span&gt;&lt;b&gt;반복할수록 amplitude를 절반으로 감소시킴&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;exp_range&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;반복할 때마다 max_lr를 &lt;span&gt;&lt;b&gt;지수적으로 감소&lt;/b&gt;&lt;/span&gt;시킴&lt;/li&gt;
&lt;li&gt;주기 안에서는 증가/감소를 반복하되, 전체적으로는 감소함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cyclical learning rates의 장점은 따로 learning rate를 찾지 않아도 돼서, 하이퍼파라미터 튜닝 시간을 단축 시켜주고, 수렴이 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 overfitting을 방지할 수 있다는 장점도 있다.&lt;/p&gt;</description>
      <category>ML</category>
      <category>Learning Rate</category>
      <category>Linear Regression</category>
      <category>경사하강법</category>
      <category>머신러닝</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/76</guid>
      <comments>https://cases.tistory.com/entry/ML-Linear-Regression-%EC%A0%95%EB%A6%AC#entry76comment</comments>
      <pubDate>Tue, 27 May 2025 00:16:32 +0900</pubDate>
    </item>
    <item>
      <title>[ML] Azure 클라우드 기반 머신러닝 실습 - 다중 선형 회귀</title>
      <link>https://cases.tistory.com/entry/ML-Azure-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EA%B8%B0%EB%B0%98-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EC%8B%A4%EC%8A%B5-%EB%8B%A4%EC%A4%91-%EC%84%A0%ED%98%95-%ED%9A%8C%EA%B7%80</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 자전거 대여에 관한 정보 및 데이터들을 수집하여, 자전거 렌탈 수요 예측 모델을 만들어 보는게 목표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure에서 제공하는 Machine Learning Studio로 실습을 진행할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Azure에서는 데이터의 흐름과 모델 학습까지의 머신러닝을 파이프라인으로 구성할 수 있어, 사용자에게 조금 더 복잡하지 않고, 직관적으로 보여주기 때문에 머신러닝에 익숙하지 않은 사람들도 쉽게 이용을 할 수 있게 서비스를 제공하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;UCI&quot;&lt;/b&gt; 사이트에 가면 여러가지 데이터들을 받아서 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 그 중 &quot;Bike Sharing Dataset&quot;을 이용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실습에 들어가기 앞서, &lt;b&gt;다중 선형 회귀(Multi Linear Regression)&lt;/b&gt;에 대해서 간략히 설명을 하고 넘어가보자.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;다중 선형 회귀&lt;b&gt;(Multi Linear Regression)&lt;/b&gt;&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 선형 회귀는 단순 선형 회귀와 달리, 종속변수가 &lt;b&gt;여러 독립변수&lt;/b&gt;에 의해 영향을 받는 경우에 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 예를 들어, 시험 점수 예측 모델을 만든다고 할 때, 하나의 요소만 영향을 주는 것이 아니라,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공부 시간, 수면 시간, 출석률 등 여러개의 독립 변수(x들)가 결과(y)에 영향을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로&amp;nbsp;&lt;b&gt;다중 선형 회귀&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 이번 실습에서는 왜 다중 선형 회귀 모델이어야 할까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;종속 변수(y) = 자전거 수요&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;독립 변수(x) = 기온, 습도, 바람, 계절, 휴일 여부 등...&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯, 여러 개의 독립 변수들이 영향을 주기 때문에 다중 선형 회귀 모델이 적합하다고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_스크린샷 2025-05-14 오전 12.36.38.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RWN7Q/btsNVbfViLY/G4KsXngPoYk0CZnAAFcdOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RWN7Q/btsNVbfViLY/G4KsXngPoYk0CZnAAFcdOK/img.png&quot; data-alt=&quot;Azure Machine Learning Studio&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RWN7Q/btsNVbfViLY/G4KsXngPoYk0CZnAAFcdOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRWN7Q%2FbtsNVbfViLY%2FG4KsXngPoYk0CZnAAFcdOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1314&quot; height=&quot;868&quot; data-filename=&quot;edited_스크린샷 2025-05-14 오전 12.36.38.png&quot; data-origin-width=&quot;1314&quot; data-origin-height=&quot;868&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Azure Machine Learning Studio&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, Azure Machine Learning Studio의 Data 탭 상단의 Create 버튼을 클릭하여, 준비한 csv파일을 업로드 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Compute 인스턴스는 생성을 하고 작업을 해야, 나중에 작업 후 모델을 돌려볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-14 오전 12.42.48.png&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;613&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1lKT4/btsNWiLKUV2/bWJ69ZEtSI3jImkTkBeaG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1lKT4/btsNWiLKUV2/bWJ69ZEtSI3jImkTkBeaG0/img.png&quot; data-alt=&quot;Designer 탭&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1lKT4/btsNWiLKUV2/bWJ69ZEtSI3jImkTkBeaG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1lKT4%2FbtsNWiLKUV2%2FbWJ69ZEtSI3jImkTkBeaG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;963&quot; height=&quot;613&quot; data-filename=&quot;스크린샷 2025-05-14 오전 12.42.48.png&quot; data-origin-width=&quot;963&quot; data-origin-height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Designer 탭&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compute 인스턴스, Data 까지 모두 준비가 되었다면, 이제 Designer 탭에서 작업을 시작해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-14 오전 12.47.22.png&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;879&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSCG10/btsNUFhdo0q/4GUSEqSnxJ2azK7uV3WKK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSCG10/btsNUFhdo0q/4GUSEqSnxJ2azK7uV3WKK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSCG10/btsNUFhdo0q/4GUSEqSnxJ2azK7uV3WKK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSCG10%2FbtsNUFhdo0q%2F4GUSEqSnxJ2azK7uV3WKK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1054&quot; height=&quot;879&quot; data-filename=&quot;스크린샷 2025-05-14 오전 12.47.22.png&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;879&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌측에 보이는 Data 리스트에서 내가 업로드 한 Dataset을 드래그로 가져와 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음, Component 탭으로 가서, Select Columns in Dataset 을 가져와서 연결해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 데이터 파일에서 특성(feature)으로 사용할 열만 선택해주는 기능을 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-14 오전 1.01.56.png&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blEENf/btsNUPYmr3z/eGKgAkuzM3vm5JO4kFzfkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blEENf/btsNUPYmr3z/eGKgAkuzM3vm5JO4kFzfkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blEENf/btsNUPYmr3z/eGKgAkuzM3vm5JO4kFzfkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblEENf%2FbtsNUPYmr3z%2FeGKgAkuzM3vm5JO4kFzfkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;306&quot; height=&quot;363&quot; data-filename=&quot;스크린샷 2025-05-14 오전 1.01.56.png&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;363&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 day, year 컬럼은 모델 학습에 영향이 없는 데이터 이므로, 두 개의 컬럼을 뺀 나머지 컬럼들을 모두 select 해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 범주형 데이터 -&amp;gt; Categorical 로 변환해주는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Edit Metadata&amp;nbsp;&lt;/b&gt;컴포넌트를 가져와서 변환할 범주형 데이터를 select 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mnth, season, holiday, weekday, workingday, weatersit 컬럼을 select 한 후, Categorical로 변경해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-15 오후 11.50.20.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCuU8/btsNZGS8efY/2p3ePIfhjOmHzYhg20FRFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCuU8/btsNZGS8efY/2p3ePIfhjOmHzYhg20FRFk/img.png&quot; data-alt=&quot;Categorical 변환&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCuU8/btsNZGS8efY/2p3ePIfhjOmHzYhg20FRFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCuU8%2FbtsNZGS8efY%2F2p3ePIfhjOmHzYhg20FRFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;846&quot; height=&quot;453&quot; data-filename=&quot;스크린샷 2025-05-15 오후 11.50.20.png&quot; data-origin-width=&quot;846&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Categorical 변환&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 categorical로 변환된 컬럼들을 one-hot 인코딩 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 해줘야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는, 머신러닝 알고리즘(특히 회귀, SVM, 트리 기반 모델 등)은 대부분 &lt;span&gt;&lt;b&gt;숫자만 처리 가능&lt;/b&gt;하기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;Convert to indicator values &lt;/b&gt;컴포넌트를 사용하여, one-hot 인코딩을 해주면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 수치형 데이터에 대해서 정규화를 진행해줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;정규화 및 표준화를 진행하는 이유를 이번 실습 데이터로 예들 들자면, 이 데이터들의 특성 중 온도, 체감온도, 풍속, 강수량 등은&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;서로 단위, 분포가 다르다. 이 데이터들을 그대로 학습 시키게 되면 모델 학습에 끼치는 영향력이 균등할 수 없기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.13.51.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o5YtO/btsN0ixqbW6/f1Lw9xEYjdmsZZk2wtTdfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o5YtO/btsN0ixqbW6/f1Lw9xEYjdmsZZk2wtTdfk/img.png&quot; data-alt=&quot;데이터 정규화&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o5YtO/btsN0ixqbW6/f1Lw9xEYjdmsZZk2wtTdfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo5YtO%2FbtsN0ixqbW6%2Ff1Lw9xEYjdmsZZk2wtTdfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;743&quot; height=&quot;379&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.13.51.png&quot; data-origin-width=&quot;743&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 정규화&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Normalize Data 컴포넌트로 정규화를 진행해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제&amp;nbsp;&lt;b&gt;Split Data&lt;/b&gt;로 &lt;b&gt;train data, test data&amp;nbsp;&lt;/b&gt;를 나누어 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 70%를 학습데이터로 할당해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음, Linear Regression 알고리즘을 선택해주고, Train model에 학습데이터와 해당 알고리즘을 연결해준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.18.39.png&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ehTZGP/btsNXPqrEBX/Io26WNAIhJUiH3JMjsKt00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ehTZGP/btsNXPqrEBX/Io26WNAIhJUiH3JMjsKt00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ehTZGP/btsNXPqrEBX/Io26WNAIhJUiH3JMjsKt00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FehTZGP%2FbtsNXPqrEBX%2FIo26WNAIhJUiH3JMjsKt00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;487&quot; height=&quot;358&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.18.39.png&quot; data-origin-width=&quot;487&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Train model에서는&amp;nbsp;&lt;b&gt;Label column&lt;/b&gt;을 지정해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습의 Label column은 rentals 이므로, 이를 지정해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.20.40.png&quot; data-origin-width=&quot;517&quot; data-origin-height=&quot;135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjhG02/btsN0hrQCmA/WwXjWHkpmewRCSmIFHHWl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjhG02/btsN0hrQCmA/WwXjWHkpmewRCSmIFHHWl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjhG02/btsN0hrQCmA/WwXjWHkpmewRCSmIFHHWl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjhG02%2FbtsN0hrQCmA%2FWwXjWHkpmewRCSmIFHHWl0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;517&quot; height=&quot;135&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.20.40.png&quot; data-origin-width=&quot;517&quot; data-origin-height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Score Model로 점수를 매기고, 모델 평가를 해주는 단계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 평가는 Evaluate Model 컴포넌트로 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.22.45.png&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LVfOc/btsNX3h3iA3/6Wn6jS5Lg4zfIyxJifocs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LVfOc/btsNX3h3iA3/6Wn6jS5Lg4zfIyxJifocs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LVfOc/btsNX3h3iA3/6Wn6jS5Lg4zfIyxJifocs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLVfOc%2FbtsNX3h3iA3%2F6Wn6jS5Lg4zfIyxJifocs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;447&quot; height=&quot;316&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.22.45.png&quot; data-origin-width=&quot;447&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 완료했다면, Job을 돌려주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Job을 돌리기 전 항상 &lt;b&gt;compute 인스턴스가 running 상태&lt;/b&gt;인지 확인하고 돌리기를 권한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.26.11.png&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRhkwW/btsNXTzCin8/NcMiZdKZO3fZOq1KcldOMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRhkwW/btsNXTzCin8/NcMiZdKZO3fZOq1KcldOMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRhkwW/btsNXTzCin8/NcMiZdKZO3fZOq1KcldOMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRhkwW%2FbtsNXTzCin8%2FNcMiZdKZO3fZOq1KcldOMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;557&quot; height=&quot;419&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.26.11.png&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jobs 탭에서 현재 진행상황을 확인 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 사진처럼 위에서 부터 순차적으로 진행이 되고 있는것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 진행이 완료되고 나면, Evaluate Model에서 평가 지표를 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.27.59.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OTZFN/btsNZIKaUwD/UihCeiMPBOXTr94bP7YHa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OTZFN/btsNZIKaUwD/UihCeiMPBOXTr94bP7YHa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OTZFN/btsNZIKaUwD/UihCeiMPBOXTr94bP7YHa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOTZFN%2FbtsNZIKaUwD%2FUihCeiMPBOXTr94bP7YHa1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;912&quot; height=&quot;287&quot; data-filename=&quot;스크린샷 2025-05-16 오전 12.27.59.png&quot; data-origin-width=&quot;912&quot; data-origin-height=&quot;287&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지표상으로 보면 그렇게 잘 나온 것 같지는 않다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 지표를 확인하고, 다시 전처리 과정이나 알고리즘 선택, 하이퍼 파라미터 등을 수정해 가면서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지표가 더 좋게 나오는 방향으로 업그레이드 해 나가는 것이 머신러닝의 전반적인 과정이라고 볼 수 있다.&lt;/p&gt;</description>
      <category>ML</category>
      <category>aischool</category>
      <category>Azure</category>
      <category>ml</category>
      <category>마이크로소프트</category>
      <category>머신러닝</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/75</guid>
      <comments>https://cases.tistory.com/entry/ML-Azure-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EA%B8%B0%EB%B0%98-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EC%8B%A4%EC%8A%B5-%EB%8B%A4%EC%A4%91-%EC%84%A0%ED%98%95-%ED%9A%8C%EA%B7%80#entry75comment</comments>
      <pubDate>Fri, 16 May 2025 00:31:41 +0900</pubDate>
    </item>
    <item>
      <title>MS AI School 7기 한 달차 후기</title>
      <link>https://cases.tistory.com/entry/MS-AI-School-7%EA%B8%B0-%ED%95%9C-%EB%8B%AC%EC%B0%A8-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 수업을 따라가느라 엄청 바쁜 하루 하루를 보내고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 azure 클라우드 기반 머신러닝을 배우는 중인데, 수학적 개념까지 들어가니 조금 난이도가 있는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 수업 외적으로 수학 공부를 병행하고 있는 중인데, 수업 내용을 이해하는데에 도움이 많이 되는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 나의 일상 루틴 :&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;09 : 00 - 18 : 00 - AI School 수업&lt;/li&gt;
&lt;li&gt;18 : 00 - 19 : 00 - 수업 내용 복습&lt;/li&gt;
&lt;li&gt;19 : 00 - 21 : 00 - 저녁 식사 및 휴식&lt;/li&gt;
&lt;li&gt;21 : 00 - 22 : 00 - 프로그래머스 문제 풀이&lt;/li&gt;
&lt;li&gt;22 : 00 - 24 : 00 - 수학 공부&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 이런 식으로 매일을 보내는 것 같다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하루에 자는 시간을 제외 하고는 거의 책상 앞에 앉아 있다 보니, 허리가 많이 안좋아진 것이 느껴진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-13 오후 2.52.05.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;922&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uxrMM/btsNVgU9KDq/LOh11OmDPmpebHNQUFUgm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uxrMM/btsNVgU9KDq/LOh11OmDPmpebHNQUFUgm0/img.png&quot; data-alt=&quot;프로그래머스 내가 푼 문제&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uxrMM/btsNVgU9KDq/LOh11OmDPmpebHNQUFUgm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuxrMM%2FbtsNVgU9KDq%2FLOh11OmDPmpebHNQUFUgm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1490&quot; height=&quot;922&quot; data-filename=&quot;스크린샷 2025-05-13 오후 2.52.05.png&quot; data-origin-width=&quot;1490&quot; data-origin-height=&quot;922&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프로그래머스 내가 푼 문제&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬에 익숙해지기 위해서 한 달 동안 프로그래머스에서 문제를 조금씩이라도 매일 풀려고 노력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 모든 문제를 스스로의 힘만으로 풀었다고는 말 못하지만, 풀이를 찾아보며 코드를 한 줄씩 이해하며 다시 푸는 과정을 반복한 결과...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 어느정도 파이썬에 조금 익숙해진 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 시간이 조금 여유가 생길때, 알고리즘 강의를 조금 더 들으며, Lv1 ~ Lv2 문제들도 도전해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(지금은 수업 내용 조차 따라가기 힘들어서, 코딩테스트 공부는 조금 후순위로 미뤄뒀다...)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 수학 공부를 하고 있는 방법에 대해서 살짝 소개를 하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Khan Academy라는 사이트에서 강의를 들으며 공부하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.khanacademy.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ko.khanacademy.org/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1747115927844&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Khan Academy&quot; data-og-description=&quot;&quot; data-og-host=&quot;ko.khanacademy.org&quot; data-og-source-url=&quot;https://ko.khanacademy.org/&quot; data-og-url=&quot;https://ko.khanacademy.org/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://ko.khanacademy.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ko.khanacademy.org/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Khan Academy&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ko.khanacademy.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-13 오후 3.00.17.png&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boMo6F/btsNVLmQKzN/70TnXzNL6Spsv6zGLwaWsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boMo6F/btsNVLmQKzN/70TnXzNL6Spsv6zGLwaWsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boMo6F/btsNVLmQKzN/70TnXzNL6Spsv6zGLwaWsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboMo6F%2FbtsNVLmQKzN%2F70TnXzNL6Spsv6zGLwaWsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;842&quot; height=&quot;676&quot; data-filename=&quot;스크린샷 2025-05-13 오후 3.00.17.png&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 처럼 인공지능 공부에 필요한 선형대수학, 확률과 통계, 미적분학 등을 학습할 수 있게, 강의가 제공 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 강의이긴 하지만, 한글 번역을 지원하고 있어서 학습에 큰 문제는 없고, 퀄리티도 생각보다 좋아서 애용하고 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 달 말에는 또 타운홀 미팅이 잡혀있고, 말 일에는 바로 첫 번째 프로젝트가 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI School 과정을 진행하면서, 조금 불만이었던 점을 말하자면...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 강사가 너무 자주 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 각 과정별로 특화된 강사님이 들어오신다면 환영이겠지만, 딱히 그런것 같지도 않을 뿐더러, 강의력에 상당히 의구심이 드는 강사님도 계셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 강사가 너무 자주 바뀔때, 강사간의 커뮤니케이션이 잘 되어 어느 부분을 얼만큼 배웠는지를 잘 인지하고 있다면 그나마 좀 낫겠지만, 서로 어떤 수업을 진행했는지도 잘 모르시는 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 소수의 강사님을 제외하면 대부분 높은 퀄리티의 수업을 진행해주셔서 나름 만족하면서 열심히 듣고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 배울 내용이 훨씬 더 많이 남아있기 때문에, 나의 목표를 향해 열심히 달려봐야지.&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>Academy</category>
      <category>AI</category>
      <category>Microsoft</category>
      <category>msaischool</category>
      <category>머신러닝</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/74</guid>
      <comments>https://cases.tistory.com/entry/MS-AI-School-7%EA%B8%B0-%ED%95%9C-%EB%8B%AC%EC%B0%A8-%ED%9B%84%EA%B8%B0#entry74comment</comments>
      <pubDate>Tue, 13 May 2025 20:50:44 +0900</pubDate>
    </item>
    <item>
      <title>[ML] 머신러닝 모델 검증을 위한 레코드 분할</title>
      <link>https://cases.tistory.com/entry/ML-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EB%AA%A8%EB%8D%B8-%EA%B2%80%EC%A6%9D%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%A0%88%EC%BD%94%EB%93%9C-%EB%B6%84%ED%95%A0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 배운 내용을 복습할 겸, 머신러닝 모델 검증을 위한 레코드 분할에 대해서 다뤄 보려고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;학습용과 검증용 데이터 나누기&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 분할은 예측 모델을 평가할 때 필요한 전처리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습 데이터와 검증 데이터는 같은 전처리를 적용해야 하며(스케일링, 결측치 처리 등), 되도록 같은 데이터로 묶어서 다루고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예측 모델에 입력하기 직전 분할하는 것이 적절하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적용 데이터는 정답을 알 수 없는 상태에서 사용하는 데이터이다. 그러므로 흐름이나 수집 시점이 다르기 때문에, 분할할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말해서, 어차피 &lt;b&gt;&quot;평가용&quot;&lt;/b&gt; 데이터가 아니고, 실제로 사용 되는&amp;nbsp;&lt;b&gt;&quot;운영용&quot; &lt;/b&gt;데이터니까 분할할 필요가 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-05-07 오후 10.35.36.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhxJYZ/btsNNEVOF6n/ysJnmjjloKLmj4k1dmvIa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhxJYZ/btsNNEVOF6n/ysJnmjjloKLmj4k1dmvIa0/img.png&quot; data-alt=&quot;데이터 레코드 분할&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhxJYZ/btsNNEVOF6n/ysJnmjjloKLmj4k1dmvIa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhxJYZ%2FbtsNNEVOF6n%2FysJnmjjloKLmj4k1dmvIa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;740&quot; data-filename=&quot;스크린샷 2025-05-07 오후 10.35.36.png&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 레코드 분할&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 이미지와 같이 교차 검증은 데이터를 검증용 데이터 / 학습용 데이터로 구분한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746626586137&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold

train_data, test_data, train_target, test_target = \
    train_test_split(production_tb.drop('fault_flg', axis=1),
                     production_tb[['fault_flg']],
                     test_size=0.2)

train_data.reset_index(inplace=True, drop=True)
test_data.reset_index(inplace=True, drop=True)
train_target.reset_index(inplace=True, drop=True)
test_target.reset_index(inplace=True, drop=True)

print(train_data)
print(train_target)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델 학습시, 정답이 없어야 하므로 fault_flg 컬럼을 drop 해주고, 테스트 데이터는 20%로 잡아주었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;결과&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746626926569&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type      length  thickness
0      E  226.187113  21.029733
1      B  133.814427  14.083716
2      B  200.142637   2.917706
3      D  142.887310  16.560487
4      E  289.162995  30.476145
..   ...         ...        ...
795    C  212.354175  38.857996
796    D  223.806149  19.650766
797    A  198.988129  31.631090
798    E  162.463041  20.752650
799    B  124.966171   8.337629

[800 rows x 3 columns]
     fault_flg
0        False
1        False
2        False
3        False
4        False
..         ...
795       True
796      False
797      False
798      False
799      False

[800 rows x 1 columns]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 잘 되었다면, 교차 검증을 위해 분할을 해줄 차례이다.&lt;/p&gt;
&lt;pre id=&quot;code_1746627076166&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;row_no_list = list(range(len(train_target)))

# 교차 검증을 위한 분할
k_fold = KFold(n_splits=4, shuffle=True)

for train_cv_no, test_cv_no in k_fold.split(row_no_list) :
    train_cv = train_data.iloc[train_cv_no, :]
    print(train_cv)
    test_cv = train_data.iloc[test_cv_no, :]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;결과&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746627200183&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;type      length  thickness
0      E  226.187113  21.029733
1      B  133.814427  14.083716
2      B  200.142637   2.917706
3      D  142.887310  16.560487
5      A   81.478578   8.840798
..   ...         ...        ...
795    C  212.354175  38.857996
796    D  223.806149  19.650766
797    A  198.988129  31.631090
798    E  162.463041  20.752650
799    B  124.966171   8.337629

[600 rows x 3 columns]
    type      length  thickness
2      B  200.142637   2.917706
3      D  142.887310  16.560487
4      E  289.162995  30.476145
6      A  135.440179  14.673518
8      E  157.480341  31.257903
..   ...         ...        ...
794    E  142.888025   2.229418
796    D  223.806149  19.650766
797    A  198.988129  31.631090
798    E  162.463041  20.752650
...
795    C  212.354175  38.857996
796    D  223.806149  19.650766

[600 rows x 3 columns]&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ML</category>
      <category>ml</category>
      <category>검증</category>
      <category>레코드분할</category>
      <category>머신러닝</category>
      <category>예측모델</category>
      <category>전처리</category>
      <category>평가</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/73</guid>
      <comments>https://cases.tistory.com/entry/ML-%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-%EB%AA%A8%EB%8D%B8-%EA%B2%80%EC%A6%9D%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%A0%88%EC%BD%94%EB%93%9C-%EB%B6%84%ED%95%A0#entry73comment</comments>
      <pubDate>Wed, 7 May 2025 23:16:28 +0900</pubDate>
    </item>
    <item>
      <title>[Kaggle] Titanic (데이터 전처리, Machine Learning)</title>
      <link>https://cases.tistory.com/entry/Kaggle-Kaggle-Competitions-%ED%83%80%EC%9D%B4%ED%83%80%EB%8B%89-%EC%83%9D%EC%A1%B4%EC%9E%90-%EC%98%88%EC%B8%A1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kaggle의 Competitions에서는 여러가지 주제의 데이터들을 가지고, 사람들과 데이터 분석, 모델 학습을 통해 경쟁을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 오늘 처음으로 Kaggle을 사용해보아서, 비교적 쉬운 난이도의 데이터를 한 번 다뤄보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;Titanic - Machine Learning from Disaster&quot; href=&quot;https://kaggle.com/competitions/titanic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kaggle.com/competitions/titanic&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746431527645&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;profile&quot; data-og-title=&quot;Titanic | Novice&quot; data-og-description=&quot;Kaggle profile for Titanic&quot; data-og-host=&quot;www.kaggle.com&quot; data-og-source-url=&quot;https://kaggle.com/competitions/titanic&quot; data-og-url=&quot;https://kaggle.com/titanic&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/siRVC/hyYMPllTaO/lVcokg35IjG1zbKmxw7bZ1/img.png?width=193&amp;amp;height=192&amp;amp;face=0_0_193_192&quot;&gt;&lt;a href=&quot;https://kaggle.com/competitions/titanic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://kaggle.com/competitions/titanic&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/siRVC/hyYMPllTaO/lVcokg35IjG1zbKmxw7bZ1/img.png?width=193&amp;amp;height=192&amp;amp;face=0_0_193_192');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Titanic | Novice&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kaggle profile for Titanic&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.kaggle.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영화 타이타닉을 주제로 만든 데이터이고, gender_submission.csv, test.csv, train.csv. 이렇게 총 3개의 파일이 들어있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; gender_submission.csv : 예시 제출 파일&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt; test.csv : 예측 데이터&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;train.csv : 모델 학습용 데이터&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정을 생각해보자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;train.csv 데이터를 전처리 후 모델에게 학습 -&amp;gt; test.csv로 학습된 모델을 테스트 -&amp;gt; 결과를 예시 파일처럼 만든 후 제출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 분석&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kaggle에서는 Jupyter Notebook 환경을 지원하기 때문에 굳이 로컬에서 작업을 할 필요가 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1746433103174&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;train = pd.read_csv('/kaggle/input/titanic/train.csv')
test = pd.read_csv('/kaggle/input/titanic/test.csv')

train.head()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;train 데이터의 각 컬럼명을 찾아보자면 아래와 같이 설명할 수 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PassengerId&lt;/td&gt;
&lt;td&gt;탑승객 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survived&lt;/td&gt;
&lt;td&gt;생존 여부 (0 = 사망, 1 = 생존) &amp;larr; 우리가 예측할 대상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pclass&lt;/td&gt;
&lt;td&gt;티켓 등급 (1등석, 2등석, 3등석)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name&lt;/td&gt;
&lt;td&gt;이름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sex&lt;/td&gt;
&lt;td&gt;성별&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Age&lt;/td&gt;
&lt;td&gt;나이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SibSp&lt;/td&gt;
&lt;td&gt;같이 탄 형제/배우자 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parch&lt;/td&gt;
&lt;td&gt;같이 탄 부모/자식 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket&lt;/td&gt;
&lt;td&gt;티켓 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fare&lt;/td&gt;
&lt;td&gt;요금&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cabin&lt;/td&gt;
&lt;td&gt;객실 번호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embarked&lt;/td&gt;
&lt;td&gt;탑승한 항구 (C, Q, S)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 결측치 값들을 한 번 살펴보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1746433430392&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;train.isnull().sum()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;결과&amp;gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: left;&quot;&gt;&lt;code&gt;PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Age, Cabin, Embarked 에 결측치들이 있는 것을 확인했으니, 나중에 그 결측치들을 어떤 값으로 채울지 생각을 해보아야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 성별, 티켓 등급, 탑승 항구 등에 따라서 생존률이 얼마나 됐는지 살펴보기 위해서 아래와 같이 코드를 작성했다.&lt;/p&gt;
&lt;pre id=&quot;code_1746433793236&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 성별
train.groupby('Sex')['Survived'].mean()
# 티켓등급
train.groupby('Pclass')['Survived'].mean()
# 탑승항구
train.groupby('Embarked')['Survived'].mean()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;결과&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1746433850548&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Sex
female    0.742038
male      0.188908
Name: Survived, dtype: float64
##################################
Pclass
1    0.629630
2    0.472826
3    0.242363
Name: Survived, dtype: float64
####################################
Embarked
C    0.553571
Q    0.389610
S    0.336957
Name: Survived, dtype: float64&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여성 생존률 약 74%, 남성 생존률 약19%로 여성 생존률이 훨씬 높았고, 1등급 생존률 62%, 3등급 24%로 등급이 높을 수록 생존률이 올라가는 것을 알 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 전처리&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요한 데이터들에 대한 결측치를 채우고, 모델에게 학습시킬 수 있도록, 숫자 형태로 데이터를 변환시켜 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1746434960887&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;train['Age'].fillna(train['Age'].mean(), inplace=True)
train['Embarked'].fillna(train['Embarked'].mode()[0], inplace=True)
train['Sex'] = train['Sex'].map({'male' : 0, 'female' : 1})
train['Embarked'] = train['Embarked'].map({'S' : 0, 'C' : 1, 'Q' : 2})&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;Age는 결측치를 모든 값들의 평균으로 채워줬다.&lt;/li&gt;
&lt;li&gt;Embarked는 가장 mode() 함수를 사용하여, 가장 자주 등장하는 값으로 채워주었다. 여기서, mode()함수는 가장 자주 등장하는 값들이 리스트 형태로 반환되기 때문에, [0]과 같이 하나의 인덱스 값을 지정해주어야 한다.&lt;/li&gt;
&lt;li&gt;성별 컬럼에서 남성은 0, 여성은 1로 변환해주었다.&lt;/li&gt;
&lt;li&gt;Embarked 역시 문자열 형태로 되어있기 때문에 S, C, Q를 각각 0, 1, 2 로 변환해주었다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;모델 학습&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모델에게 학습시킬 데이터만 따로 features에 담아서 준비하고, 입력 데이터와 맞춰야 할 정답을 나눠준다.&lt;/p&gt;
&lt;pre id=&quot;code_1746436538068&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

x = train[features]
y = train['Survived']&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x에는 우리가 입력할 데이터. 즉, features를 넣어주고, y에는 우리가 맞춰야 할 생존율인 Survived를 담아준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모델을 임포트 해주고, 학습을 시킬 차례이다.&lt;/p&gt;
&lt;pre id=&quot;code_1746436849880&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier(random_state=42)
model.fit(x,y)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; RandomForestClassifier&lt;/b&gt; 는 여러 개의 Decision Tree(의사결정 트리)를 만들어서 결과를 투표로 결정하는 모델이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 개의 트리보다 더 안정적이고, overfitting에 강하다. 특히, 분류 문제에서 매우 잘 작동한다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;random_state=42&lt;/b&gt; 는 왜 작성해줘야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; RandomForestClassifier&lt;/b&gt;와 같은 랜덤 포레스트는 여러 개의 트리를 무작위로 만들기 때문에, 매번 실행할 때마다 결과가 조금씩 달라질 수 가 있다. 때문에 random_state에 고정된 숫자를 입력하여, 랜덤 시드를 고정하는 역할을 하게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대한 정보를 찾아보다가 알게 되었는데, 숫자는 아무 숫자나 입력해도 되지만, 42라는 숫자가 관습처럼 많이 쓰여진다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;모델 테스트&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 모델 학습까지 완료 되었으니, test.csv 데이터를 이용해, 테스트를 거친 후 나온 결과를 예시 파일과 같이 만든 후 제출만 하면 끝이다.&lt;/p&gt;
&lt;pre id=&quot;code_1746437663602&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;test = pd.read_csv('/kaggle/input/titanic/test.csv')

test['Age'] = test['Age'].fillna(train['Age'].median())
test['Fare'] = test['Fare'].fillna(train['Fare'].median())
test['Embarked'] = test['Embarked'].fillna(train['Embarked'].mode()[0])

test['Sex'] = test['Sex'].map({'male': 0, 'female': 1})
test['Embarked'] = test['Embarked'].map({'S': 0, 'C': 1, 'Q': 2})

X_test = test[features]
predictions = model.predict(X_test)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 가공했던 것과 동일하게 test파일의 데이터들을 가공해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;model.predict()를 통해 예측을 진행한다. 그 결과는 predictions에 담아주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 제출용 데이터프레임을 만들어 주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1746438205657&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;output = pd.DataFrame({
    'PassengerId': test['PassengerId'],
    'Survived': predictions
})

output.to_csv('/kaggle/working/submission.csv', index=False)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해주면, 우측에 output에서 내가 만든 결과 파일을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 파일을 다운로드 한 후, 제출해주면 score가 나온다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;화면 캡처 2025-05-05 185259.png&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dspmqZ/btsNMBX7I2p/696rzF9Y0S5NlHrxOWmKr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dspmqZ/btsNMBX7I2p/696rzF9Y0S5NlHrxOWmKr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dspmqZ/btsNMBX7I2p/696rzF9Y0S5NlHrxOWmKr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdspmqZ%2FbtsNMBX7I2p%2F696rzF9Y0S5NlHrxOWmKr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1045&quot; height=&quot;535&quot; data-filename=&quot;화면 캡처 2025-05-05 185259.png&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;535&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0부터 1사이의 점수를 받을 수 있는데, 생각보다 점수가 낮게 나와서 조금 실망했다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 오늘의 결과를 발판 삼아서, 예측률을 더 높일 수 있도록 방법들을 하나씩 배워가면 될 것 같다.&lt;/p&gt;</description>
      <category>Kaggle</category>
      <category>Kaggle</category>
      <category>MachineLearning</category>
      <category>pandas</category>
      <category>데이터전처리</category>
      <category>머신러닝</category>
      <category>캐글</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/72</guid>
      <comments>https://cases.tistory.com/entry/Kaggle-Kaggle-Competitions-%ED%83%80%EC%9D%B4%ED%83%80%EB%8B%89-%EC%83%9D%EC%A1%B4%EC%9E%90-%EC%98%88%EC%B8%A1#entry72comment</comments>
      <pubDate>Mon, 5 May 2025 18:59:20 +0900</pubDate>
    </item>
    <item>
      <title>Pandas 실습 - NASA 데이터 활용</title>
      <link>https://cases.tistory.com/entry/Pandas-%EC%8B%A4%EC%8A%B5-NASA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%99%9C%EC%9A%A9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Pandas로 실제 데이터들을 가공해보면서 익혀보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NASA에서 제공하는 아폴로 임무별 데이터들을 토대로 만들어 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 부터 달탐사는 현재까지 진행 중이고, 앞으로 진행 예정인 작전에 도움을 줄 수 있도록 수집할 암석들의 종류와 갯수를 구해보는 작업을 진행하였다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;NASA에서 제공하는 데이터 가져오기&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://curator.jsc.nasa.gov/lunar/samplecatalog/index.cfm&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://curator.jsc.nasa.gov/lunar/samplecatalog/index.cfm&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1744795584592&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Apollo Samples and Photo catalog&quot; data-og-description=&quot;&quot; data-og-host=&quot;curator.jsc.nasa.gov&quot; data-og-source-url=&quot;https://curator.jsc.nasa.gov/lunar/samplecatalog/index.cfm&quot; data-og-url=&quot;https://curator.jsc.nasa.gov/lunar/samplecatalog/index.cfm&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://curator.jsc.nasa.gov/lunar/samplecatalog/index.cfm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://curator.jsc.nasa.gov/lunar/samplecatalog/index.cfm&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Apollo Samples and Photo catalog&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;curator.jsc.nasa.gov&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 사이트에서 필요한 컬럼을 체크하여, 데이터들을 csv 파일로 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 프레임 생성 및 정보 확인&lt;/b&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1744795851697&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 데이터 파일을 읽어오기
rock_samples = pd.read_csv('./data/rocksamples.csv')&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 읽어올 경로는 자신의 csv파일의 실제 경로를 작성해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744796192376&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#열수
rock_samples.shape[1]
#모양
rock_samples.shape
#컬럼명
rock_samples.columns&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수들로 일단 데이터의 모양, 컬럼명 등을 확인해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744796385361&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#결측치 갯수 확인
rock_samples.isnull().sum()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 6.40.02.png&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/banOWZ/btsNomU3GNg/GK6bvjYxCxAwJczxWBkI10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/banOWZ/btsNomU3GNg/GK6bvjYxCxAwJczxWBkI10/img.png&quot; data-alt=&quot;출력결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/banOWZ/btsNomU3GNg/GK6bvjYxCxAwJczxWBkI10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbanOWZ%2FbtsNomU3GNg%2FGK6bvjYxCxAwJczxWBkI10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;326&quot; height=&quot;246&quot; data-filename=&quot;스크린샷 2025-04-16 오후 6.40.02.png&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출력결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결측치 개수 확인 결과, Subtype이라는 컬럼에 3개의 결측치가 있다는 것을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결측치가 있는 행만 출력하고 싶을때는, 아래 처럼 작성해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1744796650606&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rock_samples[rock_samples['Type'].isnull().any(axis=1)]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;데이터 가공&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터에 대한 웬만한 정보들을 알아냈으니, 이제 본격적으로 데이터들을 가공해볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 컬럼명 변경 &amp;amp; 컬럼값 변환&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744796974359&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 컬럼값변환 : rock_samples['Weight (g)'] -&amp;gt; rock_samples['Weight (kg)]
rock_samples['Weight (g)'] = rock_samples['Weight (g)'].apply(lambda x : x * 0.001)
# 컬럼명 변환 : 'Weight(g)' -&amp;gt; 'Weight(kg)
rock_samples.rename(columns={'Weight (g)' : 'Weight(kg)'}, inplace=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;g으로 표기되어 있는 단위를 kg으로 바꾸어 주었고, 그에 맞게 컬럼의 값들도 모두 변경해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- missions 데이터 프레임 구성하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rock_samples에서 'Mission'별로 중복되지 않게 값들을 출력해보면 아래와 같은 결과가 나올 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1744798122911&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#rock_samples['Mission']의 중복되지 않은 고유값들을 알아본다.
rock_samples['Mission'].unique()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.09.24.png&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;70&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qqrzp/btsNoa8tVQP/K88EvXBRJJ64Y5TpPQ59qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qqrzp/btsNoa8tVQP/K88EvXBRJJ64Y5TpPQ59qk/img.png&quot; data-alt=&quot;아폴로 미션별 출력이 된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qqrzp/btsNoa8tVQP/K88EvXBRJJ64Y5TpPQ59qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQqrzp%2FbtsNoa8tVQP%2FK88EvXBRJJ64Y5TpPQ59qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;972&quot; height=&quot;70&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.09.24.png&quot; data-origin-width=&quot;972&quot; data-origin-height=&quot;70&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;아폴로 미션별 출력이 된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1744798057988&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 빈 데이터 프레임을 만들고 변수 missions에 할당한다.
missions = pd.DataFrame()
# mission 데이터프레임의 새로운 컬럼인 mission['Mission']에 할당한다.
missions['Mission'] = rock_samples['Mission'].unique()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고유값들을 그대로 새로 만든 missions 데이터 프레임에 할당해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.12.58.png&quot; data-origin-width=&quot;278&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qjhas/btsNnY0SEyP/lI25oG86C2BYv47M3zW2LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qjhas/btsNnY0SEyP/lI25oG86C2BYv47M3zW2LK/img.png&quot; data-alt=&quot;missions를 출력한 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qjhas/btsNnY0SEyP/lI25oG86C2BYv47M3zW2LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQjhas%2FbtsNnY0SEyP%2FlI25oG86C2BYv47M3zW2LK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;278&quot; height=&quot;476&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.12.58.png&quot; data-origin-width=&quot;278&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;missions를 출력한 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 rock_samples에서 미션별 sample의 중량의 총합을 구해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미션별로 묶기 위해서 groupby() 함수를 사용해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744798601856&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;total_weight = rock_samples.groupby('Mission')['Weight(kg)'].sum()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 미션별 샘플들의 총합(total_weight)을 위에서 만들었던 missions와 병합해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1744798698179&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;missions = pd.merge(missions,total_weight, on='Mission')
missions&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.18.51.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wfBw8/btsNnkLiAam/kMJPahkcQUodFHjn7So9h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wfBw8/btsNnkLiAam/kMJPahkcQUodFHjn7So9h1/img.png&quot; data-alt=&quot;merge total_weight가 된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wfBw8/btsNnkLiAam/kMJPahkcQUodFHjn7So9h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwfBw8%2FbtsNnkLiAam%2FkMJPahkcQUodFHjn7So9h1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;386&quot; height=&quot;472&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.18.51.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;merge total_weight가 된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 작업으로는, 각 미션들이 진행됨에 따라, 샘플 수집 중량 증가수를 알기 위해서 각 미션별 중량들의 차이를 구해서 병합해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Weight diff의 첫 번째 행은 비교대상이 없기 때문에,&amp;nbsp; Nan이 나올것이다. 그래서 그 값을 0으로 채워주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744799093361&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 아폴로 임무간의 수집해온 암석 샘플 총중량 차이를 나타내는 컬럼 'Weight diff'를 missions 데이터프레임에 추가
missions['Weight diff'] = missions['Sample weight (kg)'].diff()
missions.fillna(value=0, inplace=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.25.14.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;470&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zI9er/btsNl9IO2bt/dGGiRkzM4Ox4bY4ChDpaEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zI9er/btsNl9IO2bt/dGGiRkzM4Ox4bY4ChDpaEK/img.png&quot; data-alt=&quot;+weight diff&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zI9er/btsNl9IO2bt/dGGiRkzM4Ox4bY4ChDpaEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzI9er%2FbtsNl9IO2bt%2FdGGiRkzM4Ox4bY4ChDpaEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;470&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.25.14.png&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;470&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;+weight diff&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;달 탐사선은 달모듈 + 명령모듈로 이루어져 있는데, 이 둘을 합쳐서 승무원 모듈이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일단 NASA 사이트에서 모듈명, 중량 데이터를 가져와서 병합해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744799667801&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;달모듈이름 = ['Eagle (LM-5)', 'Intrepid (LM-6)', 'Antares (LM-8)', 'Falcon (LM-10)', 'Orion (LM-11)', 'Challenger (LM-12)']
달모듈중량 = [15103, 15235, 15264, 16430, 16445, 16456]
missions['Lunar module (LM)'] = 달모듈이름
missions['LM mass (kg)'] = 달모듈중량&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.35.48.png&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mF0Uv/btsNmHk1mf0/F8cmVo8BGqSzM1wkpxCODk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mF0Uv/btsNmHk1mf0/F8cmVo8BGqSzM1wkpxCODk/img.png&quot; data-alt=&quot;모듈명, 중량까지 반영된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mF0Uv/btsNmHk1mf0/F8cmVo8BGqSzM1wkpxCODk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmF0Uv%2FbtsNmHk1mf0%2FF8cmVo8BGqSzM1wkpxCODk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1074&quot; height=&quot;322&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.35.48.png&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모듈명, 중량까지 반영된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 동일한 방법으로 명령모듈 컬럼까지 만들어서 병합해준다. + 각 모듈의 미션별 중량 증가량까지&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.44.23.png&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6Ht7C/btsNobM85kZ/kKVKRKLzrAux0nQEEYckI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6Ht7C/btsNobM85kZ/kKVKRKLzrAux0nQEEYckI0/img.png&quot; data-alt=&quot;Lm diff, CM diff 까지 병합 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6Ht7C/btsNobM85kZ/kKVKRKLzrAux0nQEEYckI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6Ht7C%2FbtsNobM85kZ%2FkKVKRKLzrAux0nQEEYckI0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1990&quot; height=&quot;452&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.44.23.png&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Lm diff, CM diff 까지 병합 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말했듯이 승무원 영역은 LM 과 CM의 합친 값이기 때문에, 두 컬럼의 합으로 승무원 영역의 중량(총 중량)을 병합한다.&lt;/p&gt;
&lt;pre id=&quot;code_1744800453545&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 승무원영역 = 달모듈 + 명령모듈
# 달모듈과 명령모듈 중량을 합한 값을 'Total weight (kg)'라는 새로운 컬럼을 만들어 missions 데이터프레임에 추가
missions['Total weight (kg)'] = missions['LM mass (kg)'] + missions['CM Mass (kg)']
# 마찬가지로 증가량(diff)까지 병합
missions['Total weight diff'] = missions['Total weight (kg)'].diff()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 승무원 영역이 전체 탐사선(Payload)에서 차지하는 비율을 구해야 하는데, NASA에서 찾은 탐사선의 무게는 43,500kg이라고 나와있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Total weight / 43,500 을 하면 차지하는 비율을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1744800767544&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;saturnVPayload = 43500
missions['Crewed area : Payload'] = missions['Total weight (kg)'] / saturnVPayload&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 방법으로 승무원 영역 / 전체 탐사선에서 암석 샘플이 차지하는 비율을 각각 구해서 병합해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744800889137&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 승무원 영역에서 암석 샘플이 차지하는 비율 구하기 -&amp;gt; 'Sample : Crewed area'
missions['Sample : Crewed area'] = missions['Sample weight (kg)'] / missions['Total weight (kg)']
# 페이로드에서 샘플이 차지하는 비율 구하기 -&amp;gt; 'Sample : Payload'
missions['Sample : Payload'] = missions['Sample weight (kg)'] / saturnVPayload&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.55.06.png&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHs4mj/btsNouFtKdV/9PNJngQkHYCB1ueimE2Bh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHs4mj/btsNouFtKdV/9PNJngQkHYCB1ueimE2Bh0/img.png&quot; data-alt=&quot;비율까지 모두 병합 완료&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHs4mj/btsNouFtKdV/9PNJngQkHYCB1ueimE2Bh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHs4mj%2FbtsNouFtKdV%2F9PNJngQkHYCB1ueimE2Bh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2304&quot; height=&quot;680&quot; data-filename=&quot;스크린샷 2025-04-16 오후 7.55.06.png&quot; data-origin-width=&quot;2304&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;비율까지 모두 병합 완료&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;artemis_mission DataFrame 만들기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아르테미스 임무란, 2024년부터 진행 중인 Moon to Mars 프로그램의 첫 번째 단계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Artemis 임무에서 우주 비행사는 달에서 추가로 암석 샘플을 가져올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 지난 임무긴 하지만, 우리는 아르테미스 임무에서 가져와야 할 암석 샘플을 데이터 분석을 통해 예측해볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1744801540749&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;artemis_crewedArea = 26520

artemis_dict = {'Mission' : ['artemis1', 'artemis1b', 'artemis2'],
                'Total weight (kg)' : [artemis_crewedArea, artemis_crewedArea, artemis_crewedArea],
                'Payload (kg)' : [26988, 37965, 42955]
               }
# 딕셔너리를 데이터 프레임으로 저장하기
artemis_mission = pd.DataFrame(artemis_dict)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아르테미스의 승무원 영역은 고정으로 26,520이고, 나머지 데이터들은 딕셔너리로 데이터 프레임을 만들어 주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 8.07.12.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkgSh4/btsNonzM0co/KjISTdCniewkbHS3NN0zuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkgSh4/btsNonzM0co/KjISTdCniewkbHS3NN0zuK/img.png&quot; data-alt=&quot;artemis_mission&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkgSh4/btsNonzM0co/KjISTdCniewkbHS3NN0zuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkgSh4%2FbtsNonzM0co%2FKjISTdCniewkbHS3NN0zuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;192&quot; data-filename=&quot;스크린샷 2025-04-16 오후 8.07.12.png&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;artemis_mission&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는, 밑의 단계들을 거쳐서 출발할 탐사선들의 무게에서 샘플들의 무게가 차지하는 비율을 예상을 해 볼 것이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;nbsp;missions에서 구했던 'Crewd area : Payload', 'Sample : Crewed area', 'Sample : payload' 의 평균값을 구한다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;missions 에서 Sample weight 의 총합을 구한다.&lt;/li&gt;
&lt;li&gt;이 두개의 값으로 예측치를 계산해, 병합해준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1744802447950&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# missions 데이터프레임에서 'Crewed area : Payload', 'Sample : Crewed area', 'Sample : payload' 의 평균값을 구한다.
a = missions['Crewed area : Payload'].mean()
b = missions['Sample : Crewed area'].mean()
c = missions['Sample : Payload'].mean()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1744802504468&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# missions 데이터프레임에서 'Sample weight (kg)'의 총합을 구한다.
sample_sum = missions['Sample weight (kg)'].sum()&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1744802552129&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#artemis_missions 데이터 프레임에 'Sample weight from total (kg)' 컬럼을 추가한다.
artemis_mission['Sample weight from total (kg)'] = artemis_mission['Total weight (kg)'] * b
#artemis_mission 데이터프레임에 'Sample weight from payload (kg)' 컬럼을 추가한다.
artemis_mission['Sample weight from payload (kg)'] = artemis_mission['Payload (kg)'] * c
# artemis_mission 데이터프레임에 'Estimated sample weight (kg)' 컬럼을 추가한다.
artemis_mission['Estimated sample weight (kg)'] = (artemis_mission['Sample weight from payload (kg)']
                                                    + artemis_mission['Sample weight from total (kg)']) / 2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 8.23.13.png&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PiGAD/btsNobTWqKy/79RuLEfBKXDknqFoQtZHE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PiGAD/btsNobTWqKy/79RuLEfBKXDknqFoQtZHE0/img.png&quot; data-alt=&quot;예측치까지 병합된 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PiGAD/btsNobTWqKy/79RuLEfBKXDknqFoQtZHE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPiGAD%2FbtsNobTWqKy%2F79RuLEfBKXDknqFoQtZHE0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1842&quot; height=&quot;192&quot; data-filename=&quot;스크린샷 2025-04-16 오후 8.23.13.png&quot; data-origin-width=&quot;1842&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예측치까지 병합된 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 어떤 종류의 암석을 가져와야 할지 생각을 해보면, 현재 보유 중인 암석 중에서 거의 남지 않은 암석들을 위주로 가져오는게 좋을 것 같다는 결론이 날 수 있다. (물론 나의 개인적인 생각이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rock_samples 에서 Weight와 Pristine(사용한 비율)을 이용해 남은 암석의 무게를 구하여 컬럼으로 추가해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1744803322667&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rock_samples['Remaining (kg)'] = rock_samples['Weight(kg)'] * rock_samples['Pristine (%)'] * 0.01&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중량이 0.16kg 이상이고, pristinedl 50 이하인 행만 추출하여, low_samples 라는 데이터프레임을 만든다.&lt;/p&gt;
&lt;pre id=&quot;code_1744803577303&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;low_samples = rock_samples[
    (rock_samples['Weight (kg)'] &amp;gt;= 0.16) &amp;amp; 
    (rock_samples['Pristine (%)'] &amp;lt;= 50)
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 연구에 많이 사용되는 현무암과 각력암만을 추출하여, needed_samples 데이터 프레임을 만들어준다.&lt;/p&gt;
&lt;pre id=&quot;code_1744803726204&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;needed_samples = low_samples[
    (low_samples['Type'] == 'Basalt') | (low_samples['Type'] == 'Breccia')
]
needed_samples&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 8.43.32.png&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zKbP9/btsNolWmvAu/hIe8YRl24Gm9nCxvwxIBm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zKbP9/btsNolWmvAu/hIe8YRl24Gm9nCxvwxIBm0/img.png&quot; data-alt=&quot;needed_sample&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zKbP9/btsNolWmvAu/hIe8YRl24Gm9nCxvwxIBm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzKbP9%2FbtsNolWmvAu%2FhIe8YRl24Gm9nCxvwxIBm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1180&quot; height=&quot;584&quot; data-filename=&quot;스크린샷 2025-04-16 오후 8.43.32.png&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;needed_sample&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입별로 암석 샘플의 총중량을 구해서, 아폴로 임무에서 처음부터 수집이 되지 않은 암석을 찾는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.28.39.png&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRd6Vz/btsNnvMMk7r/LpcBKWAIZ0eNoB3CDm1lY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRd6Vz/btsNnvMMk7r/LpcBKWAIZ0eNoB3CDm1lY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRd6Vz/btsNnvMMk7r/LpcBKWAIZ0eNoB3CDm1lY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRd6Vz%2FbtsNnvMMk7r%2FLpcBKWAIZ0eNoB3CDm1lY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1180&quot; height=&quot;552&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.28.39.png&quot; data-origin-width=&quot;1180&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Crustal 수집량이 현저히 떨어져 있다는 것을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1744806665289&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# rock_samples에서 'Type'이 'Crustal'. 인 행만 추출한다.
crustal = rock_samples[rock_samples['Type'] == 'Crustal']
needed_samples = pd.concat([needed_samples, crustal])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Crustal을 추출하여 need_samples에 추가해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 Crusatal의 경우에는 데이터 프레임의 옆이 아니라, 위아래 방향으로 붙여주는 것이기 때문에, pd.concat() 함수를 사용하여 붙여주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.31.50.png&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DR3WO/btsNovxGsss/rV7vsTu0DMs0jDZ2QMXiJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DR3WO/btsNovxGsss/rV7vsTu0DMs0jDZ2QMXiJk/img.png&quot; data-alt=&quot;Crustal 추가 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DR3WO/btsNovxGsss/rV7vsTu0DMs0jDZ2QMXiJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDR3WO%2FbtsNovxGsss%2FrV7vsTu0DMs0jDZ2QMXiJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1154&quot; height=&quot;268&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.31.50.png&quot; data-origin-width=&quot;1154&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Crustal 추가 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완성된 needed_samples의 타입은 모두 'Basalt', 'Breccia', 'Crustal' 이 세 개인 것을 알 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.37.17.png&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buRZo7/btsNn1c0plq/uj6hgkanZyoQS36YVBuO3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buRZo7/btsNn1c0plq/uj6hgkanZyoQS36YVBuO3K/img.png&quot; data-alt=&quot;unique()로 고유값 출력&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buRZo7/btsNn1c0plq/uj6hgkanZyoQS36YVBuO3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuRZo7%2FbtsNn1c0plq%2Fuj6hgkanZyoQS36YVBuO3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;812&quot; height=&quot;184&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.37.17.png&quot; data-origin-width=&quot;812&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;unique()로 고유값 출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;need samples overview 만들기&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 최종적으로 우주인에게 보여줄 overview를 제작할 차례이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, needed_samples에서 암석 타입별로 중량의 합을 구하여 needed_samples_overview 데이터 프레임과 병합해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1744807344546&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 아르테미스 임무에 전달할 최종 데이터프레임 needed_samples_overview 만든다.
needed_samples_overview = pd.DataFrame
needed_sample_weight = needed_samples.groupby('Type')['Weight(kg)'].sum().reset_index()
# 두 데이터프레임을 병합한다.
needed_samples_overview = needed_sample_weight&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;needed_samples 에서 암석 타입별 중량의 평균을 구하여 병합한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼명도 변경 해주었다.&lt;/p&gt;
&lt;pre id=&quot;code_1744807501786&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# needed_samples 데이터프레임에서 암석유형별 중량의 평균을 구한다.
Weight_mean = needed_samples.groupby('Type')['Weight(kg)'].mean()
needed_samples_overview = pd.merge(needed_samples_overview, Weight_mean, on='Type')
# 컬럼명 변경. 'ID' -&amp;gt; 'Number of samples'
needed_samples_overview.rename(columns={'count' : 'Number of samples'}, inplace=True)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.48.28.png&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kwNpo/btsNobsYfKG/IPviTfUBJa7ffKf7Vp5sV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kwNpo/btsNobsYfKG/IPviTfUBJa7ffKf7Vp5sV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kwNpo/btsNobsYfKG/IPviTfUBJa7ffKf7Vp5sV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkwNpo%2FbtsNobsYfKG%2FIPviTfUBJa7ffKf7Vp5sV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;962&quot; height=&quot;190&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.48.28.png&quot; data-origin-width=&quot;962&quot; data-origin-height=&quot;190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제, 각 암석별 차지하는 비율을 계산하기 위해, Number of samples의 총합을 구하여 total_rocks에 할당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 비율을 구하여 새로운 컬럼으로 할당해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1744807854944&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# needed_samples_overview['Number of samples'] 의 총합을 구하고 total_rocks에 할당
total_rocks = needed_samples_overview['Number of samples'].sum()
# 각 암석유형별로 차지하는 비율을 구해서 'Percentage of rocks'라는 컬럼에 할당한다.
needed_samples_overview['Percentage of rocks'] = (needed_samples_overview['Number of samples'] / total_rocks)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.51.59.png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tj0z0/btsNkotq4da/hWRsDM9u2xKspYsNnXlw2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tj0z0/btsNkotq4da/hWRsDM9u2xKspYsNnXlw2K/img.png&quot; data-alt=&quot;결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tj0z0/btsNkotq4da/hWRsDM9u2xKspYsNnXlw2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftj0z0%2FbtsNkotq4da%2FhWRsDM9u2xKspYsNnXlw2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1228&quot; height=&quot;182&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.51.59.png&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수집해야 할 무게 계산을 위해, 잊고 있었던 artemis_mission을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Estimated sample weight 의 평균을 구하여 이번 달탐사의 예상 암석 중량 예측치를 계산하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1744808074110&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# artemis_mission['Estimated sample weight (kg)']의 평균을 구해서 Artemis 달탐사의 에상 암석 중량을 구한다.
sample_weight_mean = artemis_mission['Estimated sample weight (kg)'].mean()
# needed_samples_overview['Weight to collect'] 컬럼을 만든다.
needed_samples_overview['Weight to collect'] = needed_samples_overview['Percentage of rocks']
* sample_weight_mean&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 수집할 각각의 암석 개수를 구하여 마무리&lt;/p&gt;
&lt;pre id=&quot;code_1744808171021&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 수집할 각각의 암석 개수를 알려준다.
needed_samples_overview['Rocks to collect'] = (needed_samples_overview['Weight to collect']
/ needed_samples_overview['Average weight (kg)'])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.56.45.png&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brrv1T/btsNnAtNRf7/hu2sL9FbtfWLIWtH7QAl0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brrv1T/btsNnAtNRf7/hu2sL9FbtfWLIWtH7QAl0K/img.png&quot; data-alt=&quot;최종결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brrv1T/btsNnAtNRf7/hu2sL9FbtfWLIWtH7QAl0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrrv1T%2FbtsNnAtNRf7%2Fhu2sL9FbtfWLIWtH7QAl0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1680&quot; height=&quot;184&quot; data-filename=&quot;스크린샷 2025-04-16 오후 9.56.45.png&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종적으로 Basalt : 13개, Breccia : 36개, Crusatal : 21개 가량을 수집해오면 된다는 결과를 도출해냈다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Python/Pandas</category>
      <category>AI</category>
      <category>NASA</category>
      <category>numpy</category>
      <category>pandas</category>
      <category>Python</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/71</guid>
      <comments>https://cases.tistory.com/entry/Pandas-%EC%8B%A4%EC%8A%B5-NASA-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%99%9C%EC%9A%A9#entry71comment</comments>
      <pubDate>Wed, 16 Apr 2025 22:02:28 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 옹알이(1) - Python</title>
      <link>https://cases.tistory.com/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%98%B9%EC%95%8C%EC%9D%B41-Python</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 공부를 하며, 프로그래머스 문제로 연습을 하던 도중...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 흥미로운 녀석을 만나서 포스트를 작성하게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문제설명 &amp;gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;머쓱이는 태어난 지 6개월 된 조카를 돌보고 있습니다. 조카는 아직 &quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot; 네 가지 발음을 최대 한 번씩 사용해 조합한(이어 붙인) 발음밖에 하지 못합니다. 문자열 배열&amp;nbsp;&lt;/span&gt;babbling&lt;span style=&quot;text-align: left;&quot;&gt;이 매개변수로 주어질 때, 머쓱이의 조카가 발음할 수 있는 단어의 개수를 return하도록 solution 함수를 완성해주세요.&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;제한사항 &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #263747; text-align: left;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: inherit; color: #000000;&quot;&gt;1 &amp;le;&lt;span&gt;&amp;nbsp;&lt;/span&gt;babbling의 길이 &amp;le; 100&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit; color: #000000;&quot;&gt;1 &amp;le;&lt;span&gt;&amp;nbsp;&lt;/span&gt;babbling[i]의 길이 &amp;le; 15&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit; color: #000000;&quot;&gt;babbling의 각 문자열에서 &quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot;는 각각 최대 한 번씩만 등장합니다.
&lt;ul style=&quot;list-style-type: disc; color: #000000;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: inherit; color: #000000;&quot;&gt;즉, 각 문자열의 가능한 모든 부분 문자열 중에서 &quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot;가 한 번씩만 등장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: inherit; color: #000000;&quot;&gt;문자열은 알파벳 소문자로만 이루어져 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나의 풀이 &amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1744293370161&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def solution(babbling):
    joka = [&quot;aya&quot;, &quot;ye&quot;, &quot;woo&quot;, &quot;ma&quot;]
    answer = 0
    for i in babbling :
        for j in joka :
            i = i.replace(j, &quot; &quot;)
        if i.strip() == &quot;&quot;:
            answer += 1
    return answer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 한 줄씩 살펴보면, 먼저 조카가 발음 가능한 단어들을 &quot;joka&quot;라는 리스트에 담아준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. babbling을 for문으로 돌리면서 joka도 함께 for문으로 비교를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. babbling의 i에 대응하는 joka의 j가 같은게 있으면 그것을 빈 문자열 &quot; &quot; 로 replace() 함수를 이용해 치환해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 모든 for문이 돌고 나면, i와 j가 대응되었던 곳은 모두 &quot; &quot;로 치환이 되었을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 여기서, 양 끝이 빈 문자열인 것을 없앴을때 아무것도 남지 않닸다면, 그 단어는 완벽히 발음 가능한 단어 이므로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 경우에만 answer 카운터 수 +1 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 Python이 익숙치 않아서, strip() 같은 함수도 몰랐지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 문제를 풀면서 하나씩 얻어가는게 있는 것 같다.&lt;/p&gt;</description>
      <category>Python</category>
      <category>programmers</category>
      <category>Python</category>
      <category>옹알이(1)</category>
      <category>프로그래머스</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/70</guid>
      <comments>https://cases.tistory.com/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%98%B9%EC%95%8C%EC%9D%B41-Python#entry70comment</comments>
      <pubDate>Thu, 10 Apr 2025 23:02:42 +0900</pubDate>
    </item>
    <item>
      <title>The 2025 AI Index Report | Stanford HAI - 요약</title>
      <link>https://cases.tistory.com/entry/The-2025-AI-Index-Report-Stanford-HAI-%EC%9A%94%EC%95%BD</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-08 오후 4.57.26.png&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUReIF/btsNclJW1y7/Ue3rzt5K7jqiDJOGIyDLDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUReIF/btsNclJW1y7/Ue3rzt5K7jqiDJOGIyDLDK/img.png&quot; data-alt=&quot;[The 2025 AI Index Report]&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUReIF/btsNclJW1y7/Ue3rzt5K7jqiDJOGIyDLDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUReIF%2FbtsNclJW1y7%2FUe3rzt5K7jqiDJOGIyDLDK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;673&quot; height=&quot;870&quot; data-filename=&quot;스크린샷 2025-04-08 오후 4.57.26.png&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[The 2025 AI Index Report]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;color: #0e0e0e;&quot; data-ke-style=&quot;style1&quot;&gt;지금 AI는 어디까지 왔을까?&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 4월, 스탠퍼드 인간중심 AI 연구소(HAI)는 매년 발간하는 &lt;span&gt;&lt;b&gt;AI Index Report&lt;/b&gt;&lt;/span&gt;의 여덟 번째 에디션을 발표했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI의 기술 발전, 산업 활용, 정책 대응, 교육 변화 등 &lt;span&gt;&lt;b&gt;AI가 사회 전반에 미치는 영향&lt;/b&gt;&lt;/span&gt;을 폭넓게 분석한 세계 최고 권위의 보고서인데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 보고서에서는 특히 &lt;b&gt;&amp;ldquo;AI가 상상에서 현실로 완전히 진입했다&amp;rdquo;&lt;/b&gt;는 점이 인상 깊게 드러납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI는 얼마나 똑똑해졌을까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&amp;nbsp;&lt;/span&gt;&lt;b&gt;초고난이도 벤치마크 성능 대폭 향상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull; SWE-bench(코딩 문제 해결) 기준, 1년 만에 **4.4% &amp;rarr; 71.7%**로 급상승&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;소형 모델도 초거대 모델급 성능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;Microsoft의 **Phi-3-mini (3.8B 파라미터)**가 GPT-3.5 수준 달성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&amp;nbsp;&lt;/span&gt;&lt;b&gt;AI 비디오 생성 기술 급성장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;OpenAI SORA, Meta Movie Gen 등 고퀄리티 텍스트 &amp;rarr; 비디오 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWE 벤치마크 성능의 향상폭이 가장 눈에 띄는 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  AI는 이제 기업의 핵심 자산&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;AI 도입 기업 비율&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;2023년 55% &amp;rarr; 2024년 78%&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;생성형 AI 투자금&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;b&gt;$33.9B&lt;/b&gt;&lt;/span&gt;, 전체 AI 투자 중 &lt;span&gt;&lt;b&gt;20% 이상 차지&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;AI가 생산성 향상과 인력 격차 해소에 기여&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;⚡ &lt;/span&gt;&lt;b&gt;AI 수요로 원자력 등 대체 에너지에 대한 관심 증가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  미국과 중국의 AI 패권 경쟁&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  미국:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;AI 모델 수 40개&lt;/b&gt;&lt;span&gt;로 세계 1위&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  중국:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;논문 수 1위&lt;/b&gt;&lt;span&gt;, &lt;/span&gt;&lt;b&gt;특허 수 1위&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;모델 성능 격차도 크게 좁힘 (HumanEval 기준 2023년 31.6% &amp;rarr; 2024년 3.7%)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 미국의 압도적 1위라는 말은 점점 사라져 갈 듯 하네요.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중국은 특히, 모든걸 오픈소스로 공개한다는 점이 매력적일 수 밖에 없는 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;⚖️ 책임 있는 AI(RAI)는 아직 갈 길이 멀다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;AI 관련 사고 급증&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;2024년 233건 (전년 대비 56% 증가)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;암묵적 편향 여전&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;GPT-4, Claude 3 등 최신 모델도 인종/성별 편향 지속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;OECD, EU, UN 등 국제 거버넌스 등장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  과학과 의학도 AI가 주도&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt; &amp;zwj;⚕️ &lt;/span&gt;&lt;b&gt;의사보다 정확한 진단&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;GPT-4가 임상 진단에서 인간보다 뛰어난 결과 도출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;span&gt;&lt;b&gt;AI 의료기기&lt;/b&gt;&lt;/span&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;2015년 6개 &amp;rarr; 2023년 223개로 폭증&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;바이오 대형 모델 등장&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;AlphaFold 3, Med-Gemini, VisionFM 등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; ️ 정책과 규제도 본격화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;⚖️ &lt;/span&gt;&lt;b&gt;미국 AI 관련 연방 규제 수&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;2023년 25건 &amp;rarr; 2024년 59건&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt; ️ &lt;/span&gt;&lt;b&gt;AI 안전 연구소 전 세계 확산&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;美&amp;middot;英 &amp;rarr; 韓, EU, 日 등으로 확대&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;사우디, 프랑스, 캐나다 등 대규모 AI 인프라 투자&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  교육과 여론, 그리고 대중의 인식 변화&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt; &amp;zwj;  &lt;span&gt;&lt;b&gt;초중등 CS 교육 국가&lt;/b&gt;&lt;/span&gt;: 전 세계 2/3 이상 도입 or 계획 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;미국 AI 석사 졸업자 수 급증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;AI 낙관론 증가&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;2022년 52% &amp;rarr; 2024년 55%&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;  &lt;/span&gt;&lt;b&gt;하지만 여전히 존재하는 불신&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&amp;bull;&lt;span&gt; &lt;/span&gt;&amp;ldquo;AI는 공정하지 않다&amp;rdquo;는 응답 증가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  결론: AI는 &amp;lsquo;미래 기술&amp;rsquo;이 아닌 &amp;lsquo;현재의 기술&amp;rsquo;이다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 더 이상 연구소나 실험실의 전유물이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 기업, 병원, 학교, 심지어 정치와 사회 문제 해결에까지 &lt;span&gt;&lt;b&gt;실질적인 영향을 주는 기술&lt;/b&gt;&lt;/span&gt;로 자리잡았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 AI 인덱스 리포트는 우리에게 다음과 같은 질문을 던집니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #0e0e0e;&quot; data-ke-style=&quot;style1&quot;&gt;&amp;ldquo;이 급변하는 기술의 흐름 속에서, 우리는 얼마나 잘 준비되어 있는가?&amp;rdquo;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘 수업에서 2024 Report를 리뷰했었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교수님이 바로 어제 2025 Report가 발표되었다고 하셔서, 찾아보게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인공지능을 공부하는 분들이라면, Report 원문을 살펴보시는게 현재 AI 시장의 동향을 파악하는 것에 도움이 될 것 같습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;  &lt;/span&gt;&lt;b&gt;전체 리포트 원문 보기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;a href=&quot;https://hai.stanford.edu/ai-index/2025-ai-index-report&quot;&gt;Stanford HAI AI Index Report 2025 바로가기&lt;/a&gt;&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>HAI</category>
      <category>Stanford</category>
      <category>the 2025 ai index report</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/69</guid>
      <comments>https://cases.tistory.com/entry/The-2025-AI-Index-Report-Stanford-HAI-%EC%9A%94%EC%95%BD#entry69comment</comments>
      <pubDate>Tue, 8 Apr 2025 16:56:23 +0900</pubDate>
    </item>
    <item>
      <title>[Microsoft AI School 7기] 비전공자 최종 합격 후기</title>
      <link>https://cases.tistory.com/entry/Microsoft-AI-School-7%EA%B8%B0-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EC%B5%9C%EC%A2%85-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;몇 달 전, 혼자 진행했던 사이드 프로젝트를 진행을 하며, AI 기술 분야에 관심이 생겨 관련 교육기관들을 찾아보던 중, Microsoft에서 진행하는 MS AI School을 알게되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 요즘 AI가 엄청 핫한 이슈이기 때문에 이와 관련된 교육을 해주는 아카데미, 부트캠프는 꽤나 많았지만, 커리큘럼을 하나씩 뜯어서 살펴본 결과, MS AI School이 가장 마음에 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 마음에 들었던 부분은 Azure를 꽤나 많이 지원 해주어서, Azure OpenAI 서비스를 공부해볼 수 있다는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 온라인으로 진행되긴 하지만, Teams를 활용하여 오프라인 못지 않은 수업 진행방식이라면 괜찮을 것 같다는 생각이었음.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원서 제출 후 며칠 뒤, 면접 관련 메일을 받게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-05 오후 3.09.17.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;293&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PoZqf/btsM9Xh77xT/P9vCaMzEvZlXFaLwiIUlG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PoZqf/btsM9Xh77xT/P9vCaMzEvZlXFaLwiIUlG1/img.png&quot; data-alt=&quot;서류 합격 메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PoZqf/btsM9Xh77xT/P9vCaMzEvZlXFaLwiIUlG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPoZqf%2FbtsM9Xh77xT%2FP9vCaMzEvZlXFaLwiIUlG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;813&quot; height=&quot;293&quot; data-filename=&quot;스크린샷 2025-04-05 오후 3.09.17.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;293&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서류 합격 메일&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼리버드 서류 전형으로 지원하여, 면접보기 까지는 어려움 없이 진행되었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접 전, 대기실에서 코딩테스트를 진행 예정이라는 문구를 보고, Java로 문제들을 좀 풀면서 준비했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 막상 면접 당일, 코딩테스트를 치루러 들어갔을때는, 코딩테스트라기 보다는 기본적인 컴퓨터 관련 질문들? 같은 내용이라 별로 어려움 없이 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접은 3인 1조로 화상 면접이 진행되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 관련 경험, 지원동기 등의 질문을 하셨고, 가장 기억에 남는 질문은 &quot;팀 프로젝트 진행 시, 같은 팀에 '빌런'이 있으면 어떻게 할 것인가?&quot;라는 질문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 생각은 &quot;그 사람이 할 의지가 있는 사람이라면, 열심히 이끌어주며 하겠다. 하지만 의지도 없는 사람이라면 배제하고 진행을 할 것 같다.&quot; 였고, 그대로 답변을 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 면접관님도 &quot;그런 사람이 있으면 자기 할 일만 잘 하고, 자기가 빼먹을 수 있는 부분을 최대한 빼먹으면서 하면 된다. 라고 말씀 해주셨다. (무슨 말인지 매우 공감... 끄덕)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 면접을 끝마치고 3일 뒤.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2025-04-05 오후 3.22.03.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;141&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EHPe2/btsNam9scVA/hbtQ39e3OTKi1nRH5rMHD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EHPe2/btsNam9scVA/hbtQ39e3OTKi1nRH5rMHD0/img.png&quot; data-alt=&quot;최종합격&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EHPe2/btsNam9scVA/hbtQ39e3OTKi1nRH5rMHD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEHPe2%2FbtsNam9scVA%2FhbtQ39e3OTKi1nRH5rMHD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;443&quot; height=&quot;141&quot; data-filename=&quot;스크린샷 2025-04-05 오후 3.22.03.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;141&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;최종합격&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 합격 메일 받음...&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딥러닝, 머신러닝의 기본 언어가 파이썬인 만큼, 수업 시작 전까지 파이썬 공부를 꾸준히 하면서 수업을 들어갈 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 엔지니어가 되려면 대학원을 나오는게 기본이라는 말들이 많지만, 해보지 않으면 모르는 일 아니겠어요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피나는 노력으로 불가능 한 일은 없다고 생각한다 ㅎㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6개월이란 시간이 길다면 길고 짧다면 짧겠지만, 최대한 열심히 해서 목표를 향해 달려보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(수업을 들으면서 파이썬, 확률과통계, 선형대수학 등의 공부도 병행해야 할 것 같습니다.)&lt;/p&gt;</description>
      <category>MS AI School</category>
      <category>Microsoft</category>
      <category>msaischool</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/68</guid>
      <comments>https://cases.tistory.com/entry/Microsoft-AI-School-7%EA%B8%B0-%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-%EC%B5%9C%EC%A2%85-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0#entry68comment</comments>
      <pubDate>Sat, 5 Apr 2025 15:32:08 +0900</pubDate>
    </item>
    <item>
      <title>[Project] Google Gemini API 활용</title>
      <link>https://cases.tistory.com/entry/Project-Google-Gemini-API-%ED%99%9C%EC%9A%A9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트 컨셉이 여행 관련 사이트를 만드는 것인데, 요즘 또 대 AI 시대인 만큼 나도 AI 챗봇 등에 관심이 많아서 프로젝트에 AI를 활용한 시스템을 한 번 만들고 싶어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러가지 챗봇 API 중 현재 무료로 사용 가능한 Google Gemini를 내 프로젝트에 적용시켜 보기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chat GPT도 일정 크레딧 까지는 무료로 사용이 가능하다고 나와있었는데, 내 계정엔 크레딧이 없어서 다른걸 찾던 중 Gemini를 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Gemini API를 어떻게 불러와서 사용하는지, 내 프로젝트에 어떻게 적용시켰는지를 포스팅 해보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저, Gemini API를 사용하기 위해서는 API 키를 발급받아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ai.google.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ai.google.dev/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1732072234463&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Gemini Developer API | Gemma open models &amp;nbsp;|&amp;nbsp; Google AI for Developers&quot; data-og-description=&quot;Build with Gemini 1.5 Flash and 1.5 Pro using the Gemini API and Google AI Studio, or access our Gemma open models.&quot; data-og-host=&quot;ai.google.dev&quot; data-og-source-url=&quot;https://ai.google.dev/&quot; data-og-url=&quot;https://ai.google.dev/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bB594f/hyXDlYpcBf/k5SeGmzwHydCFIMcAqoPz0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://ai.google.dev/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ai.google.dev/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bB594f/hyXDlYpcBf/k5SeGmzwHydCFIMcAqoPz0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Gemini Developer API | Gemma open models &amp;nbsp;|&amp;nbsp; Google AI for Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Build with Gemini 1.5 Flash and 1.5 Pro using the Gemini API and Google AI Studio, or access our Gemma open models.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ai.google.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 링크로 들어가서 회원가입을 한 후 API 키를 발급 받고 따로 키를 메모를 해둬야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키를 다 발급 받았으면 본격적으로 API를 불러와서 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CDN 방식과 NPM 환경에서 하는 방법이 있는데, 나는 CDN 방식을 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 라이브러리 호출&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1732072606781&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script type=&quot;importmap&quot;&amp;gt;
{
    &quot;imports&quot;: {
        &quot;@google/generative-ai&quot;: &quot;https://esm.run/@google/generative-ai&quot;
    }
}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 라이브러리를 CDN 방식으로 불러온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 객체 생성&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1732072756887&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { GoogleGenerativeAI } from &quot;@google/generative-ai&quot;;

// API 키 설정
const API_KEY = 'API KEY';

// GoogleGenerativeAI 객체 생성
const genAI = new GoogleGenerativeAI(API_KEY);
const model = genAI.getGenerativeModel({ model: &quot;gemini-1.5-flash&quot; });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 API 키를 사용하여 GoogleGenerativeAI 객체를 생성해주고, 어떤 모델을 사용할지 적어줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flash 모델, pro 모델이 있지만 무료로 사용하기에 가장 적합한 flash 모델을 사용해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 프롬프트 생성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 Gemini에게 요청을 보낼 프롬프트를 생성해야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1732073077473&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;category&quot;&amp;gt;여행 카테고리:&amp;lt;/label&amp;gt; 
    &amp;lt;select id=&quot;category&quot;&amp;gt;
        &amp;lt;option value=&quot;자연&quot;&amp;gt;자연&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;휴양&quot;&amp;gt;휴양&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;액티비티&quot;&amp;gt;액티비티&amp;lt;/option&amp;gt;
        &amp;lt;option value=&quot;도시&quot;&amp;gt;도시&amp;lt;/option&amp;gt;
    &amp;lt;/select&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;country&quot;&amp;gt;여행 국가:&amp;lt;/label&amp;gt; 
    &amp;lt;input type=&quot;text&quot; id=&quot;country&quot; placeholder=&quot;여행할 국가를 입력하세요&quot;&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;days&quot;&amp;gt;여행 일수:&amp;lt;/label&amp;gt; 
    &amp;lt;input type=&quot;number&quot; id=&quot;days&quot; placeholder=&quot;여행 일수를 입력하세요&quot;&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;button id=&quot;generate-button&quot;&amp;gt;추천 경로 생성&amp;lt;/button&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 위와 같이 HTML 에서 사용자가 원하는 여행 국가, 카테고리, 여행 일 수를 선택할 수 있게 하여 그걸 토대로 프롬프트를 생성하도록 구현하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1732073185132&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; document.getElementById('generate-button').addEventListener('click', async () =&amp;gt; {
    const category = document.getElementById('category').value.trim();
    const country = document.getElementById('country').value.trim();
    const days = document.getElementById('days').value.trim();
    
 const prompt = `${country}에서 ${category} 컨셉의 ${days}일 여행 도시를 추천해주고, ...`;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 요청 문장과 함께 조합하여 프롬프트를 생성해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. Gemini API 호출 및 응답 처리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위에서 생성한 프롬프트를 API 호출하여 날아오는 응답을 처리해주면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1732073368318&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    // API 호출
    const result = await model.generateContent(prompt);
    console.log('API Result:', result);

    // 응답 데이터 처리
    if (result &amp;amp;&amp;amp; result.response &amp;amp;&amp;amp; result.response.candidates.length &amp;gt; 0) {
        const content = result.response.candidates[0].content.parts[0].text;
        document.getElementById('response').innerText = content;
    } else {
        document.getElementById('response').innerText = &quot;추천 결과가 없습니다.&quot;;
    }
} catch (error) {
    console.error('API 호출 중 오류 발생:', error);
    document.getElementById('response').innerText = &quot;오류가 발생했습니다.&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출 후 응답 처리를 해야 하는데, 생각보다 해당 응답이 복잡한 경로 안에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 적힌 코드와 같이 &lt;b&gt;result &amp;gt; candidates[0] &amp;gt; content &amp;gt; parts[0] &amp;gt; text&amp;nbsp;&lt;/b&gt;이 경로에 응답이 들어있다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 있는 응답을 innerText로 화면에 잘 출력해주기만 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 이렇게 복잡할 줄도 모르고 그냥 다른 사람들이 올려놓은 Gemini API 호출 방법만 보고 간단해 보여서 사용해보았는데, 생각보다 막히는 부분도 많고 복잡했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 아직 JavaScript에 대한 지식이 많이 부족해서 더 힘들었던 것도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 다 구현하고 응답을 처리해주기만 하면 되는데, 화면에 계속 응답이 출력이 되질 않아서 애를 많이 먹었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 도구로 하나하나 뒤져 가면서 찾아 내어 겨우 처리할 수 있었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힘들었던 기능이지만 막상 구현해놓고 보니 뿌듯하기도 하고, 더 많은 것을 배운 느낌이라 오히려 기분은 더 좋다.&lt;/p&gt;</description>
      <category>Project</category>
      <category>API</category>
      <category>GEMINI</category>
      <category>prompt</category>
      <category>springboot</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/67</guid>
      <comments>https://cases.tistory.com/entry/Project-Google-Gemini-API-%ED%99%9C%EC%9A%A9#entry67comment</comments>
      <pubDate>Wed, 20 Nov 2024 12:40:00 +0900</pubDate>
    </item>
    <item>
      <title>[Project] Spring Security를 활용한 회원기능</title>
      <link>https://cases.tistory.com/entry/Project-Spring-Security%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%9A%8C%EC%9B%90%EA%B8%B0%EB%8A%A5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트를 진행하면서 처음으로 Spring에서 지원하는 Spring Security를 활용하여 회원기능을 구현해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 아직 Spring boot를 배우면서 프로젝트를 진행하는 중이라 기능들을 구현하는 데에 시간이 좀 오래걸렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 모르는 부분들은 Chat gpt를 활용하면서 하나씩 해결해 나갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그럼 Spring Security가 무엇인지, 그리고 이를 어떤식으로 프로젝트에 적용 시켰는지 기록 해볼것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Security란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 왜 Spring Security를 사용하여 회원 기능을 구현해야 하는지에 대해서 말하자면, 웹사이트에서 로그인, 로그아웃 등의 기능을 구현하면 이에 대한 권한 부여 / 관리 등이 필요하다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 Spring에서 쉽고 효율적으로 구현할 수 있게 개발된 것이 Spring Security 이다. Spring Security를 사용하여 개발하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 / 인가 등의 &lt;b&gt;보안적인 기능을 개발하는 데에 있어서 아주 효율적이고 신속하게 개발&lt;/b&gt;이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어, 로그인을 한 사용자에게만 사이트 접속 권한을 부여한다던지 이런 기능들을 효율적으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Security 사용 방법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 Spring Security는 어떻게 사용하는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;Spring Boot gradle을 사용한 나의 프로젝트 기준&amp;gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, build.gradle에 아래와 같이 의존성을 추가해주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1731516931850&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	testImplementation 'org.springframework.security:spring-security-test'
    	&amp;lt;-- 기타 의존성 생략 --&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 config 클래스를 생성하여 Spring Security 기능을 구현해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;b&gt;src &amp;gt; main &amp;gt; java &amp;gt; com.xx.MyProject&lt;/b&gt; 경로 안에 &lt;b&gt;config 패키지를 생성&lt;/b&gt; 해주고, config 패키지 안에 &lt;b&gt;SecurityConfig 클래스를 생성&lt;/b&gt;해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 나의 &lt;b&gt;SecurityConfig&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;클래스의 구성에 대해서 하나하나 자세히 알아보자.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;전체 코드&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1731517533978&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.yubin.SpringBootTest.config;

import com.yubin.SpringBootTest.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
        auth.authenticationProvider(authenticationProvider());
        return auth.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(auth -&amp;gt; auth
                        .requestMatchers(&quot;/register&quot;, &quot;/login&quot;,&quot;/error&quot;).permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -&amp;gt; form
                        .loginPage(&quot;/login&quot;)
                        .loginProcessingUrl(&quot;/login&quot;)  // 로그인 처리 URL 설정
                        .defaultSuccessUrl(&quot;/home&quot;, true)  // 로그인 성공 후 이동할 URL 지정
                        .failureUrl(&quot;/login?error=true&quot;)  // 로그인 실패 시 이동할 경로
                        .permitAll()
                )
                .logout(LogoutConfigurer::permitAll)
                .csrf(AbstractHttpConfigurer::disable); // CSRF 비활성화;
        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt; PasswordEncoder&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731517602497&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자의 패스워드를 암호화 하기 위한 메서드이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; BCryptPasswordEncoder는 SpringSecurity에서 지원하는 클래스&lt;/b&gt;로, 비밀번호를 &lt;b&gt;bcrypt 해시 함수로 암호화하고 검증&lt;/b&gt;하는 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 생성 시 매번 무작위로 salt를 추가하여 같은 비밀번호도 항상 다른 해시 값을 가지게 만들어 보안에 아주 강력하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt; DaoAuthenticationProvider &lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731517887853&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(passwordEncoder());
    return authProvider;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DaoAuthenticationProvider는 &lt;b&gt;UserDetailsService를 통해 데이터베이스에서 사용자 정보를 로드&lt;/b&gt;하고, &lt;b&gt;PasswordEncoder를 사용하여 비밀번호를 검증&lt;/b&gt;한다. 그리고 CustomUserDetailsService를 이용해 사용자 정보를 조회하고, 사용자 인증 시 암호를 인코딩해 매칭한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;b&gt;UserDetailsService&lt;/b&gt;가 무엇이냐?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService는 UserDetails 객체를 반환하며, Spring Security가 인증 절차에서 사용자 세부 정보를 관리할 수 있도록 표준화된 인터페이스이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 CustomUserDetailsService 클래스를 따로 생성해주는 이유는?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDetailsService가 세부 정보를 관리할 수 있게 해주었다면, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;CustomUserDetailsService&lt;span&gt;에서&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;사용자 정보 등을 데이터 베이스와 연동하여 조회, 또는 이 정보를 기반으로 인증을 수행&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;lt;CustomUserDetailsService&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package com.yubin.SpringBootTest.service;

import com.yubin.SpringBootTest.model.User;
import com.yubin.SpringBootTest.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -&amp;gt; new UsernameNotFoundException(&quot;User not found&quot;));

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.isEnabled(),
                true, 
                true, 
                true, 
                List.of(new SimpleGrantedAuthority(&quot;ROLE_USER&quot;)) // 기본 권한 추가
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;AuthenticationManager&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
    AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
    auth.authenticationProvider(authenticationProvider());
    return auth.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AuthenticationManager&lt;/b&gt; 는 로그인 시도 시 사용자의 신원을 검증하는 역할을 하며, &lt;b&gt;성공적으로 인증된 Authentication 객체를 반환하거나 인증 실패 시 예외를 던진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흐름을 한번 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자가 로그인 페이지를 통해 사용자명과 비밀번호를 입력하고 제출하면, Spring Security는 로그인 요청을 받아 AuthenticationManager.authenticate()를 호출.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. AuthenticationManager는 여러 AuthenticationProvider 목록을 가지고 있으며, 요청된 Authentication 객체를 각 AuthenticationProvider에 전달하여 인증을 시도.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(현재 나의 코드에서는 위에서 확인 했듯이&amp;nbsp; &lt;b&gt;DaoAuthenticationProvide&lt;/b&gt;임)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 인증에 성공하면 세션에 인증 정보를 저장하고, 사용자를 인증된 상태로 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 인증에 실패하면 AuthenticationProvider는 예외를 던져 인증 실패를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt; SecurityFilterChain&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1731519633788&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -&amp;gt; auth
            .requestMatchers(&quot;/register&quot;, &quot;/login&quot;, &quot;/error&quot;).permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(form -&amp;gt; form
            .loginPage(&quot;/login&quot;)
            .loginProcessingUrl(&quot;/login&quot;)
            .defaultSuccessUrl(&quot;/home&quot;, true)
            .failureUrl(&quot;/login?error=true&quot;)
            .permitAll()
        )
        .logout(LogoutConfigurer::permitAll)
        .csrf(AbstractHttpConfigurer::disable);
    return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SecurityFilterChain&lt;/b&gt;은 DaoAuthenticationProvider를 사용하여 사용자 정보를 로드하고, SecurityFilterChain으로 HTTP 요청을 보호하면서 지정된 페이지에 대한 접근 권한을 설정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;requestMatchers &lt;/b&gt;메서드를 통해 &quot;/register&quot;, &quot;/login&quot;, &quot;/error&quot; 경로를 모든 사용자에게 허용 (permitAll) 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;formLogin&lt;/b&gt;을 통해 로그인 페이지와 로그인 처리 URL, 성공 or 실패 시 이동할 경로를 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;loginPage(&quot;/login&quot;) &lt;/b&gt;: 사용자 정의 로그인 페이지 경로를 설정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;loginProcessingUrl(&quot;/login&quot;)&lt;/b&gt; : 로그인 요청을 처리할 URL.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;defaultSuccessUrl(&quot;/home&quot;, true) &lt;/b&gt;: 로그인 성공 시 이동할 페이지를 지정한다. true는 로그인 페이지를 강제로 /home으로 지정하여 마지막 페이지가 아닌 특정 페이지로 이동하게 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;failureUrl(&quot;/login?error=true&quot;) &lt;/b&gt;: 로그인 실패 시 이동할 URL.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그아웃 (logout) &lt;/b&gt;:&lt;b&gt;&amp;nbsp;&lt;/b&gt;permitAll로 설정하여 모든 사용자에게 로그아웃을 허용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 내가 이 &lt;b&gt;SecurityFilterChain&amp;nbsp;&lt;/b&gt;부분을 공부하고 적용하면서 엄청 애를 많이 먹었는데, 분명히 reauestMatchers에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;/register&quot;, &quot;/login&quot; 등의 경로를 지정해 주었다. 그런데도 계속 프로젝트를 실행하면 403에러가 뜨는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 끙끙대며 머리를 싸매며 해결 방법을 찾다가 &lt;b&gt;csrf를 disable&lt;/b&gt; 해주어야 한다는 글을 보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 Spring Security를 사용하면 csrf가 활성화 되어 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 csrf란 &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Cross Site Request forgery, 사이트 간 위조 요청으로 일종의 보안 위협 요소이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 글들을 찾아봤는데 대부분의 가이드 글들이 csrf를 disable 하고 사용하라고 나와 있어서 그렇게 해보니 403 에러가 사라지고 요청이 잘 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 대부분 csrf를 사용하지 않는지&lt;/b&gt; 궁금하여 한 번 찾아봤는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Rest API를 이용한 서버라면, session 기반 인증과는 다르게 stateless하기 때문에 서버에 인증 정보를 보관하지 않는다. 일반적으로 jwt 같은 토큰을 사용하여 인증하기 때문에 해당 토큰을 Cookie에 저장하지 않는다면 csrf 취약점에 대해서는 어느 정도 안전하다고 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모르는 부분을 하나씩 새로 배워가면서 프로젝트를 진행하다 보니 진행 속도가 많이 더디지만, 그래도 알차게 제대로 배워가는 것 같아서 뿌듯하긴 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아 그리고 csrf.disable()은 곧 지원이 중단된다고 하길래 그럼 어떻게 해줘야 할지 gpt에게 물었더니 위 코드처럼&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;.csrf(AbstractHttpConfigurer::disable)&amp;nbsp;&lt;/b&gt;라고 작성하면 된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project</category>
      <category>login</category>
      <category>Spring</category>
      <category>springsecurity</category>
      <category>보안</category>
      <category>인증</category>
      <category>회원</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/66</guid>
      <comments>https://cases.tistory.com/entry/Project-Spring-Security%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%ED%9A%8C%EC%9B%90%EA%B8%B0%EB%8A%A5#entry66comment</comments>
      <pubDate>Thu, 14 Nov 2024 03:15:07 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Thymeleaf</title>
      <link>https://cases.tistory.com/entry/Spring-Thymeleaf</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번 포스트에서 미리 예고 했듯이, Thymeleaf에 대해서 알아 볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 Thymeleaf가 무엇일까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Thymeleaf란?&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Thymeleaf(타임리프)는 템플릿 엔진의 일종으로 흔히 View Template(뷰 템플릿) 이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 사용했던 JSP 같은 템플릿 엔진의 한 종류라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;-2-jsp와-thymeleaf-차이점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JSP와 Thymeleaf 차이점&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이때까지 사용했던 JSP와 어떤 차이점이 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 기존에 사용했던 &lt;b&gt;JSP는 Servlet으로 변환되어 실행&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Servlet은 Java 소스이기 때문에 HTML 코드에 Java 코드를 합하여 동적으로 웹 페이지를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(때문에 JSP 파일 내부에 자바 코드를 작성하는 것도 가능하지만 웬만하면 그렇게 하지 않는 것을 추천한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, &lt;b&gt;Thymeleaf는 Servlet으로 변환되지 않고, 순수 HTML파일을 최대한 유지&lt;/b&gt;하기 때문에 비즈니스 로직이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 분리된다. 그래서 파일을 웹 브라우저에서 바로 실행 시켜도 그 내용을 확인하는게 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 둘의 가장 큰 차이는 Thymeleaf는 jar 파일로 export가 가능하다는 것이다.&lt;br /&gt;Spring으로 빌드하면 기본적으로 jar파일로 빌드가 되는데 JSP는 jar패키징이 불가능하고 war패키징만 가능하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;jar로 패키징이 가능한 Thymeleaf를 사용하는 것이 더 좋고 편리&lt;/b&gt;하다.&lt;br /&gt;(war로 패키징을 하려면 was가 필요하고 사전에 정의된 구조만을 사용해야 해 복잡하다. 그래서 jar로 패키징 하는 것이 더 편리함)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;-2-jsp와-thymeleaf-차이점&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JSP와 Thymeleaf 의 동작 차이&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Thymeleaf는 HTML파일을 파싱하고 분석한 후 정해진 위치에 데이터를 치환해 웹 페이지를 생성한다.&lt;br /&gt;반면, JSP는 Servlet으로 변환되어 웹 애플리케이션 서버에서 동작하며 필요한 기능을 수행하고 생성된 데이터를 웹 페이지와 함께 클라이언트로 응답한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Thymeleaf만 쓰면 되지 JSP를 왜 쓰는 것이냐? 장점만 있는게 아니냐? 라고 할 수 도 있지만,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSP보다 Thymeleaf가 응답 속도가 느리다는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 웬만하면 Thymeleaf를 사용하는 것이 배포하기도 편리하고, 여러 방면으로 장점도 많아서 앞으로 주 엔진으로 사용할 것 같다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>JSP</category>
      <category>Spring</category>
      <category>Thymeleaf</category>
      <category>차이점</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/65</guid>
      <comments>https://cases.tistory.com/entry/Spring-Thymeleaf#entry65comment</comments>
      <pubDate>Mon, 4 Nov 2024 20:11:08 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Boot 시작하기</title>
      <link>https://cases.tistory.com/entry/Spring-Spring-Boot-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;STS에서 Spring Framework로만 프로젝트를 하다가, 이번에 아예 Spring Boot를 시작해보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 기존의 Spring보다 의존성 관리, 초기 설정 등에 대해서 훨씬 간단하고 편리한 서비스를 제공한다고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 대부분 IntelliJ를 많이 사용한다고 하여, 나도 이번 기회에 넘어가보기로 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.jetbrains.com/ko-kr/idea/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.jetbrains.com/ko-kr/idea/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1729621255514&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;IntelliJ IDEA &amp;ndash; Java 및 Kotlin을 위한 최고의 IDE&quot; data-og-description=&quot;&quot; data-og-host=&quot;www.jetbrains.com&quot; data-og-source-url=&quot;https://www.jetbrains.com/ko-kr/idea/&quot; data-og-url=&quot;https://www.jetbrains.com/ko-kr/idea/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.jetbrains.com/ko-kr/idea/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.jetbrains.com/ko-kr/idea/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;IntelliJ IDEA &amp;ndash; Java 및 Kotlin을 위한 최고의 IDE&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.jetbrains.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 링크에서 다운을 받으면 되는데, 인텔리제이는 유료 버전이 있기 때문에 무료로 사용하려면 &lt;b&gt;&quot;community&quot; 버전&lt;/b&gt;을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 해주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 &lt;a href=&quot;https://start.spring.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://start.spring.io/&lt;/a&gt; 링크에서 &lt;b&gt;프로젝트 기본 정보 입력 및 설정 후 &amp;gt; 필요한 의존성 부여 &amp;gt; 압축을 풀어&lt;/b&gt; 인텔리제이에서 해당 파일을 열어서 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;springio.PNG&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yCGMf/btsKfzqiXgv/o3MRuR6nOW0DfE7tR5Ajr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yCGMf/btsKfzqiXgv/o3MRuR6nOW0DfE7tR5Ajr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yCGMf/btsKfzqiXgv/o3MRuR6nOW0DfE7tR5Ajr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyCGMf%2FbtsKfzqiXgv%2Fo3MRuR6nOW0DfE7tR5Ajr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1515&quot; height=&quot;742&quot; data-filename=&quot;springio.PNG&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때까지 Java11버전을 사용하다가, Spring Boot는 더 이상 11버전을 지원하지 않는다고 하여 17버전을 새로 설치해 주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 부트가&amp;nbsp; 진짜 편하다고 느낀게 이렇게 프로젝트를 생성해주면 더 이상 설정을 만져줄게 없다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(새로 의존성을 부여해야 하는게 아닌 이상)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어 서버도 내장 되어 있어서, 톰캣을 다운 하고 Server를 설정 해야하는 번거로운 일도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Spring과 약간 다르게 여기서는 &lt;b&gt;Thymeleaf&lt;/b&gt;를 사용하고 있어서, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;src &amp;gt; main &amp;gt;&lt;/b&gt; &lt;b&gt;resources &amp;gt; templates&amp;nbsp;&lt;/b&gt;경로에 jsp 파일이 아닌 html 파일을 생성하여 view를 구성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Thymeleaf&lt;/b&gt;에 대해서는 다음 포스팅에 자세히 알아볼 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 controller, service 등의 패키지는 &lt;b&gt;src &amp;gt; main &amp;gt; java &amp;gt; com.OO.프로젝트이름&lt;/b&gt; 경로 하단에 생성하여 작성해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 테스트 해볼 수 있게 view와 controller를 구성했다면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;application.PNG&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xoVGL/btsKfr6YZhJ/EFq30Mt08Qhkb0ofRhFXw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xoVGL/btsKfr6YZhJ/EFq30Mt08Qhkb0ofRhFXw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xoVGL/btsKfr6YZhJ/EFq30Mt08Qhkb0ofRhFXw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxoVGL%2FbtsKfr6YZhJ%2FEFq30Mt08Qhkb0ofRhFXw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;973&quot; height=&quot;245&quot; data-filename=&quot;application.PNG&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 생긴 (프로젝트 이름)Application.java 파일을 실행해주고, localhost:8080을 들어가면 확인을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 나는 실행을 해보니 콘솔에 에러가 떴었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; Execution failed for task ':com.yubin.SpringBootTest.SpringBootTestApplication.main()'. &amp;gt; Process 'command 'C:\Program Files\Java\jdk-17\bin\java.exe'' finished with non-zero exit value 1 * Try: &amp;gt; Run with --stacktrace option to get the stack trace. &amp;gt; Run with --info or --debug option to get more log output. &amp;gt; Run with --scan to get full insights. &amp;gt; Get more help at &lt;a style=&quot;color: #ee2323;&quot; href=&quot;https://help.gradle.org.&quot;&gt;https://help.gradle.org.&lt;/a&gt; BUILD FAILED in 1s 3 actionable tasks: 1 executed, 2 up-to-date &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 오류가 뜨길래 ChatGPT의 도움을 받아봤더니 인텔리제이에서 jdk의 버전 설정이 제대로 안되었을 때의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 코드라고 하여, 해결 방법을 찾아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;해결방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;File &amp;gt; Project Structure &amp;gt; Project&amp;nbsp;&lt;/b&gt;부분에서 프로젝트 SDK, 모듈 SDK를 모두 %JAVA_HOME%의 경로에 있는 jdk-17버전으로 맞춰주어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, &lt;b&gt;File &amp;gt; Settings &amp;gt; Build, Execution, Deployment &amp;gt; Build Tools &amp;gt; Gradle&lt;/b&gt;에서 &lt;b&gt;Gradle JVM&lt;/b&gt;이 JDK 17로 설정되어 있는지 확인까지 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 위와 같은 방법을 사용하니 프로젝트가 잘 작동하는 것을 확인할 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Spring은 새로 프로젝트를 만들어 시작하려고 하면 설정도 다 처음부터 해야하고, dependency도 하나 하나 추가 해줘야 하는게 너무 번거로웠는데, 스프링 부트를 처음 사용해보니 정말 신세계였다. (이걸 왜 이제야 맛 본 걸까...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지 빼고는 Spring과 비슷해서 금방 적응할 것 같긴한데, 인텔리제이에도 아직 익숙하지 않아서 조금의 적응 시간이 필요한 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로 사이드 프로젝트를 만들어보고 싶은게 생겨서, 그 프로젝트는 스프링 부트로 한 번 진행해볼 생각이다.&lt;/p&gt;</description>
      <category>Spring</category>
      <category>IntelliJ</category>
      <category>Spring</category>
      <category>springboot</category>
      <category>설정</category>
      <category>시작하기</category>
      <category>인텔리제이</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/64</guid>
      <comments>https://cases.tistory.com/entry/Spring-Spring-Boot-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0#entry64comment</comments>
      <pubDate>Wed, 23 Oct 2024 03:53:06 +0900</pubDate>
    </item>
    <item>
      <title>[Java] StringBuffer</title>
      <link>https://cases.tistory.com/entry/Java-StringBuffer</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 StringBuffer 클래스가 무엇인지 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;StringBuffer&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;String클래스는 인스턴스를 생성할 때 지정된 문자열을 변경할 수 없기 때문에 문자열 결합시 계속해서 새로운 인스턴스를 생성한다.&lt;br /&gt;하지만 StringBuffer클래스는 변경이 가능하다.&lt;br /&gt;내부적으로 문자열 편집을 위한 버퍼(buffer)를 가지고 있으며,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;StringBuffer인스턴스를 생성할 때 그 크기를 지정할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;크기를 지정할 때 나중에 편집을 고려하여 버퍼의 길이를 충분히 잡아주는 것이 좋다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;버퍼의 길이를 넘어서게 되면 버퍼의 길이를 늘려주는 작업을 추가적으로 해야 하기 때문이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;StringBuffer의 생성자&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;StringBuffer클래스의 인스턴스를 생성하면 동시에 char형 배열이 생성된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 배열은 이후 문자열을 저장하고 편집하기 위한 공간(buffer)로 사용된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 StringBuffer클래스의 생성자는 위 char형 배열의 길이를 매개변수로 받게 되어있다.&lt;br /&gt;만약 매개변수로 길이를 지정해주지 않을시 16개의 문자를 저장할 수 있는 크기의 버퍼를 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728721146213&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public StringBuffer(int length) {
	value = new char[length];
    shared = flase;
    // 인스턴스 생성시 int값을 넣어주면 해당 길이의 배열생성
}

public StringBuffer() {
	this(16);
    // 인스턴스 생성시 아무런 값을 넣지 않으면 길이를 16으로 지정
}

public StringBuffer(String str) {
	this(str.length() + 16);
    append(str);
    // 인스턴스 생성시 문자열을 넣으면 해당 문자보다 16 더 크게 버퍼 생성
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;-stringbuffer를-이용하여-문자열-변경하기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StringBuffer를 이용하여 문자열 변경하기&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728721165265&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StringBuffer sb = new StringBuffer(&quot;abc&quot;);
// char[]에 앞에서부터 a,b,c를 저장

sb.append(&quot;123&quot;);
// 배열에서 a,b,c 뒤에 1,2,3 추가하여 저장
// append()는 반환타입이 StringBuffer인데 자신의 주소를 반환한다.

StringBuffer sb2 = sb.append(&quot;가나&quot;);
// sb내용 뒤에 가,나를 덧붙인다.

System.out.println(sb);		// &quot;abc123가나&quot; 출력
System.out.println(sb2);	// &quot;abc123가나&quot; 출력&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;sb와 sb2 모두 같은 StringBuffer인스턴스를 가리키고 있으므로 같은 내용이 출력된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;-stringbuffer-문자열-비교&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StringBuffer 문자열 비교&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;StringBuffer클래스는 String클래스와 다르게 equals메서드가 오버라이딩 되어있지 않아 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;equals메서드를 사용하여도 등가비교연산자(==)로 비교한 것과 같은 결과를 얻는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728721210086&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String sb1 = new StringBuffer(&quot;abc&quot;);
String sb2 = new StringBuffer(&quot;abc&quot;);

System.out.println(sb==sb2);		// false
System.out.println(sb.equals(sb2);	// false&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;반면에 toString( )은 오버라이딩이 되어 있기 때문에 StringBuffer인스턴스를 String으로 바꿀 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이렇게 String으로 바꾸어준 후 equals메서드를 사용하면 비교 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728721229734&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;String s1 = sb1.toString()
String s2 = sb2.toString()

System.out.println(s.equals(s2));	// true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그냥 쉽게 생각해서 변경(편집)이 가능한 String 클래스 = StringBuffer 클래스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 생각하면 될 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 09.28. 20:18';
&lt;/script&gt;</description>
      <category>Java</category>
      <category>string</category>
      <category>StringBuffer</category>
      <category>toString</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/63</guid>
      <comments>https://cases.tistory.com/entry/Java-StringBuffer#entry63comment</comments>
      <pubDate>Sat, 12 Oct 2024 17:22:25 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] DispatcherServlet</title>
      <link>https://cases.tistory.com/entry/Spring-DispatcherServlet</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 DispatcherServlet을 도입하여 모든 과정을 중앙에서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 요청된 URL과 매핑된 각각의 Servlet class를 찾아갔다면, Spring MVC는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청을 DispatcherServlet에서 받는다는 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 DispatcherServlet은 이 요청을 분석하여 그 요청에 맞는 컨트롤러를 불러온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[이전(모델1)에는 servlet과 controller가 분리되지 않았지만, Spring MVC(모델2)는 분리되었다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이 DispatcherServlet은 어떻게 구현해야 되는지 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 직접적으로 구현할 필요는 없고, DispatcherServlet 라이브러리를 설치해주기만 하면 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Spring Web MVC 라이브러리&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mvnrepository.com/&quot;&gt;https://mvnrepository.com/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 의존성을 추가 해줄때 사용하는 Maven Repository 사이트에서 Spring 이라고 서치를 하면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Web MVC가 보일 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최선 버전의 코드를 복사하여 pom.xml에 의존성을 추가해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘 설치가 되었는지 확인이 하고 싶다면 추가한 프로젝트에서&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JavaResources &amp;gt; Libraries &amp;gt; Maven Dependencies 에서 확인을 해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 설치하지도 않은 라이브러리가 있는 이유는 내가 설치한 라이브러리가 필요로 하는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리들을 Maven이 자동으로 설치를 해주기 때문에 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;web.xml에 매핑하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet 클래스가 모든 요청을 받아서 처리를 해야하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 내용을 web.xml에 적어주어야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728716182452&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;servlet&amp;gt;
	  	&amp;lt;servlet-name&amp;gt;dispatcher&amp;lt;/servlet-name&amp;gt;
	  	&amp;lt;servlet-class&amp;gt;org.springframework.web.servlet.DispatcherServlet&amp;lt;/servlet-class&amp;gt;
&amp;lt;/servlet&amp;gt;
&amp;lt;servlet-mapping&amp;gt;
        &amp;lt;servlet-name&amp;gt;dispatcher&amp;lt;/servlet-name&amp;gt;
        &amp;lt;url-pattern&amp;gt;/*&amp;lt;/url-pattern&amp;gt;
&amp;lt;/servlet-mapping&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;모든 URL요청(/)이 들어오면 DispatcherServlet.class로 보내 처리하도록 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;servlet-name은 아무거나 지정해도 상관이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;servlet-class의 이름을 작성하기 힘들거나 실수할 것 같으면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이브러리에서 DispatcherServlet.class를 찾아 우클릭 후&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Copy Qualified Name을 하면 패키지와 클래스 이름이 복사된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;붙여넣기 할때 .class까지 같이 붙여넣어지므로 꼭 이때는 .class를 지워준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;--servletxml-파일-만들기&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;-servlet.xml 파일 만들기&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지만 설정하고 jsp를 만들어 실행하면 오류가 발생한다.&lt;br /&gt;오류의 내용은 다음과 같다.&lt;br /&gt;/WEB-INF/dispatcher(내가 매핑할때 썼던 이름)-servlet.xml 이 없기 때문이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;DispatcherServlet과 컨트롤러가 분리되면서 DispatcherServlet은 모든 요청을 받게된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 각 요청에 맞는 매핑을 찾아서 컨트롤러를 호출해야 하는데,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 매핑정보가 web.xml이 아닌&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;(내가 지정한 이름)-servlet.xml 파일에 담기게 되는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;현재는 이 파일이 없는 상태이므로 만들어 준다.&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;u&gt;위치 : WEB-INF 폴더 안에 xml 파일 생성&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;xml 파일을 생성할 때 이름은 -servlet.xml을 꼭 뒤에 붙여주기만 한다면 아무렇게나 지어도 된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;안에 내용을 채우기 위해서 먼저&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs&quot;&gt;https://docs.spring.io/spring-framework/docs&lt;/a&gt;&lt;br /&gt;사이트에 들어가서 내가 spring-webmvc 라이브러리를 설치한 버전과 맞는 곳으로 들어간다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;나는 5.2.9 버전이기 때문에 &lt;b&gt;spring-framework/docs/5.2.x&lt;/b&gt;으로 들어갔다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 맨 밑의 spring-framework-reference로 들어간다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;core &amp;gt; 1.The IoC Contatiner &amp;gt; 1.2.1 부분의 beans 코드&lt;/p&gt;
&lt;pre id=&quot;code_1728716694121&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
    xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans
        				https://www.springframework.org/schema/beans/spring-beans.xsd&quot;&amp;gt;

    &amp;lt;bean id=&quot;/index&quot; class=&quot;com.newlecture.web.controller.IndexController&quot;&amp;gt;  
    	&amp;lt;!-- /index URL요청이 오면 IndexController를 객체화해서 호출한다 --&amp;gt;
        &amp;lt;!-- collaborators and configuration for this bean go here --&amp;gt;
    &amp;lt;/bean&amp;gt;

&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때까지 이미 만들어져 있는 web.xml을 받아와서 실습을 진행했었는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 배우고 보니 자동으로 기입되어 있는 코드가 아니란걸 알게 되어 좀 신기했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 09.16. 23:51';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>DispatcherServlet</category>
      <category>Library</category>
      <category>Spring</category>
      <category>라이브러리</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/62</guid>
      <comments>https://cases.tistory.com/entry/Spring-DispatcherServlet#entry62comment</comments>
      <pubDate>Sat, 12 Oct 2024 16:31:18 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] QueryString</title>
      <link>https://cases.tistory.com/entry/Spring-QueryString</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 QueryString을 파라미터로 받아오는 방법에 대해 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, URL요청이 localhost:8080/info?name=&quot;yubin&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이렇게 들어왔을 때, info 뒤의 name이 QueryString이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;요청을 받아서 처리하는 컨트롤러에서 이 쿼리스트링을 사용해야 한다면 그냥 바로 파라미터로 받아올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728640785804&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/info&quot;)
public void info(String name) {
	System.out.println(name);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원래라면 request에서 이 쿼리스트링을 받아와야 하지만,&lt;br /&gt;스프링은 아주 편하게 자동으로 연결해준다.하지만 주의해야할 점은 스프링이 파라미터의 이름과 쿼리스트링의 이름이 일치해야만 연결을 해준다는 것이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 파라미터의 이름을 name이 아닌 다른 이름으로 받고싶다면 어떻게 해야할까?&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;보통 브라우저는 URL길이의 제한을 두고 있기 때문에 쿼리스트링은 최대한 간결하게 적는 것이 좋고,&lt;br /&gt;자바에서 변수의 이름은 최대한 알기 쉽고 직관적으로 적어줘야 하기 때문에 서로의 이름이 달라질 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@RequestParam&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 문제를 해결하기 위해 @RequestParam 어노테이션을 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1728640877124&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/info&quot;)
public void info(@RequestParam(name=&quot;name&quot;) String userName) {
	System.out.println(userName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드로 바꿔 준다면 쿼리스트링으로 들어온 name을 위 메서드에서는 userName으로 바꿔 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;-requestparam의-옵션&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;@RequestParam의 옵션&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;name&lt;/b&gt; : &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;바로 위에서 name옵션을 사용하여 파라미터의 변수 이름을 바꿔줄 수 있었다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;defaultValue&amp;nbsp;&lt;/b&gt;: &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;위와 같은 코드를 쓴 다음 그냥 /info URL로 들어가면 어떻게 될까? userName이라는 문자열을 받아오기로 했는데 아무것도 들어가지 않았기 때문에 오류가 발생한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;이 때 사용할 수 있는 옵션이 defaultValue다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;요청에 지정한 쿼리스트링이 들어오지 않았을 경우 해당 쿼리스트링의 기본값을 지정할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt; &lt;b&gt;required&amp;nbsp;&lt;/b&gt;: &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;이 required는 따로 지정하지 않으면 기본값은 true로 설정되어있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;만약 이 옵션을 false로 설정한다면 쿼리스트링의 값이 들어오지 않았을 경우 값을 null로 처리한다.&lt;/span&gt; &lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1728641012011&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/info&quot;)
public void info(@RequestParam(name=&quot;name&quot;, required=false) String userName) {
	System.out.println(userName);
}
// 만약 name 쿼리스트링이 들어오지 않은채 위 함수가 실행되면
// userName의 값은 null로 처리된다.&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;-쿼리스트링의-자료형&quot; style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;QueryString의 자료형&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 쿼리스트링은 받아올 때 String으로 인식한다.&lt;br /&gt;따라서 매개변수의 타입도 String으로 해야 옳다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어,&amp;nbsp;&lt;span style=&quot;background-color: #f8f9fa; color: #212529; text-align: start;&quot;&gt;localhost:8080/info?name=&quot;yubin&quot;&amp;amp;age=&quot;20&quot;&lt;/span&gt; 같은 요청이 들어왔을 때,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;age의 값은 숫자 30이 아니라 문자열 30으로 인식한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 원래라면 모두 String으로 받아야 옳지만&lt;br /&gt;스프링은 이를 인식하여 int로 바꿔주기 때문에 자료형을 int로 해도 괜찮다&lt;/p&gt;
&lt;pre id=&quot;code_1728641125077&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequestMapping(&quot;/info&quot;)
public void info(@RequestParam(name=&quot;age&quot;) int userAge) {
	System.out.println(userName);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 스프링이 자동으로 바꿔주지 않는다면&lt;br /&gt;String을 int로 바꿔주는 코드 1줄이 더 추가되었을 것이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Spring이 자동으로 변환할 수 없는 자료형이거나 잘못된 값이 들어오면 예외가 발생할 수 있으므로, 자료형 변환에 꼭 주의해야 한다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 08.20. 17:11';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>QueryString</category>
      <category>RequestParam</category>
      <category>Spring</category>
      <category>자료형</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/61</guid>
      <comments>https://cases.tistory.com/entry/Spring-QueryString#entry61comment</comments>
      <pubDate>Fri, 11 Oct 2024 19:06:45 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] ViewResolver</title>
      <link>https://cases.tistory.com/entry/Spring-ViewResolver</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 ViewResolever에 대해서 한 번 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre id=&quot;code_1728639765496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class IndexController implements Controller{
	
	@Override
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) 
									throws Exception {
		ModelAndView mav = new ModelAndView(&quot;/WEB-INF/view/index.jsp&quot;);
		mav.addObject(&quot;text&quot;, &quot;Spring MVC&quot;);

		return mav;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 view로 이동하기 위해서 controller에서는 경로를 적어줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위와 같이 복잡한 경로를 모든 컨트롤러에 계속해서 적어주는 것은 아주 불편할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 경로는&amp;nbsp;&lt;b&gt;&quot;/WEB-INF/view/ ... .jsp&quot;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 공통 부분이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이 공통부분을 제외하고 jsp페이지의 이름만 적는다면 경로를 적을때 훨씬 수월할 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이때 공통부분을 알아서 적용시킬 수 있는 것이 바로 ViewResolever이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ViewResolver&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ViewResolver의 종류는 많은 데 그 중 InternalResourceViewResolver는 뷰 이름을 실제 JSP 파일의 경로로 변환하는 역할을 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링은 ModelAndView에서 객체를 반환할 때 InternalResourceViewResolver를 거쳐서 처리함으로써&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;뷰 이름을 경로로 바꿀 수 있게된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;prefix는 뷰 이름의 앞에, suffix는 뷰 이름의 뒤에 붙여져서 파일 경로를 생성한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ViewResolver 설정하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;설정을 위해 dispatcher-servlet.xml 파일로 들어간다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;bean태그를 만들고 옵션의 클래스 부분을 작성해야 하는데,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;실수를 줄이기 위해 아무 클래스에 들어가서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;InternalResourceViewResolver&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;를 임포트해서 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;임포트 부분의 경로를 복사해서 붙이는 것이 좋다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728639997727&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&amp;gt;
    &amp;lt;property name=&quot;prefix&quot; value=&quot;/WEB-INF/view/&quot; /&amp;gt;
    &amp;lt;property name=&quot;suffix&quot; value=&quot;.jsp&quot; /&amp;gt;
&amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정해주면 이제 ModelAndView에 복잡한 경로를 넣지 않고 view의 이름만&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성해 주어도 페이지 이동이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론, 해당 페이지는 WEB-INF/view 폴더 안에 존재해야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 그냥 흘려듣고 넘어갔던 내용인데, 이러한 사소한(?) 용어까지 정확히 짚고 넘어가야&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 그 다음 내용을 들어도 코드의 흐름을 파악할 때 더 수월하게 이해가 된다고 생각이 들어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한 번 정리해 보았다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 06.18. 01:17';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>View</category>
      <category>ViewResolver</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/60</guid>
      <comments>https://cases.tistory.com/entry/Spring-ViewResolver#entry60comment</comments>
      <pubDate>Fri, 11 Oct 2024 18:50:16 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 인증 메일 보내기</title>
      <link>https://cases.tistory.com/entry/Spring-%EC%9D%B8%EC%A6%9D-%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;웹 사이트에 새로 가입을 할 때 이메일로 인증 코드를 받는 경우가 되게 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그 인증 메일을 보내는 방법을 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;사전 작업&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 프로젝트 내에서 바로 id, password 를 임의로 작성해줄 것이기 때문에&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;편의성을 위해서는 2차인증을 &quot;해제&quot; 해두자.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. 네이버 메일 -&amp;gt; 환경설정 -&amp;gt; POP3/IMAP 설정에서,&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;POP3/SMTP 설정 , IMAP/SMTP 설정 둘다 사용함으로 체크해둔 후, &quot;저장&quot;해두어야 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;꼭, 네이버가 아니더라도&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2번에서 언급한 환경설정만 해둔다면 메일 보내기 가능하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3. 프로젝트에 필요한 설정&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pom.xml&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존성 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728637066356&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-websocket&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;${org.springframework-version}&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- 스프링에서 STOMP 처리를 위한 라이브러리 --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.integration&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-integration-stomp&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;5.4.13&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;


		&amp;lt;!-- https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;com.sun.mail&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;jakarta.mail&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;2.0.1&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;

		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-context-support&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;${org.springframework-version}&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
	&amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;serlvet-context.xml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;web-socket message-broker prefix 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728637137273&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;websocket:message-broker application-destination-prefix=&quot;/app&quot;&amp;gt;
		&amp;lt;websocket:stomp-endpoint path=&quot;/endpoint&quot;&amp;gt;
			&amp;lt;websocket:sockjs websocket-enabled=&quot;true&quot; /&amp;gt;
		&amp;lt;/websocket:stomp-endpoint&amp;gt;
		&amp;lt;websocket:simple-broker prefix=&quot;/broker&quot; /&amp;gt;
&amp;lt;/websocket:message-broker&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;root-context.xml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;component를 사용할 거기 때문에 스프링 빈 등록&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728637189078&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
	xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
	xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd&quot;&amp;gt;
	
	&amp;lt;!-- Root Context: defines shared resources visible to all other web components --&amp;gt;
		
		&amp;lt;context:component-scan base-package=&quot;com.itbank.component&quot; /&amp;gt;
	
&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ex03.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증번호가 담겨있는 메일을 보내고,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자에게 인증번호입력을 받아서&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;일치하는지 확인하기.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인증번호 보내기 버튼을 누르면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;숨어있던 인증번호 입력 form이 나타난다.&lt;/p&gt;
&lt;pre id=&quot;code_1728637255914&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ include file=&quot;header.jsp&quot; %&amp;gt;


&amp;lt;h3&amp;gt;AJAX로 메일보내기&amp;lt;/h3&amp;gt;


&amp;lt;div class=&quot;mailSend&quot;&amp;gt;
	&amp;lt;form&amp;gt;
		&amp;lt;h3&amp;gt;인증번호 발송&amp;lt;/h3&amp;gt;
		&amp;lt;p&amp;gt;
			&amp;lt;input type=&quot;email&quot; name=&quot;address&quot; placeholder=&quot;email&quot;&amp;gt;
			&amp;lt;button&amp;gt;인증번호 보내기&amp;lt;/button&amp;gt;	
		&amp;lt;/p&amp;gt;
		&amp;lt;p class=&quot;message&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;div class=&quot;auth box hidden&quot;&amp;gt;
	&amp;lt;form&amp;gt;
		&amp;lt;h3&amp;gt;인증번호 확인&amp;lt;/h3&amp;gt;
		&amp;lt;p&amp;gt;
			&amp;lt;input type=&quot;text&quot; name=&quot;authNumber&quot; placeholder=&quot;인증번호 입력&quot;&amp;gt;
			&amp;lt;button&amp;gt;인증 확인&amp;lt;/button&amp;gt;	
		&amp;lt;/p&amp;gt;
		&amp;lt;p class=&quot;message&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;


&amp;lt;script&amp;gt;
	const mailSendForm = document.forms[0]		//	form은 배열로 바로 불러올 수 있음
	const authForm = document.forms[1]
	
	mailSendForm.onsubmit = async function(event) {
		event.preventDefault()
		const url = '${cpath}/ajax/sendMail'
		const opt = {
				method: 'POST',
				body : JSON.stringify({
					address: event.target.querySelector('input').value
				}),
				headers: {
					'Content-Type' : 'application/json;charset=utf-8'
				}
		}
		const result = await fetch(url, opt).then(resp =&amp;gt; resp.text())
		const message = event.target.querySelector('p.message')
		if(result == 1) {
			message.innerText = '메일을 전송했습니다'
			message.style.color = 'blue'
			document.querySelector('.auth').classList.remove('hidden')
		}
		else {
			message.innerText = '메일을 보낼 수 없습니다'
			message.style.color = 'red'
		}
	}
	
	
	
	authForm.onsubmit = async function(event) {
		event.preventDefault()
		
		const inputNumber = event.target.querySelector('input').value
		const url = '${cpath}/ajax/authNumber/' + inputNumber	//	/를 꼭 붙여야 pathVariable 사용할 수 있음
		const result = await fetch(url).then(resp =&amp;gt; resp.text())
		const message = event.target.querySelector('p.message')
		
		if(result == 1) {
			message.innerText = '인증 성공'
			message.style.color = 'blue'
		}
		else {
			message.innerText = '인증 실패'
			message.style.color = 'red'
		}
	}
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;AjaxController&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728637299204&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.HashMap;
import java.util.Random;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.itbank.component.MailComponent;


//	AJAX 니까 그냥 controller 말고 Restcontroller

//	문자열 하나만 받는다면, 굳이 ajax 필요없음
//	객체로 묶어주기 위해서 ajax 로 처리하자
@RestController
@RequestMapping(&quot;/ajax&quot;)
public class AjaxController {

	@Autowired private MailComponent component;
	
	private Random ran = new Random();
	
	@PostMapping(&quot;/sendMail&quot;)
	public int sendMail(@RequestBody HashMap&amp;lt;String, String&amp;gt; param, HttpSession session) {
		
		System.out.println(&quot;address : &quot; + param.get(&quot;address&quot;));
		int num = ran.nextInt(999999);
		
		String authNumber = String.format(&quot;%06d&quot;, num);
		System.out.println(&quot;authNumber : &quot; + authNumber);
		
		session.setAttribute(&quot;authNumber&quot;, authNumber);
		session.setMaxInactiveInterval(180);
		
		
		param.put(&quot;subject&quot;, &quot;인증번호&quot;);
		param.put(&quot;content&quot;, authNumber);
		
		int row = component.sendMimeMessage(param);
		
		System.out.println(row != 0 ? &quot;전송 성공&quot; : &quot;전송 실패&quot;);
		
		return row;
	}
	
	@GetMapping(&quot;/authNumber/{inputNumber}&quot;)
	public int authNumber(@PathVariable(&quot;inputNumber&quot;) String inputNumber, HttpSession session) {
		//	만약, 세션이 만료되었다면 (== 180초)
		//	authNumber 의 값은 1이다
		//	두개의 문자열의 일치를 비교할때 A.equals(B) 형태로 비교한다
		//	null 일 가능성이 있는 문자열을 뒤에 배치하여 Nullpointer 에러를 방지함
		
		//	만약, 세션이 만료되었을 때 예외를 발생시켜서 다른 반환값을 전달하려면,
		//	ExceptionHandler 를 사용하거나,
		//	@RestControllerAdvice 클래스를 작성하여 처리할 수도 있다
		String authNumber = (String) session.getAttribute(&quot;authNumber&quot;);
		
		int row = 0;
		
		if(inputNumber.equals(authNumber)) {
			row = 1;
		}
		return row;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MailComponent&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728637334172&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.IOException;
import java.util.HashMap;
import java.util.Properties;
import java.util.Scanner;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import jakarta.mail.Authenticator;
import jakarta.mail.Message;
import jakarta.mail.MessagingException;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;

@Component
public class MailComponent {

   private final String host = &quot;smtp.naver.com&quot;;
   private final int port = 465;
   private String serverId = &quot;로그인할 아이디&quot;;		//	이 계정으로 로그인해서 메일을 보낼것임 
   private String serverPw = &quot;비번&quot;;
   
   private Properties props;
   
   //	@Autowired 가 자동으로 스프링 빈 연결하듯이
   //	@Value는 자동으로 자원(파일)을 연결한다
   //	org.springframework.core.io.Resource
   //	classpath : &quot;src/main/java&quot;		or		&quot;src/main/resources
   @Value(&quot;classpath:mailForm.html&quot;)
   private Resource mailForm;
   
   @PostConstruct
   private void init() {
      props = new Properties();
      props.put(&quot;mail.smtp.host&quot;, host);
      props.put(&quot;mail.smtp.prot&quot;, port);
      props.put(&quot;mail.smtp.auth&quot;, &quot;true&quot;);
      props.put(&quot;mail.smtp.ssl.enable&quot;, &quot;true&quot;);
      props.put(&quot;mail.smtp.true&quot;, host);
   }
   
   // 단순 텍스트 메일 보내기 (ex01)
   public int sendSimpleMessage(String address, String content, String subject) {
      
      // 1) 메일 서버 인증 (접속에 필요하다)
      Session mailSession = Session.getDefaultInstance(props, new Authenticator() {
         String un = serverId;
         String pw = serverPw;
         
         @Override
         protected PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(un, pw);
         }
      });
      
      mailSession.setDebug(true);      // 메일 전송 과정을 콘솔창에 출력한다
      
      // 2) 보낼 메세지 작성
      Message message = new MimeMessage(mailSession);
      
      try {
         message.setFrom(new InternetAddress(serverId + &quot;@naver.com&quot;));
         message.setRecipient(Message.RecipientType.TO, new InternetAddress(address));   // 받는사람
         message.setSubject(subject);   // 제목
         message.setText(content);   // 내용
         
         Transport.send(message);   // 3) 준비가 끝난 메시지를 발송한다
         return 1;
         
      } catch (MessagingException e) {
         e.printStackTrace();
         return 0;
      }
   }

   //	html 포함한 메일(ex02)
   public int sendMimeMessage(HashMap&amp;lt;String, String&amp;gt; param) {

	      // 1) 메일 서버 인증 (접속에 필요하다)
	      Session mailSession = Session.getDefaultInstance(props, new Authenticator() {
	         String un = serverId;
	         String pw = serverPw;
	         
	         @Override
	         protected PasswordAuthentication getPasswordAuthentication() {
	            return new PasswordAuthentication(un, pw);
	         }
	      });
	      
	      mailSession.setDebug(true);      // 메일 전송 과정을 콘솔창에 출력한다
	      
	      
	      // 2) 보낼 메세지 작성
	      Message message = new MimeMessage(mailSession);
	      String address = param.get(&quot;address&quot;);
	      String subject = param.get(&quot;subject&quot;);
	      String content = param.get(&quot;content&quot;);
	      
	      try {
	         message.setFrom(new InternetAddress(serverId + &quot;@naver.com&quot;));
	         message.setRecipient(Message.RecipientType.TO, new InternetAddress(address));   // 받는사람
	         message.setSubject(subject);   // 제목
	         
	         
	         String tag = &quot;&quot;;
	         Scanner sc = new Scanner(mailForm.getFile());
	         
	         while(sc.hasNextLine()) {
	        	 tag += sc.nextLine();
	         }
	         sc.close();
	         
	         content = String.format(tag, content);
	         
	         message.setContent(content, &quot;text/html; charset=utf-8&quot;);	//	태그 포함 내용
	         
	         Transport.send(message);   
	         return 1;
	         
	      } catch (MessagingException | IOException e) {
	         e.printStackTrace();
	         return 0;
	      }

   }
   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버가 아니라 구글이라도 메일을 보내려면 POP/IMAP을 활성화 해야하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이트 별로 port가 다르기 때문에 유의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 port : 465&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글 port : 587&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 05.30. 21:17';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>mail</category>
      <category>Spring</category>
      <category>이메일인증</category>
      <category>인증메일</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/59</guid>
      <comments>https://cases.tistory.com/entry/Spring-%EC%9D%B8%EC%A6%9D-%EB%A9%94%EC%9D%BC-%EB%B3%B4%EB%82%B4%EA%B8%B0#entry59comment</comments>
      <pubDate>Fri, 11 Oct 2024 18:07:09 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] hidden 활용</title>
      <link>https://cases.tistory.com/entry/JavaScript-hidden-%ED%99%9C%EC%9A%A9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;hidden을 활용하여 다중 필터 기능을 구현해볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;배열에 사용할 수 있는 함수&lt;/b&gt;&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;함수&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;arr.forEach&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;단순&amp;nbsp;반복&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;arr.filter&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;조건&amp;nbsp;필터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;arr.toSorted&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;arr.map&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;재구성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;arr.slice&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;잘라내기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;arr.includes&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;포함여부&amp;nbsp;확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;다중필터&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;boxList 는 check box의 내용을 모두 불러온것 (querySelect 로 불러온건 모두 nodeList)&lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const boxList = document.querySelectorAll('div.left &amp;gt; label &amp;gt;&amp;nbsp;&amp;nbsp;input[type=&quot;checkbox&quot;]')&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배열에서 원하는 속성만 가져오거나,&amp;nbsp; 별도의 속성을 추가해서 가져올때 Array.map 사용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;map 을 이용하여 name 속성에 있는 데이터에서 a 를 지우고(replace) 정수(+)만 남긴다&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const checkedArray = Array.from(boxList).filter(e =&amp;gt; e.checked).map(e =&amp;gt; +e.name.replace('a', ''))&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;반복문을 이용하여, 소수점 아래 지움(floor)&lt;/b&gt;&lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; const age = Math.floor(+tr.children[1].innerText / 10) * 10&amp;nbsp;&lt;b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;체크박스에 체크가 되어있는 목록에서 해당 나이가 있다면&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;if(checkedArray.includes(age)) {&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;hidden 목록에서 제거&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;tr.classList.remove('hidden') }&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;아니라면,&amp;nbsp;hidden&amp;nbsp;에&amp;nbsp;추가&lt;/b&gt;&amp;nbsp; &amp;nbsp;  &lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;else { tr.classList.add('hidden') }&lt;b&gt;&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;체크박스를 클릭하면 filterHandler 실행&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;boxList.forEach(box =&amp;gt; box.onclick = filterHandler&lt;/p&gt;
&lt;pre id=&quot;code_1728636362585&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
        #root {
            display: flex;
            width: 500px;
            height: 350px;
        }
        .left {
            display: flex;
            flex-direction: column;
            width: 100px;
        }
        .left &amp;gt; label {
            flex: 1
        }
        table {
            border-collapse: collapse;
            border: 2px solid black;
        }
        td {
            border: 1px solid grey;
            padding: 10px 20px;
        }
        tr.hidden {
            display: none;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

    &amp;lt;h1&amp;gt;다중조건필터&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;

    &amp;lt;div id=&quot;root&quot;&amp;gt;
        &amp;lt;div class=&quot;left&quot;&amp;gt;
            &amp;lt;label&amp;gt;&amp;lt;input type=&quot;checkbox&quot; name=&quot;a10&quot;&amp;gt;10대&amp;lt;/label&amp;gt;
            &amp;lt;label&amp;gt;&amp;lt;input type=&quot;checkbox&quot; name=&quot;a20&quot;&amp;gt;20대&amp;lt;/label&amp;gt;
            &amp;lt;label&amp;gt;&amp;lt;input type=&quot;checkbox&quot; name=&quot;a30&quot;&amp;gt;30대&amp;lt;/label&amp;gt;
            &amp;lt;label&amp;gt;&amp;lt;input type=&quot;checkbox&quot; name=&quot;a40&quot;&amp;gt;40대&amp;lt;/label&amp;gt;
            &amp;lt;label&amp;gt;&amp;lt;input type=&quot;checkbox&quot; name=&quot;a50&quot;&amp;gt;50대&amp;lt;/label&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;right&quot;&amp;gt;
            &amp;lt;table&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;남도일&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;17&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;카리나&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;25&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;이지은&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;31&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;홍진호&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;42&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;유재석&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;51&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;천우희&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;37&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;윈터&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;23&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;td&amp;gt;나루토&amp;lt;/td&amp;gt;
                    &amp;lt;td&amp;gt;16&amp;lt;/td&amp;gt;
                &amp;lt;/tr&amp;gt;

            &amp;lt;/table&amp;gt;

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


    &amp;lt;!--    다중 필터   --&amp;gt;
    &amp;lt;script&amp;gt;
        const boxList = document.querySelectorAll('div.left &amp;gt; label &amp;gt;  input[type=&quot;checkbox&quot;]')     //  NodeList

        function filterHandler(event) {
           
            const checkedArray = Array.from(boxList).filter(e =&amp;gt; e.checked).map(e =&amp;gt; +e.name.replace('a', ''))
           
            console.log(checkedArray)

            const trList = document.querySelectorAll('table &amp;gt; tbody &amp;gt; tr')

            trList.forEach(tr =&amp;gt; {
               
                const age = Math.floor(+tr.children[1].innerText / 10) * 10 
                
                if(checkedArray.includes(age)) {    
                    tr.classList.remove('hidden')   
                }
                else {
                    tr.classList.add('hidden')   
                }
            })
        }
        boxList.forEach(box =&amp;gt; box.onclick = filterHandler)
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;클릭이벤트의 대상만 처리 한다면&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728636429502&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    const boxList = document.querySelectorAll('div.left &amp;gt; label &amp;gt;  input[type=&quot;checkbox&quot;]')

    function filterHandler(event) {
        //  배열에서 원하는 속성만 가져오거나
        //  별도의 속성을 추가해서 가져올때 Array.map 사용
        
        const checkedArray = Array.from(boxList).map(e =&amp;gt;  {
            const ob = {
                age : +e.name.replace('a', ''),
                checked  : e.checked
            }
            return ob
        })
        console.log(checkedArray)

        //  만약, 다중조건이 아니라 클릭이벤트의 대상만 처리한다면
        const flag = +event.target.name.replace('a', '')

        const trList = document.querySelectorAll('table &amp;gt; tbody &amp;gt; tr')

        trList.forEach(tr =&amp;gt; {
            const value = +tr.children[1].innerText

            if(flag &amp;lt;= value &amp;amp;&amp;amp; value &amp;lt; flag + 10 ) {
                tr.classList.remove('hidden')
            }
            else {
                tr.classList.add('hidden')
            }
        })
        
    }

    boxList.forEach(box =&amp;gt; box.onclick = filterHandler)
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hidden 속성을 추가, 삭제하여 해당하지 않는 데이터는 사용자에게 보여지지 않게&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 말하는 필터 기능을 구현하니 또 색달랐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 필터 기능이더라도 어떤식으로 접근 하느냐에 따라 많은 방법들이 존재하는 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 05.16. 16:30';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>Hidden</category>
      <category>JavaScript</category>
      <category>필터</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/58</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-hidden-%ED%99%9C%EC%9A%A9#entry58comment</comments>
      <pubDate>Fri, 11 Oct 2024 17:51:08 +0900</pubDate>
    </item>
    <item>
      <title>[Project] 프로젝트 후기</title>
      <link>https://cases.tistory.com/entry/Project-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;힘들고 험난했지만, 정말 다시는 겪지 못할 만큼 재밌었던 팀원들 과의 프로젝트가 끝이났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 한 달 가량을 매일 붙어서 프로젝트를 진행하다 보니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들과 상상 이상으로 친해져서 끝난 후에 같이 피크닉도 다녀왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 팀이 만들어졌을때는 이렇게 자주 만나서 놀고 할 정도로 친해질 줄은 몰랐는데...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20241010_224606432_01.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OgSbg/btsJZVO0AkF/ArejZNndXy7LjAisMG98Ek/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OgSbg/btsJZVO0AkF/ArejZNndXy7LjAisMG98Ek/img.jpg&quot; data-alt=&quot;케잌이랑 스피커 싸들고 돗자리에서 피크닉 즐기기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OgSbg/btsJZVO0AkF/ArejZNndXy7LjAisMG98Ek/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOgSbg%2FbtsJZVO0AkF%2FArejZNndXy7LjAisMG98Ek%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; data-filename=&quot;KakaoTalk_20241010_224606432_01.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;케잌이랑 스피커 싸들고 돗자리에서 피크닉 즐기기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때가 딱 봄이라서 벚꽃 구경도 할 겸 같이 만나서 피크닉도 즐겨줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20241010_224606432_05.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu231W/btsJ04RMMX6/zpIYapoDSo6dxknN2TiY9K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu231W/btsJ04RMMX6/zpIYapoDSo6dxknN2TiY9K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu231W/btsJ04RMMX6/zpIYapoDSo6dxknN2TiY9K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu231W%2FbtsJ04RMMX6%2FzpIYapoDSo6dxknN2TiY9K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot; data-filename=&quot;KakaoTalk_20241010_224606432_05.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀러가기 전 단백질 보충도 충분히 해주었다. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1sqiK/btsJZ6XbRYN/w16sKJgu6h2EHmszffoJcK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1sqiK/btsJZ6XbRYN/w16sKJgu6h2EHmszffoJcK/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_20241010_225235713.jpg&quot; width=&quot;378&quot; data-widthpercent=&quot;50&quot; style=&quot;width: 49.4186%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1sqiK/btsJZ6XbRYN/w16sKJgu6h2EHmszffoJcK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1sqiK%2FbtsJZ6XbRYN%2Fw16sKJgu6h2EHmszffoJcK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b03xEU/btsJ1M30Mh4/b6stKx8dLXp85wR1wM6HU1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b03xEU/btsJ1M30Mh4/b6stKx8dLXp85wR1wM6HU1/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;KakaoTalk_20241010_225235713_01.jpg&quot; width=&quot;401&quot; height=&quot;535&quot; style=&quot;width: 49.4186%;&quot; data-widthpercent=&quot;50&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b03xEU/btsJ1M30Mh4/b6stKx8dLXp85wR1wM6HU1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb03xEU%2FbtsJ1M30Mh4%2Fb6stKx8dLXp85wR1wM6HU1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;감기걸린 채로 프로젝트 하러 나오니까 꿀물 챙겨 준 우리 팀...&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 팀에서 막내라 그런지 매번 챙김을 받기만 한 것 같아서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 미안하고 고마웠다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 시작할때는 아직 사이도 어색하고, 우리가 정한 주제이지만 잘 해낼 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라는 생각이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트를 진행하면서 팀원들이 전부 의욕 넘치게 다 도와주고, 의견을 주고받으니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힘든게 없었다면 거짓말이겠지만 엄청 힘들 일도 덕분에 쉽게 해결할 수 있었던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트가 끝나고 나면 정말 기쁠 줄 알았는데, 막상 끝이나니 뭔가 아쉬운 마음이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 많이 드는 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 소규모 프로젝트때 아는게 많이 없어 구현할 수 없었던 것을 많이 보완할 수 있어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그건 많이 뿌듯한 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히, 저번 프로젝트때는 비동기 함수로 처리하는 방법을 몰랐어서 좋아요 기능이나 찜 같은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부분을 페이지의 새로고침 없이 처리할 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이번 프로젝트를 진행하면서 그러한 부분을 보완하여 웹페이지의 요청, 응답 속도를 개선하니&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확실히 편의성이 향상된 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 완벽하진 않지만 불과 한 두달 전에 비해서 많이 성장한 것 같은 나를 보니 더 열심히 하고 싶은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기부여가 확실히 된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;카톡.jpg&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;1503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l3Pf8/btsJZWmR6sx/eRcz517usaN4rfINKZfPI0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l3Pf8/btsJZWmR6sx/eRcz517usaN4rfINKZfPI0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l3Pf8/btsJZWmR6sx/eRcz517usaN4rfINKZfPI0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl3Pf8%2FbtsJZWmR6sx%2FeRcz517usaN4rfINKZfPI0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1179&quot; height=&quot;1503&quot; data-filename=&quot;카톡.jpg&quot; data-origin-width=&quot;1179&quot; data-origin-height=&quot;1503&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 다음에 프로젝트를 하게 되면 카카오톡 말고 코드를 주고 받을 수 있는 더 좋은 방법을 찾아봐야겠다 ㅎㅎ..&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 04.20. 21:37';
&lt;/script&gt;</description>
      <category>Project</category>
      <category>PROJECT</category>
      <category>Review</category>
      <category>Spring</category>
      <category>팀프로젝트</category>
      <category>후기</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/57</guid>
      <comments>https://cases.tistory.com/entry/Project-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9B%84%EA%B8%B0#entry57comment</comments>
      <pubDate>Thu, 10 Oct 2024 23:25:36 +0900</pubDate>
    </item>
    <item>
      <title>[Project] 실시간 1:1 채팅</title>
      <link>https://cases.tistory.com/entry/Project-%EC%8B%A4%EC%8B%9C%EA%B0%84-11-%EC%B1%84%ED%8C%85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;내가 맡은 역할 중 실시간 1:1 채팅 기능이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 STOMP 프로토콜을 활용한 채팅 구현을 해본 경험이 있기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 별로 대수롭지 않게 생각했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 그건 나의 엄청난 오만이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 채팅을 하는 시스템은 비슷하지만 저번에 했던 채팅은 단순한 단체 채팅방 이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에 필요한 기능은 관리자와 고객들의 1:n 관계 채팅 시스템이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;channel 생성 부터 구독 하는 것까지 엄청 골머리를 썩혔지만 결국 해결해냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 내가 이 난관을 어떻게 극복했는지 한 번 적어보겠다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;유저 채팅 채널 생성&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728553664218&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
	const chat = document.getElementById('chatIcon')
	const questions = document.querySelectorAll('.question')
	const userid = '${login.userid}'
	questions.forEach(e =&amp;gt; e.onclick = clickHandler)
	
	function clickHandler(event) {
		const arr = Array.from(questions)
		const idx = arr.indexOf(event.currentTarget)
		
		const answer = document.querySelectorAll('.answer')
		answer[idx].classList.toggle('hidden')
	}
	
	chat.onclick = async function() {
		const url = '${cpath}/create/' + userid
		const result = await fetch(url).then(resp=&amp;gt;resp.text())
	}
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 관리자가 아닌 일반 사용자의 입장에서 채팅 icon을 클릭하면&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 유저의 고유키인 &quot;userid&quot;를 이용해 독단적인 채널을 생성하게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;사용자 마다 각각 다르게 개별적으로 채널을 어떻게 생성해야 하지?&quot; 라는 고민을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;했었는데, &quot;그럼 각각 마다 다르게 가지고 있는게 뭐가 있을까&quot; 라고 생각을 하던 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 고유키인 &quot;userid&quot;를 사용하자 ! 라는 결론이 나오게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;유저 채팅 기능&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728554409566&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// onclick
exit.onclick = exitChat
send.onclick = sendMsg
inputMsg.onkeyup = function(e) {
	if(e.key == 'Enter') sendMsg()
	if(e.key == 'Escape') e.target.value = ''
}
   
stomp.connect({}, onConnect)
function onConnect() {
	stomp.subscribe('/sendTo/' + userid)
	stomp.send('/app/enter/' + userid, {}, JSON.stringify({
		writer: username
	}))
	onInput()
}

async function onInput() {
	const ad = '${cpath}/getRoom/' + userid
	const adResult = await fetch(ad).then(resp=&amp;gt;resp.json())
	const arr = Array.from(adResult)
	console.log(arr)
	chat_idx = arr[0].idx
	console.log(chat_idx)
	const url = '${cpath}/getMsg/' + chat_idx
	const result = await fetch(url).then(resp=&amp;gt;resp.json())
	console.log(result)
	const arr2 = Array.from(result)
	if(arr2.length != 0) {
		arr2.forEach(e =&amp;gt; {
		let messageDirection = 'message-left'; // 기본적으로 왼쪽 정렬
       	if(e.writer != 'admin') { // 현재 사용자가 보낸 메시지인 경우 오른쪽 정렬
         	messageDirection = 'message-right';
      	 }
	 	let str = ''
	    	str += '&amp;lt;div class=&quot;' + messageDirection + '&quot;&amp;gt;'
	    	str += '&amp;lt;div&amp;gt;' + e.nickname + '&amp;lt;/div&amp;gt;'
	    	str += '&amp;lt;div class=&quot;content&quot;&amp;gt;' + e.content + '&amp;lt;/div&amp;gt;'
	    	str += '&amp;lt;/div&amp;gt;'
	   	chatArea.innerHTML += str
		})
	}
   	chatArea.scrollTop = chatArea.scrollHeight;
	stomp.subscribe('/sendTo/' + userid, onReceive)
	stomp.send('app/sendTo/admin/' + userid, {}, JSON.stringify({
		writer: username
	}))
}

async function sendMsg() {
	const text = document.querySelector('input[name=&quot;msg&quot;]').value
	if(text == '') {
		return
	}
	document.querySelector('input[name=&quot;msg&quot;]').value = ''
	console.log(chat_idx)
	const url = '${cpath}/sendMsg'
	const opt = {
			method: 'POST',
			body: JSON.stringify({
				chat_idx: chat_idx,
				writer: userid,
				content: text
			}),
			headers: {
				'Content-Type': 'application/json; charset=utf-8'
			}
	}
	const result = await fetch(url,opt).then(resp=&amp;gt;resp.text())
	stomp.send('/app/sendTo/' + userid, {}, JSON.stringify({
		content: text,
		writer: username
	}))
}

async function exitChat() {
	 const url = '${cpath}/exitChat';
	 try {
	     await fetch(url).then(resp =&amp;gt; resp.text());
	     
	     stomp.send('/app/disconnect', {}, JSON.stringify({
	         writer: username,
	     }));
	     stomp.disconnect(function() {
	         console.log('Disconnected');
	     });
	     
	     window.location.href = '${cpath}/inquiry/list';
	 } catch (error) {
	     console.error('에러:', error);
	 }
}


function onReceive(chat) {
    const chatContent = JSON.parse(chat.body)
    const text = chatContent.content
    const from = chatContent.writer
   	let messageDirection = 'message-left'; // 기본적으로 왼쪽 정렬
 	if (from === username) { // 현재 사용자가 보낸 메시지인 경우 오른쪽 정렬
        messageDirection = 'message-right';
    }
    let str = ''
   	str += '&amp;lt;div class=&quot;' + messageDirection + '&quot;&amp;gt;'
   	str += '&amp;lt;div&amp;gt;' + from + '&amp;lt;/div&amp;gt;'
   	str += '&amp;lt;div class=&quot;content&quot;&amp;gt;' + text + '&amp;lt;/div&amp;gt;'
   	str += '&amp;lt;/div&amp;gt;'
   	chatArea.innerHTML += str
   	chatArea.scrollTop = chatArea.scrollHeight;
}
	
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 자신이 userid를 이용하여 개설한 채팅방에 message input을 해주어야 하기 때문에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;adResult를 배열로 만들어 그 배열의 0번째에 해당하는 채널의 메세지를 볼 수 있게 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 작업도 처음엔 어떻게 같은 채널의 메세지를 보게 할 지 잘 몰랐었는데, 팀원들과 협업하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 잘 해결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;관리자 관점 채팅&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728554717451&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stomp.connect({}, onConnect)
function onConnect() {
	stomp.subscribe('/sendTo/admin', onReceive)
	stomp.send('/app/enter', {}, JSON.stringify({
		writer: username
	}))
}
send.onclick = sendMsg
close.onclick = function() {
    chatFrame.style.bottom = &quot;-600px&quot;; 
    setTimeout(() =&amp;gt; {
        chatFrame.classList.add('hidden');
        chatFrame.classList.remove('visible')
        chatOpenButton.classList.remove('hidden'); 
    }, 500); 
}

chatOpenButton.onclick = function() {
    chatFrame.style.bottom = &quot;-600px&quot;
    chatFrame.classList.remove('hidden')
    chatFrame.classList.add('visible')
    setTimeout(() =&amp;gt; {
        chatFrame.style.bottom = &quot;20px&quot;
    }, 10);
    chatOpenButton.classList.add('hidden'); 
}

inputMsg.onkeyup = function(e) {
	if(e.key == 'Enter') sendMsg()
	if(e.key == 'Escape') e.target.value = ''
}
chatList()

async function chatList() {
	const url = '${cpath}/chatList'
	const result = await fetch(url).then(resp=&amp;gt;resp.json())
	const arr = Array.from(result)
	console.log(arr)
	arr.forEach(e=&amp;gt; {
		let str = ''
		str += '&amp;lt;div class=&quot;chatRoom&quot;&amp;gt;' + e + '&amp;lt;/div&amp;gt;'
		listFrame.innerHTML += str
	})
	const chat = document.querySelectorAll('.chatRoom')
	chat.forEach(e =&amp;gt; e.addEventListener('click', enterChat))
}

async function enterChat(event) {
	userid = event.target.textContent
	const ad = '${cpath}/getRoom/' + userid
	const adResult = await fetch(ad).then(resp=&amp;gt;resp.json())
	const arr = Array.from(adResult)
	chat_idx = arr[0].idx
	console.log(chat_idx)
	const url = '${cpath}/getMsg/' + chat_idx
	const result = await fetch(url).then(resp=&amp;gt;resp.json())
	console.log(result)
	const arr2 = Array.from(result)
	console.log(arr2)
	if(arr2.length != 0) {
		arr2.forEach(e =&amp;gt; {
		let messageDirection = 'message-left'; // 기본적으로 왼쪽 정렬
       	if(e.writer == 'admin') { // 현재 사용자가 보낸 메시지인 경우 오른쪽 정렬
         	messageDirection = 'message-right';
      	 }
	 	let str = ''
	    	str += '&amp;lt;div class=&quot;' + messageDirection + '&quot;&amp;gt;'
	    	str += '&amp;lt;div&amp;gt;' + e.nickname + '&amp;lt;/div&amp;gt;'
	    	str += '&amp;lt;div class=&quot;content&quot;&amp;gt;' + e.content + '&amp;lt;/div&amp;gt;'
	    	str += '&amp;lt;/div&amp;gt;'
	   	chatArea.innerHTML += str
		})
	}
   	chatArea.scrollTop = chatArea.scrollHeight;
	chatFrame.classList.remove('hidden')
	chatFrame.classList.add('visible')
	stomp.subscribe('/sendTo/' + userid, onReceive)
}

async function sendMsg() {
	const text = document.querySelector('input[name=&quot;msg&quot;]').value
	if(text == '') {
		return
	}
	document.querySelector('input[name=&quot;msg&quot;]').value = ''
	const url = '${cpath}/sendMsg'
	const opt = {
			method: 'POST',
			body: JSON.stringify({
				chat_idx: chat_idx,
				writer: '${login.userid}',
				content: text
			}),
			headers: {
				'Content-Type' : 'application/json; charset=utf-8'
			}
	}
	const result = await fetch(url,opt).then(resp=&amp;gt;resp.text())
	stomp.send('/app/sendTo/' + userid, {}, JSON.stringify({
		content: text,
		writer: username
	}))
}

function onReceive(chat) {
    const chatContent = JSON.parse(chat.body)
    const text = chatContent.content
    const from = chatContent.writer
	let messageDirection = 'message-left'; 
    if(from == username) { 
        messageDirection = 'message-right';
    }
   
    let str = ''
    	str += '&amp;lt;div class=&quot;' + messageDirection + '&quot;&amp;gt;'
    	str += '&amp;lt;div&amp;gt;' + from + '&amp;lt;/div&amp;gt;'
    	str += '&amp;lt;div class=&quot;content&quot;&amp;gt;' + text + '&amp;lt;/div&amp;gt;'
    	str += '&amp;lt;/div&amp;gt;'
	chatArea.innerHTML += str
   	chatArea.scrollTop = chatArea.scrollHeight;
}

&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 관리자는 chatList로 채팅이 활성화 되어 있는 전체 리스트를 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 방을 클릭하면 해당 방으로 입장하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 각 사용자마다 채널이 다르기 때문에 각각 채널을 구독시키기 위해서 userid를 let으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선언해주었고, 각 방을 클릭 시 해당 고객과의 1:1 채팅방에 들어갈 수 있게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독할 채널을 추출하는 방식은 전 방식과 동일하게 배열에서 추출하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 이렇게 돌아보면 정말 별거 아닌거 처럼 보일진 모르겠지만, 내 딴에는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 힘들었던 부분이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막혀서 진도는 안나가는데 마감 기한은 점점 다가오고... 하지만 방법은 도저히 모르겠고...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 팀원들에게 도움을 청해서 같이 의견을 주고받으니까 훨씬 수월하게 해결했던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 두번째 팀 프로젝트 이지만 이 경험이 나중에 내 개발자 인생에 있어서 정말 큰 도움이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 03.21. 22:17';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>STOMP</category>
      <category>실시간채팅</category>
      <category>팀프로젝트</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/56</guid>
      <comments>https://cases.tistory.com/entry/Project-%EC%8B%A4%EC%8B%9C%EA%B0%84-11-%EC%B1%84%ED%8C%85#entry56comment</comments>
      <pubDate>Thu, 10 Oct 2024 19:14:38 +0900</pubDate>
    </item>
    <item>
      <title>[Project] Spring 팀 프로젝트 시작</title>
      <link>https://cases.tistory.com/entry/Project-Spring-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 spring 팀 프로젝트를 시작하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 팀 프로젝트에서 사용할 DB인 Oracle의 계정을 새로 만들어 보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;Oracle 계정 생성하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;사전에 VMware에 linux 환경이 구축되어 있고, Oracle도 당연히 설치가 되어 있어야한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;VMware에 접속한 후 터미널을 열고 명령어를 작성해 준다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;명령어&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;명령어에 대한 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: left;&quot;&gt;. oraenv&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: left;&quot;&gt;The Oracle base has been set to /기본 설치 디렉토리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: left;&quot;&gt;create user 계정명 identified by 비밀번호;&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: left;&quot;&gt;User created.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: left;&quot;&gt;grant connect, resource, dba to 계정명;&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: left;&quot;&gt;Grant succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;오라클계정생성.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;804&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmtnqg/btsJ14QBcQI/k5e4ckdVSnq3KFdlyB5znk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmtnqg/btsJ14QBcQI/k5e4ckdVSnq3KFdlyB5znk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmtnqg/btsJ14QBcQI/k5e4ckdVSnq3KFdlyB5znk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmtnqg%2FbtsJ14QBcQI%2Fk5e4ckdVSnq3KFdlyB5znk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;938&quot; height=&quot;804&quot; data-filename=&quot;오라클계정생성.png&quot; data-origin-width=&quot;938&quot; data-origin-height=&quot;804&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저 생성 및 권한 부여가 완료되었으면, SQL Developer를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; 방금 생성한 계정으로 접속을 시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VMware에서 작성한 계정명, 비밀번호를 사용자 이름, 비밀번호 란에 기입해주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 버튼을 눌러서 정상적으로 연결이 되는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; &quot;상태 : 접속 성공&quot; 이 뜬다면 정상적으로 생성 및 연결이 된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;sqldeveloper접속.png&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boTANB/btsJ04DZjkR/olwnnpLZsy4m3B1j9KszWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boTANB/btsJ04DZjkR/olwnnpLZsy4m3B1j9KszWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boTANB/btsJ04DZjkR/olwnnpLZsy4m3B1j9KszWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboTANB%2FbtsJ04DZjkR%2FolwnnpLZsy4m3B1j9KszWk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1009&quot; height=&quot;707&quot; data-filename=&quot;sqldeveloper접속.png&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;707&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 5명의 팀원과 함께 프로젝트를 진행하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원들 모두 열정이 넘치고 다 같이 으쌰으쌰 하는 분위기라 결과물도 그 만큼 잘 나올 것 같아 기분이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저번 프로젝트에서 아쉬웠던 부분을 최대한 보완하여 더 발전된 결과물을 만들어 보고 싶다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 03.07. 17:11';
&lt;/script&gt;</description>
      <category>Project</category>
      <category>Linux</category>
      <category>oracle</category>
      <category>Spring</category>
      <category>sqldeveloper</category>
      <category>VMware</category>
      <category>계정생성</category>
      <category>접속만들기</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/55</guid>
      <comments>https://cases.tistory.com/entry/Project-Spring-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91#entry55comment</comments>
      <pubDate>Thu, 10 Oct 2024 17:57:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] WebSocket - 실시간 채팅</title>
      <link>https://cases.tistory.com/entry/Spring-WebSocket-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번 메모장 기능 구현에 이어 이번에는 실시간 채팅을 한 번 만들어 볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실전에 들어가기 앞서, 이번 채팅 구현에 있어서는 stomp에 대해서 알고 이 녀석을 사용해 줄거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;STOMP&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;STOMP란, Simple Text Oriented Messaging Protocol의 약자이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;간단한 메세지를 전송하기 위한 프로토콜로 메세지 브로커를 publisher - subscriber 방식을 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메세지의 발행자와 구독자가 존재하고 메세지를 보내는 사람과 받는 사람이 구분되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메세지 브로커는 발행자가 보낸 메세지를 구독자에게 전달해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STOMP는 HTTP와 비슷하게frame 기반 프로토콜 command, header, body로 이루어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;사전 설정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;servlet-context.xml &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;prefix 지정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;(web socket message broker prefix / web socket simple broker prefix)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728548336997&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory --&amp;gt;
	&amp;lt;beans:bean class=&quot;org.springframework.web.servlet.view.InternalResourceViewResolver&quot;&amp;gt;
		&amp;lt;beans:property name=&quot;prefix&quot; value=&quot;/WEB-INF/views/&quot; /&amp;gt;
		&amp;lt;beans:property name=&quot;suffix&quot; value=&quot;.jsp&quot; /&amp;gt;
	&amp;lt;/beans:bean&amp;gt;
	
	&amp;lt;view-controller path=&quot;/&quot; view-name=&quot;home&quot; /&amp;gt;
	
	&amp;lt;context:component-scan base-package=&quot;com.itbank.controller&quot; /&amp;gt;
	
	&amp;lt;websocket:message-broker application-destination-prefix=&quot;/app&quot;&amp;gt;
		&amp;lt;websocket:stomp-endpoint path=&quot;/endpoint&quot;&amp;gt;
			&amp;lt;websocket:sockjs websocket-enabled=&quot;true&quot; /&amp;gt;
		&amp;lt;/websocket:stomp-endpoint&amp;gt;
		&amp;lt;websocket:simple-broker prefix=&quot;/broker&quot; /&amp;gt;
	&amp;lt;/websocket:message-broker&amp;gt;
	
&amp;lt;/beans:beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;root-context.xml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링빈 등록을 위한 scan 지정&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728548378790&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;beans xmlns=&quot;http://www.springframework.org/schema/beans&quot;
	xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
	xmlns:context=&quot;http://www.springframework.org/schema/context&quot;
	xsi:schemaLocation=&quot;http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd&quot;&amp;gt;
	
	&amp;lt;!-- Root Context: defines shared resources visible to all other web components --&amp;gt;
		
		
	&amp;lt;context:component-scan base-package=&quot;com.itbank.repository&quot; /&amp;gt;
	
&amp;lt;/beans&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pom.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728548399405&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;!-- 스프링에서 웹소켓을 처리할 수 있도록 하는 라이브러리 --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-websocket&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;${org.springframework-version}&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
		
		&amp;lt;!-- 스프링에서 STOMP 처리를 위한 라이브러리 --&amp;gt;
		&amp;lt;dependency&amp;gt;
			&amp;lt;groupId&amp;gt;org.springframework.integration&amp;lt;/groupId&amp;gt;
			&amp;lt;artifactId&amp;gt;spring-integration-stomp&amp;lt;/artifactId&amp;gt;
			&amp;lt;version&amp;gt;5.4.13&amp;lt;/version&amp;gt;
		&amp;lt;/dependency&amp;gt;
				        
	&amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ChatController&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방 목록 보여주기, 개설된 채팅방으로 입장하기&lt;/p&gt;
&lt;pre id=&quot;code_1728548442910&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.itbank.model.RoomDTO;
import com.itbank.repository.ChatRoomRepository;

@Controller
@RequestMapping(&quot;/chat&quot;)
public class ChatController {
	
	@Autowired
	private ChatRoomRepository repository;

	@GetMapping(&quot;/rooms&quot;)
	public ModelAndView rooms(String username, HttpSession session) {
		ModelAndView mav = new ModelAndView();
		if(username != null) {
			session.setAttribute(&quot;username&quot;, username);
			session.setMaxInactiveInterval(600);
		}
		List&amp;lt;RoomDTO&amp;gt; list = repository.findAllRooms();
		System.out.println(&quot;=== 현재 개설된 방 목록 ===&quot;);
		list.forEach(System.out::println);
		System.out.println(&quot;========================\n&quot;);
		mav.addObject(&quot;list&quot;, list);
		return mav;
	}
	
	@PostMapping(&quot;/rooms&quot;)
	public String create(String name, RedirectAttributes rttr) {
		RoomDTO room = repository.createChatRoom(name);
		rttr.addFlashAttribute(&quot;roomName&quot;, room.getName());
		return &quot;redirect:/chat/rooms&quot;;	// -&amp;gt; @GetMapping(&quot;/rooms&quot;)
	}
	
	@GetMapping(&quot;/room&quot;)
	public ModelAndView getRoom(String roomId) {
		ModelAndView mav = new ModelAndView();
		mav.addObject(&quot;room&quot;, repository.findRoomById(roomId));
		return mav;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ChatRepository&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728548471765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Repository;

import com.itbank.model.RoomDTO;

@Repository
public class ChatRoomRepository {

	private Map&amp;lt;String, RoomDTO&amp;gt; roomMap = new LinkedHashMap&amp;lt;&amp;gt;();
	
	public List&amp;lt;RoomDTO&amp;gt; findAllRooms() {			// 모든 방의 객체를 리스트로 반환
		List&amp;lt;RoomDTO&amp;gt; result = new ArrayList&amp;lt;&amp;gt;(roomMap.values());	// Map의 values만 추출
		Collections.reverse(result);			// 순서 뒤집기(최신방먼저)
		return result;
	}
	
	public RoomDTO findRoomById(String id) {		// 저장된 방은 각각 고유 id가 있다
		return roomMap.get(id);				// id를 key로 사용하여 방을 찾아 반환
	}
	
	public RoomDTO createChatRoom(String name) {	// 방 생성, 이름을 전달받는다
		RoomDTO room = RoomDTO.create(name);		// 이름을 전달하여 방 객체 생성
		roomMap.put(room.getRoomId(), room);		// 방의 id를 key로 지정하여 Map에 저장
		return room;					// 생성한 방을 반환
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;StompController&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채팅방 안에서 이루어지는 대화&lt;/p&gt;
&lt;pre id=&quot;code_1728548500408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import com.itbank.model.MessageDTO;

@Controller
public class StompController {

	@MessageMapping(&quot;/enter/{roomId}&quot;)	// 들어오는 주소
	@SendTo(&quot;/broker/room/{roomId}&quot;)	// 브로커에게 보내는 주소 (브로커가 다시 클라이언트에게 보낸다)
	public MessageDTO enter(MessageDTO message) {
		message.setText(message.getFrom() + &quot;님이 채팅방에 참여하였습니다&quot;);
		message.setFrom(&quot;service&quot;);
		return message;
	}
	
	@MessageMapping(&quot;/message/{roomId}&quot;)
	@SendTo(&quot;/broker/room/{roomId}&quot;)
	public MessageDTO message(MessageDTO message) {
		return message;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RoomDTO&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자를 이용하여 UUID로 방 번호를 가지게 구현&lt;/p&gt;
&lt;pre id=&quot;code_1728548542221&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import org.springframework.web.socket.WebSocketSession;

// 채팅방
public class RoomDTO {

	private String roomId;
	private String name;
	private Set&amp;lt;WebSocketSession&amp;gt; sessions = new HashSet&amp;lt;&amp;gt;();
	// 웹소켓세션을 저장, 중복을 허용하지 않는다. for문으로 순회 가능하다
	
	// 자바 빈즈 DTO는 기본생성자만 가지는 편이 좋다
	public static RoomDTO create(String name) {
		RoomDTO room = new RoomDTO();
		room.roomId = UUID.randomUUID().toString().substring(0, 8);
		room.name = name;
		return room;
	}
	
	@Override
	public String toString() {
		String form = &quot;%s] %s\n%s&quot;;
		return String.format(form, roomId, name, sessions);
	}
	public String getRoomId() {
		return roomId;
	}
	public void setRoomId(String roomId) {
		this.roomId = roomId;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Set&amp;lt;WebSocketSession&amp;gt; getSessions() {
		return sessions;
	}
	public void setSessions(Set&amp;lt;WebSocketSession&amp;gt; sessions) {
		this.sessions = sessions;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;MessageDTO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728548572359&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MessageDTO {

	private String roomId;
	private String from;
	private String text;
	private String time;
	
	public String getRoomId() {
		return roomId;
	}
	public void setRoomId(String roomId) {
		this.roomId = roomId;
	}
	public String getFrom() {
		return from;
	}
	public void setFrom(String from) {
		this.from = from;
	}
	public String getText() {
		return text;
	}
	public void setText(String text) {
		this.text = text;
	}
	public String getTime() {
		return time;
	}
	public void setTime(String time) {
		this.time = time;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;&lt;b&gt;사용자에게 보여지는 부분&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;home.jsp&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;사용자의 이름을 입력하고, 채팅방 목록에 입장.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728548638589&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;ws02 - STOMP를 활용한 웹소켓 채팅&amp;lt;/title&amp;gt;
&amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;ws02 - STOMP를 활용한 웹소켓 채팅&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;div id=&quot;root&quot;&amp;gt;
	&amp;lt;form action=&quot;${cpath }/chat/rooms&quot;&amp;gt;
		&amp;lt;input name=&quot;username&quot; required autofocus&amp;gt;
		&amp;lt;input type=&quot;submit&quot; value=&quot;입장&quot;&amp;gt;
	&amp;lt;/form&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;rooms.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개설된 방 목록을 보여주는 페이지.&lt;/p&gt;
&lt;pre id=&quot;code_1728548680927&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;방 목록&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;&amp;lt;a href=&quot;${cpath }&quot;&amp;gt;rooms.jsp - ${username }&amp;lt;/a&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;fieldset&amp;gt;
	&amp;lt;form action=&quot;${cpath }/chat/rooms&quot; method=&quot;POST&quot;&amp;gt;
		&amp;lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;방제&quot; autocomplete=&quot;off&quot; autofocus required&amp;gt;
		&amp;lt;input type=&quot;submit&quot; value=&quot;채팅방 개설&quot;&amp;gt;
	&amp;lt;/form&amp;gt;
&amp;lt;/fieldset&amp;gt;

&amp;lt;ul&amp;gt;
	&amp;lt;c:forEach var=&quot;room&quot; items=&quot;${list }&quot;&amp;gt;
		&amp;lt;li&amp;gt;&amp;lt;a href=&quot;${cpath }/chat/room?roomId=${room.roomId}&quot;&amp;gt;${room.name }&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
	&amp;lt;/c:forEach&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;room.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 방을 보여주는 페이지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sockJS는 js라이브러리이며, stomp 위에서 돌아가고 있다고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(stomp : 서브 프로토콜)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json.parse : json을 객체로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json.stringify : 객체를 json으로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json.stringify({roomId : roomId, from : username})&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; 중괄호 안에 있는 것이 {객체}&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728548830079&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;room.jsp - ${room.name }&amp;lt;/title&amp;gt;
&amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
	// 자바스크립트 함수 정의
	function onReceive(chat) {					// 메시지를 받으면
		const content = JSON.parse(chat.body)	// JSON을 객체로 변환하고
		const from = content.from				// 누구에게서 온 메시지인지
		const text = content.text				// 어떤 내용인지
		let str = ''
		str += '&amp;lt;div class=&quot;' + (from == 'service' ? 'service' : from == username ? 'right' : 'left') + '&quot;&amp;gt;'
		str += '&amp;lt;div&amp;gt;'
		str += '&amp;lt;b&amp;gt;' + (from != 'service' ? from + ': ' : '') + text + '&amp;lt;/b&amp;gt;'
		str += '&amp;lt;br&amp;gt;&amp;lt;sub&amp;gt;' + content.time + '&amp;lt;/sub&amp;gt;'
		str += '&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;'
		messageArea.innerHTML += str			// 태그로 구성하여 화면에 반영
		messageArea.scrollTop = messageArea.scrollHeight	// 스크롤 이동시키기
	}
	
	function onConnect() {
		console.log('STOMP Connection')
		stomp.subscribe('/broker/room/' + roomId, onReceive)	// 구독할 채널, 메시지 받으면 실행할 함수
		stomp.send('/app/enter/' + roomId, {}, JSON.stringify({	// 서버에게 입장 메시지와 시간을 보낸다
			roomId: roomId,
			from: username,
			//time: getCurrentHHmm(),
		}))
		document.querySelector('input[name=&quot;msg&quot;]').focus()
	}
	
	function onInput() {	// 클라이언트가 메시지를 입력할 때
		const text = document.querySelector('input[name=&quot;msg&quot;]').value	// 내용을 불러와서
		if(text == '') {												// 내용이 없으면 중단
			return
		}
		document.querySelector('input[name=&quot;msg&quot;]').value = ''			// 입력창을 비워준다
		
		stomp.send('/app/message/' + roomId, {}, JSON.stringify({		
			roomId: roomId,			// 방번호, 사용자, 내용을 JSON으로 보낸다
			from: username,
			text: text,
			//time: getCurrentHHmm()
		}))
		document.querySelector('input[name=&quot;msg&quot;]').focus()	// 다시 입력할 수 있도록 포커스를 잡아준다
	}
	
	// JSP에서 자바스크립트로 넘기는 변수
	const roomName = '${room.name}'
	const roomId = '${room.roomId}'
	const username = '${username}'
	const cpath = '${cpath}'
	
	
&amp;lt;/script&amp;gt;
&amp;lt;style&amp;gt;
	#messageArea {
		border: 2px solid black;
		width: 700px;
		height: 250px;
		margin: 20px 0;
		word-wrap: break-word;
		overflow-y: scroll;
		scroll-behavior: smooth;
	}
	#messageArea &amp;gt; div &amp;gt; div {
		margin: 10px;
		padding: 10px 20px;
		border: 0.5px solid black;
		border-radius: 20px;
		width: fit-content;
		box-shadow: 2px 2px 2px grey;
	}
	.service {
		display: flex;
		justify-content: center;
	}
	.service &amp;gt; div {
		background-color: #f5f6f7;
	}
	.left {
		display: flex;
		justify-content: flex-start;
	}
	.right {
		display: flex;
		justify-content: flex-end;
	}
	.right &amp;gt; div {
		background-color: yellow;
	}
	.service sub {
		clear: both;
		display: none;
	}
	sub {
		color: grey;
	}
	.left sub {
		float: left;
	}
	.right sub {
		float: right;
	}
&amp;lt;/style&amp;gt;

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

&amp;lt;h1&amp;gt;&amp;lt;a href=&quot;${cpath }&quot;&amp;gt;room.jsp - ${room.name }&amp;lt;/a&amp;gt;&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;div id=&quot;messageArea&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div id=&quot;input&quot;&amp;gt;
	&amp;lt;input type=&quot;text&quot; name=&quot;msg&quot; id=&quot;msg&quot; placeholder=&quot;내용을 입력하세요&quot;&amp;gt;
	&amp;lt;input type=&quot;button&quot; value=&quot;send&quot;&amp;gt;
	&amp;lt;a id=&quot;disconnect&quot; href=&quot;${cpath }/chat/rooms&quot;&amp;gt;&amp;lt;button&amp;gt;나가기&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
	if(roomId == '') {
		location.href = cpath
	}
	
	const messageArea = document.getElementById('messageArea')
	const sockJS = new SockJS(cpath + '/endpoint')
	const stomp = Stomp.over(sockJS)
	
	const sendBtn = document.querySelector('input[value=&quot;send&quot;]')
	const msgInput = document.querySelector('input[name=&quot;msg&quot;]')
	const leaveLink = document.getElementById('disconnect')
	
	stomp.connect({}, onConnect)
	
	// leaveLink.onclick = onDisconnect
	sendBtn.onclick = onInput
	msgInput.onkeyup = function(e) {
		if(e.key == 'Enter') onInput()
	}
&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분이 그렇겠지만 처음부터 코드 하나 하나를 뜯어보기 보다는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 전체적인 흐름을 이해하고 들어가는게 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방이 어떤식으로 개설 되었고, 그 방을 어떻게 구독하고... 이러한 흐름을 잡고 들어가면 훨씬 이해하기가 쉽다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 03.04. 19:17';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>SockJS</category>
      <category>Spring</category>
      <category>STOMP</category>
      <category>websocket</category>
      <category>실시간채팅</category>
      <category>프로토콜</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/54</guid>
      <comments>https://cases.tistory.com/entry/Spring-WebSocket-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%B1%84%ED%8C%85#entry54comment</comments>
      <pubDate>Thu, 10 Oct 2024 17:30:11 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] WebSocket을 활용한 메모장 만들기</title>
      <link>https://cases.tistory.com/entry/Spring-WebSocket%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A9%94%EB%AA%A8%EC%9E%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 웹소켓을 활용하여 메모장 기능을 한 번 구현해볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;Ajax와 WebSocket의 차이&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;연결의 지속성에 따라 둘을 구분한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;클라이언트와 서버가 통신할 때 HTTP통신을 주로 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;HTTP통신은 다음과 같은 특징이 있다.&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;1. 비연결성 (connectionless) : 연결을 맺고 요청을 하고 응답을 받으면 연결을 끊어버린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;2. 무상태성 (stateless) : 서버가 클라이언트의 상태를 가지고 있지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;3. 단방향 통신이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;이러한 HTTP 통신의 경우 채팅과 같은 실시간 통신에 적합하지 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;물론 HTTP 통신으로 실시간 통신을 흉내낼 수는 있으나 완벽하지는 않다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;실시간 통신이 필요할 때 사용하는 통신을 소켓 통신이라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;HTTP통신과 다르게 연결을 맺고 바로 끊어버리는게 아니라 계속 유지를 하기 때문에 실시간 통신에 적합하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;Ajax&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;JavaScript를 이용해 서버와 브라우저가 비동기 방식으로 데이터를 교환할 수 있는 통신 기능&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터를 주고 받는 형식.&lt;/li&gt;
&lt;li&gt;요청과 응답이 존재한다.&lt;/li&gt;
&lt;li&gt;HTTP를 이용한 요청과 응답.&lt;/li&gt;
&lt;li&gt;클라이언트의 요청이 없으면 서버의 응답도 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;WebSocket&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사용자의 브라우저와 서버 사이의 인터액티브 통신 세션을 설정할 수 있게 하는 고급 기술&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클라이언트의 요청이 없어도 서버의 응답이 존재할 수 있다.&lt;/li&gt;
&lt;li&gt;웹소켓 연결을 끊어버리기 전까지는 자유롭게 요청, 응답이 오고 갈 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;home.jsp&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728547540209&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;

&amp;lt;script src=&quot;https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;

&amp;lt;style&amp;gt;
	#notepad {
		box-sizing: border-box;
		width: 700px;
		height: 400px;
		padding: 10px;
		margin: 10px 0;
		border: 2px solid black;
		font-size: 17px;
		overflow-y: auto;
	}
	form &amp;gt; p {
		box-sizing: border-box;
		width: 700px;
		display: flex;
		justify-content: space-between;
	}
	input[name=&quot;input&quot;] {
		padding: 5px;
		font-size: 20px;
		flex: 5;
		margin-right: 5px;
	}
	input[type=&quot;submit&quot;] {
		flex: 1;
	}
	
&amp;lt;/style&amp;gt;

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

&amp;lt;h1&amp;gt;websocket 을 이용한 메모장 (sockJS)&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;


&amp;lt;div id=&quot;notepad&quot;&amp;gt;
&amp;lt;/div&amp;gt;

	&amp;lt;form&amp;gt;
		&amp;lt;p&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;input&quot; placeholder=&quot;글을 입력&quot;&amp;gt;
			&amp;lt;input type=&quot;submit&quot;&amp;gt;
		&amp;lt;/p&amp;gt;
			
	&amp;lt;/form&amp;gt;

&amp;lt;script&amp;gt;
	//	변수선언
	const cpath = '${cpath}'
	const form = document.forms[0]
	const ws = new SockJS(cpath + '/chat')
	const notepad = document.getElementById('notepad')
	
	
	//	함수정의
	function messageHandler(event) {
		notepad.innerHTML = '&amp;lt;p&amp;gt;' + event.data + '&amp;lt;/p&amp;gt;'
		notepad.scroll({
			top : notepad.scrollHeight,
			behavior : 'smooth',
		})	
	}
	
	function submitHandler(event) {
		event.preventDefault()
		const input = event.target.querySelector('input[name=&quot;input&quot;]')
		
		
		ws.send(input.value)
		input.value = ''
		input.focus()
	}
	
	//	이벤트 연결
	ws.onmessage = messageHandler
	ws.onopen = function(msg) {}
	ws.onclose = function(msg) {}
	ws.onerror = function(msg) {}
	form.onsubmit = submitHandler
	
&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ChatComponent&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728547571560&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class ChatComponent extends TextWebSocketHandler {

	
	List&amp;lt;WebSocketSession&amp;gt; sessionList = new ArrayList&amp;lt;&amp;gt;();
	

	//	저장 - source - Override/Implements method
	
	@Override	//	연결이 성립된 이후 실행되는 함수
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		System.out.println(&quot;연결 생성 확인 : &quot; + session);
		sessionList.add(session);
	}

	
	@Override	//	텍스트 메시지를 전달받았을 때 실행되는 함수 
	protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
		System.out.printf(&quot;메시지 받음 : [%s] : %s\n&quot;, session, message);
		
		for(WebSocketSession ws : sessionList) {	//	세션리스트에 들어가있는 모든 웹 소켓에게
			ws.sendMessage(message);		//	받은 메시지를 다시 보낸다 (== 클라이언트가 응답을 받았을 것이다)
		}
	}
	
	
	@Override	//	연결이 끊어졌을 때 실행되는 함수 
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		System.out.println(&quot;연결 종료 : &quot; + session);
		sessionList.remove(session);
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;접속 이동 경로&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 작성 없이 먼저 연결부터 확인해봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;web.xml 에서 version은 3.0 이상이어야 한다. &amp;gt; WebSocket 지원이 3.0 이상 부터 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. home.jsp에서 script 태그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;const ws = new SockJS(cpath + '/chat')&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. ChatComponent 에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;afterConnectionEstablished가 작동하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 콘솔창에서 연결 확인 메시지가 뜬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;웹소켓연결콘솔.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;124&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K0ShK/btsJ1e0yIQU/qCTBerTaU8jCOQrBgKPQ4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K0ShK/btsJ1e0yIQU/qCTBerTaU8jCOQrBgKPQ4k/img.png&quot; data-alt=&quot;콘솔로 연결 여부를 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K0ShK/btsJ1e0yIQU/qCTBerTaU8jCOQrBgKPQ4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK0ShK%2FbtsJ1e0yIQU%2FqCTBerTaU8jCOQrBgKPQ4k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;124&quot; data-filename=&quot;웹소켓연결콘솔.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;124&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;콘솔로 연결 여부를 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 빠릿 빠릿 하게 통신이 되는걸 보니까 너무 재밌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 번에는 이 웹소켓을 활용하여 채팅을 한 번 구현해 볼 생각이다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 03.02. 21:04';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>Ajax</category>
      <category>JavaScript</category>
      <category>Spring</category>
      <category>websocket</category>
      <category>웹소켓</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/53</guid>
      <comments>https://cases.tistory.com/entry/Spring-WebSocket%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A9%94%EB%AA%A8%EC%9E%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0#entry53comment</comments>
      <pubDate>Thu, 10 Oct 2024 17:13:01 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] AJAX</title>
      <link>https://cases.tistory.com/entry/JavaScript-AJAX</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;다른 웹서버의 데이터를 이용해서 원하는 형식으로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 한 번 출력해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;script 코드드 vscode로 작성할 거다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;이미지 출력&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728546495513&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
        #root {
            max-width: 1200px;
            display: flex;
            flex-flow: wrap;
            margin: 20px auto;
        }
        .item {
            width: 45%;
            display: flex;
            border: 1px solid black;
            margin: 10px;
            padding: 10px;
        }
        .item img {
            margin-right: 10px;
        }

    &amp;lt;/style&amp;gt;

  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;AJAX 화면 구현&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;

    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
      //  fetch(콜백형태, 함수 내부에)

      const url = &quot;https://jsonplaceholder.typicode.com/photos&quot;;
      
      
      fetch(url)     //  주소로 요청한다
        .then(resp =&amp;gt; resp.json())     //  요청에 따른 응답이 오면, 응답을 json 으로 변환   
        .then(json =&amp;gt; {                //  json 을 이용하여 다음 내용을 수행
  
          console.log(json);

          const arr = json.slice(0, 50).map(e =&amp;gt; {
            delete e.url
            return e
          })

          console.log(arr)

          const root = document.getElementById('root')

          arr.forEach(dto =&amp;gt; {
            let tag = ''
            tag += `&amp;lt;div class=&quot;item&quot;&amp;gt;`
            tag += `    &amp;lt;div class=&quot;thumbnailUrl&quot;&amp;gt;&amp;lt;img src=&quot;${dto.thumbnailUrl}&quot;&amp;gt;&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div&amp;gt;`
            tag += `        &amp;lt;div class=&quot;title&quot;&amp;gt;${dto.title}&amp;lt;/div&amp;gt;`
            tag += `        &amp;lt;div class=&quot;id&quot;&amp;gt;${dto.id}&amp;lt;/div&amp;gt;`
            tag += `        &amp;lt;div class=&quot;albumId&amp;gt;${dto.albumId}&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;/div&amp;gt;`
            tag += `&amp;lt;/div&amp;gt;`

            root.innerHTML += tag
          })

          //    fetch.then() 함수의 콜백에는 반환을 수행하지 않는다
          //    만약, async / await 를 사용하면 반환을 수행할 수 있다 

        });

    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;ajax이미지.PNG&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkhFhy/btsJ1gRy8nk/rbFm4w0dHzPsrwcuqYeuz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkhFhy/btsJ1gRy8nk/rbFm4w0dHzPsrwcuqYeuz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkhFhy/btsJ1gRy8nk/rbFm4w0dHzPsrwcuqYeuz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkhFhy%2FbtsJ1gRy8nk%2FrbFm4w0dHzPsrwcuqYeuz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;948&quot; height=&quot;469&quot; data-filename=&quot;ajax이미지.PNG&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 위와 같이 나올 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;style에는 .item에 width를 사용하여, 한 줄ㅇ 나오는 요소의 개수를 정해 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전체 목록 출력 (페이징)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728546658241&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
        a {
            color: inherit;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
        table {
            border-collapse: collapse;
            margin: auto;
        }
        thead {
            background-color: grey;
            color: white;
        }
        tr {
            border-bottom: 1px solid grey;
        }
        td,th {
            padding: 5px 10px;
        }


    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    
    &amp;lt;h1&amp;gt;posts&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;


    &amp;lt;div id=&quot;root&quot;&amp;gt;
        &amp;lt;table&amp;gt;
            &amp;lt;thead&amp;gt;
                &amp;lt;tr&amp;gt;
                    &amp;lt;th&amp;gt;번호&amp;lt;/th&amp;gt;
                    &amp;lt;th&amp;gt;제목&amp;lt;/th&amp;gt;
                &amp;lt;/tr&amp;gt;
            &amp;lt;/thead&amp;gt;
            &amp;lt;tbody&amp;gt;&amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
    &amp;lt;/div&amp;gt;


    &amp;lt;!--
        HTTP 요청 메서드
        
        GET           : select     요청할때 메서드를 지정하지 않으면 기본값은 get       
        POST          : insert
        PUT/PATCH     : update 
        DELETE        : delete

    --&amp;gt;


    &amp;lt;script&amp;gt;
    	 //  다른 서버에있는 데이터를 가져오기 위해 주소를 넣음 
        const url = 'https://jsonplaceholder.typicode.com/posts'   

        fetch(url)
            .then(resp =&amp;gt; resp.json()) 
            .then(json =&amp;gt; {

                //  console.log(json)   항상 fetch 하고 나서 출력해보기!! 
                //  (내가 가져온 데이터가 무엇인지 알아야 다듬을 수 있음 )
                //  tbody 에 innerHTML 넣어주는 것이 정석임
                const tbody = document.querySelector('#root &amp;gt; table &amp;gt; tbody')  
                const arr = json.toSorted((a, b) =&amp;gt; b.id - a.id)    //  정수반환으로 정렬하기 
                                        .slice(0, 20)               //  slice 로 페이징 
                
                arr.forEach(dto =&amp;gt; {
                        let tag = ''
                        tag += `&amp;lt;tr&amp;gt;`
                        tag += `    &amp;lt;td&amp;gt;${dto.id}&amp;lt;/td&amp;gt;`
                        
                        //	12_post.html?id=${dto.id} : 이때 id 를 같이 넘긴다
                        tag += `    &amp;lt;td&amp;gt;&amp;lt;a href=&quot;12_post.html?id=${dto.id}&quot;&amp;gt;${dto.title}&amp;lt;/a&amp;gt;&amp;lt;/td&amp;gt;`
                        tag += `&amp;lt;/tr&amp;gt;`
                        
                        tbody.innerHTML += tag
                })
            })

    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;전체목록출력.PNG&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blm7FZ/btsJ1ZPlORj/LUcPMYBL4wUeDOyFafVN21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blm7FZ/btsJ1ZPlORj/LUcPMYBL4wUeDOyFafVN21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blm7FZ/btsJ1ZPlORj/LUcPMYBL4wUeDOyFafVN21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblm7FZ%2FbtsJ1ZPlORj%2FLUcPMYBL4wUeDOyFafVN21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;959&quot; height=&quot;468&quot; data-filename=&quot;전체목록출력.PNG&quot; data-origin-width=&quot;959&quot; data-origin-height=&quot;468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;개별 출력&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 목록 중에서 하나를 선택하면 (a태그를 이용하여 페이지 이동)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때, id를 받아와서 개별로 출력한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728546785409&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
    table {
        border: 2px solid black;
        border-collapse: collapse;
        margin: 10px 0;
    }
    td, th {
        padding: 5px 10px;
    }
    tr {
        border-bottom: 1px solid grey;
    }

&amp;lt;/style&amp;gt;

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

    &amp;lt;h1&amp;gt;post.html&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;


    &amp;lt;table&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td class=&quot;id&quot;&amp;gt;&amp;lt;/td&amp;gt;
            &amp;lt;td class=&quot;title&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
            &amp;lt;td class=&quot;userId&quot; colspan=&quot;2&quot;&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
        &amp;lt;tr&amp;gt;
           &amp;lt;td colspan=&quot;2&quot;&amp;gt;&amp;lt;pre class=&quot;body&quot;&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/td&amp;gt;
        &amp;lt;/tr&amp;gt;
    &amp;lt;/table&amp;gt;

    &amp;lt;div style=&quot;display: flex; justify-content: space-between;&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;a href=&quot;11_post.html&quot;&amp;gt;&amp;lt;button&amp;gt;목록&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;


    &amp;lt;script&amp;gt;
        //   자바스크립트에서 쿼리스트링 가져오기
        const id = new URLSearchParams(location.search).get('id')       //  위에서 생성한 id 를 
        const url = `https://jsonplaceholder.typicode.com/posts/${id}`  //  쿼리스트링 자리에 넣기 
        //  console.log(url)


        //  async / await 를 활용하여 json 을 반환값으로 받기
        //  비동기 함수를 정의할때 async        
        //  비동기 함수를 호출할때 await   :   주소로 요청을 하고 응답을 받으면 그 응답을 json 객체로 바꿈 (콘솔로그가 안뜬다)
        //  단, await 호출은    javascript 코드의 top-level 에서 호출할 수 없음(그래서, loadHandler() 를 만든 이유) 
        async function loadHandler() {
            const json = await fetch(url)
                        .then(resp =&amp;gt; resp.json())
            console.log(json)


        //  json 객체의 변수(필드) 이름과 
        //  HTML element 의 클래스 이름을 맞춰두고 하나씩 대입
            for(let key in json) {      //  key 는 index 역할, index에 접근하려면 for 문의 in 을 사용 
                console.log(key)
                const value = json[key]
                const element = document.querySelector('.' + key)       //  앞에 . 붙이면 class로 불러옴 
                element.innerText = value

            }


        //  불러온 json 객체에서 userId 를 불러와서 username을 작성자 위치에 덮어쓰기
            const username = await fetch('https://jsonplaceholder.typicode.com/users/' + json.userId)
                        .then(resp =&amp;gt; resp.json())
                        .then(json =&amp;gt; json.username)
        
            document.querySelector('.userId').innerText = username
        }
        window.onload = loadHandler
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지는 비동기 함수에 대해서 잘 모르겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수업에서 배운 내용으로 계속 한 번씩 사용해보면서 사용법을 익혀봐야 될 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 02.21. 20:11';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>Ajax</category>
      <category>JavaScript</category>
      <category>비동기</category>
      <category>비동기함수</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/52</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-AJAX#entry52comment</comments>
      <pubDate>Thu, 10 Oct 2024 16:55:36 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] json 화면 구현</title>
      <link>https://cases.tistory.com/entry/JavaScript-json-%ED%99%94%EB%A9%B4-%EA%B5%AC%ED%98%84</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;json 파일을 이용해서 화면 구현을 해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;관련 기초 문법 익히기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;element&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;div를 이용하여 데이터를 미리 구성해둔 다음,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 데이터를 출력하는 방법에 대해 배워보기.&lt;/p&gt;
&lt;pre id=&quot;code_1728493104322&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;element.html&amp;lt;/h1&amp;gt;
    &amp;lt;hr /&amp;gt;

    &amp;lt;div id=&quot;root&quot;&amp;gt;
      &amp;lt;div class=&quot;item&quot;&amp;gt;
        &amp;lt;div class=&quot;name&quot;&amp;gt;이지은&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;age&quot;&amp;gt;31&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;item&quot;&amp;gt;
        &amp;lt;div class=&quot;name&quot;&amp;gt;홍진호&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;age&quot;&amp;gt;42&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
      &amp;lt;div class=&quot;item&quot;&amp;gt;
        &amp;lt;div class=&quot;name&quot;&amp;gt;나단비&amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;age&quot;&amp;gt;5&amp;lt;/div&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
      //  1)  id 를 지정하여 단일 요소를 불러오기
      //      문서상에서 id는 유일해야하기 때문에, 반환형은 단일 요소
      const root = document.getElementById(&quot;root&quot;);

      //  2)  css 선택자를 이용하여 단일 요소를 불러오기
      //  만약, 선택자로 지정한 요소가 여러개라면, 가장 첫번째 요소 하나만 불러온다
      const firstItem = document.querySelector(&quot;.item&quot;);

      //  3)  css 선택자를 이용하여 여러 요소를 한번에 불러오기
      //    이때, 반환형은 NodeList 타입이며, forEach 정도만 호출가능
      //    배열로 형변환을 원한다면 Array.from(nodeList) 를 이용하면 된다
      const itemList = document.querySelectorAll(&quot;#root &amp;gt; .item&quot;);

      console.log(root);
      console.log(firstItem);
      console.log(itemList);

      itemList.forEach((item) =&amp;gt; {
        console.log(item.querySelector(&quot;.name&quot;).innerText);
      });

      const itemArray = Array.from(itemList); //  배열로 바꾼다
      const dataArray = itemArray.map((item) =&amp;gt; {
        //  배열로 map 함수 호출가능
        const ob = {};
        ob.name = item.querySelector(&quot;.name&quot;).innerText;//  innerText : 태그 없이 내용만 가져옴
                                                        //  innerHTML : 사용된 태그까지 다 가져옴 
        ob.age = +item.querySelector(&quot;.age&quot;).innerText; //  문자열 앞에 (+)를 붙이면 숫자로 변경됨(parseInt와 동일)
        return ob;
      });

      console.log(dataArray); //  값만으로 구성한 객체 배열

    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;appendChild&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;appendChild() : 새로운 요소를 추가하는 방법&lt;/p&gt;
&lt;pre id=&quot;code_1728493166221&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;Document&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    
    &amp;lt;h1&amp;gt;6_appendChild.html&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;

    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        //  자바스크립트에서 HTML 요소를 생성해서 문서에 반영할 수 있다
        //  1) document 내장함수 createElement 활용
        const e1 = document.createElement('div')    //  태그이름
        e1.className = 'item'
        e1.innerText = '돈까스'
        root.appendChild(e1)        //  root 에 자식요소로 e1 을 추가 


        //  2) innerHTML 을 이용하여 문자열 형식으로 태그를 작성하여 추가하기
        const e2 = '&amp;lt;div class=&quot;item&quot;&amp;gt;초밥&amp;lt;/div&amp;gt;'
        root.innerHTML += e2

        
        //  3) 백틱 문자열(``)을 이용하여 추가하기
        //  JSP 에서는 EL 문법과 겹치기 때문에 사용할 수 없고,
        //  .html 과 .js 에서는 이용가능함 
        const food = '라멘'
        const e3 = `&amp;lt;div class=&quot;item&quot;&amp;gt;${food}&amp;lt;/div&amp;gt;`
        root.innerHTML += e3
        
        //  appendChild 는 신규요소를 마지막에 추가함       &amp;lt;&amp;gt;  createElement
        //  만약, 추가하는 요소가 이미 문서에서 불러온 요소라면 마지막으로 자리를 옮긴다 
        const item1 = document.querySelector('.item')
        root.appendChild(item1)     //  새로고침하면 원래 있던 요소는 맨 마지막으로 감(재배열할때 사용 가능)

        const tag = document.querySelector('.item').outerHTML   //  시작 태그 ~ 마무리 태그 까지 전부   
        root.innerHTML += tag

        &amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;데이터를 직접 넣고 화면에 출력해보기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728493203667&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;style&amp;gt;
      #root {
        width: 900px;
        margin: auto;
      }
      .item {
        display: flex;
        border: 2px solid black;
        margin: 10px;
        padding: 10px;
      }
      .item &amp;gt; div {
        flex: 1;
      }
      .item &amp;gt; div:nth-child(2) {
        flex: 3;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;7_ JS 화면 구현&amp;lt;/h1&amp;gt;
    &amp;lt;hr /&amp;gt;


    &amp;lt;!-- 
        먼저 출력 형식 틀은 html 로 만들어둔 다음, 
        script 로 데이터를 만들어 놓고 forEach 를 이용하여 원하는 틀대로 데이터 출력시켜보기 
    --&amp;gt;


    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
      //  스프링에서 myBatis 로 select 를 수행한 결과를 List&amp;lt;DTO&amp;gt;로 받았다고 가정

      const arr = [
        {
          idx: 1,
          name: &quot;새벽 겨울 딸기 500G(팩)&quot;,
          salesPrice: 9990,
          grade: 4.6,
        },
        {
          idx: 2,
          name: &quot;알큰 딸기 700G(박스)&quot;,
          salesPrice: 14990,
          grade: 4.5,
        },
        {
          idx: 3,
          name: &quot;살살 녹는 장희 딸기 750G(팩)&quot;,
          salesPrice: 22990,
          grade: 4.1,
        },
        {
          idx: 4,
          name: &quot;슈퍼푸드 블루베리(칠레) 310G(팩)&quot;,
          salesPrice: 9990,
          grade: 4.6,
        },
      ];

      console.log(arr);



      //    arr 에 있던 요소들을    div root 에 넣기
      const root = document.getElementById(&quot;root&quot;);
      arr.forEach((dto) =&amp;gt; {
        let tag = &quot;&quot;;
        tag += `&amp;lt;div class=&quot;item&quot;&amp;gt;`;        //  백틱(``) 을 이용
        tag += `    &amp;lt;div class=&quot;idx&quot;&amp;gt;${dto.idx}&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;name&quot;&amp;gt;${dto.name}&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;salesPrice&quot;&amp;gt;${dto.salesPrice}&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;grade&quot;&amp;gt;${dto.grade}&amp;lt;/div&amp;gt;`;
        tag += `&amp;lt;/div&amp;gt;`;

        root.innerHTML += tag;
      });
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;03&amp;nbsp; &amp;nbsp;다른 사이트에 있는 json 파일 내용을 출력하기&amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li style=&quot;list-style-type: circle;&quot;&gt;미리 데이터를 준비하지 않아도 된다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle;&quot;&gt;데이터를 내가 직접 관리할 필요가 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주의할 점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle; background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li style=&quot;list-style-type: circle;&quot;&gt;해당 파일이 어떤 내용을 가지고 있는지 먼저 파악해야한다 (이름이 일치해야 출력할 수 있기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1728493241695&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
      #root {
        width: 900px;
        margin: auto;
      }
      .item {
        display: flex;
        border: 2px solid grey;
        margin: 20px;
        padding: 20px;
        background-color: ivory;
     
      }
      .item &amp;gt; div {
        flex: 1;
      }
      .item:hover {
        background-color: rgb(123, 155, 181);
        border: 2px solid black;
        color: rgb(255, 255, 255);
        font-weight: bold;
        
      }
      .item &amp;gt; div:nth-child(2) {
        flex: 1;
      }
    &amp;lt;/style&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;JSON 파일 내용 불러와서 화면에 출력하기&amp;lt;/h1&amp;gt;
    &amp;lt;hr /&amp;gt;

    &amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

    &amp;lt;script src=&quot;homeplus(berry).js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
      console.log(arr);

      const root = document.getElementById(&quot;root&quot;);


      //    원하는 조건으로 정렬한 이후에 json 데이터로 반영할 수 있다 
      //    가격순 또는 상품명순 으로 정렬 가능 
      //    이러한 기능들은 특정 기능을 함수로 만들어두는 것이 좋음 
      arr.sort((a, b) =&amp;gt; a.price - b.price)

      //    json 데이터를 화면에 반영하는 코드
      arr.forEach((dto) =&amp;gt; {
        let tag = &quot;&quot;;

        tag += `&amp;lt;div class=&quot;item&quot;&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;name&quot;&amp;gt;${dto.name}&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;price&quot;&amp;gt;${dto.price}원&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;salePrice&quot;&amp;gt;${dto.salePrice}원&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;salesCount&quot;&amp;gt;${dto.salesCount}&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;grade&quot;&amp;gt;${dto.grade}점&amp;lt;/div&amp;gt;`;
        tag += `    &amp;lt;div class=&quot;reviewCount&quot;&amp;gt;${dto.reviewCount}&amp;lt;/div&amp;gt;`;
        tag += `&amp;lt;/div&amp;gt;`;

        root.innerHTML += tag;
      });
    &amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;미리 데이터를 직접 준비하지 않아도 된다는 점이 너무 편하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그러나, 여태껏 DB에 있는 데이터를 불러와서 출력하는 경우가 많았기 때문에,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;dto.컬럼이름으로 값을 불러오기만 하면 됐었는데&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다른 사이트에 있는 데이터의 속성에는 어떤 것이 있는지 미리 파악하는 것이 좋은 방법이라 생각한다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 02.15. 19:28';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>JavaScript</category>
      <category>JSON</category>
      <category>화면구현</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/51</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-json-%ED%99%94%EB%A9%B4-%EA%B5%AC%ED%98%84#entry51comment</comments>
      <pubDate>Thu, 10 Oct 2024 02:12:09 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] HashMap json mapping</title>
      <link>https://cases.tistory.com/entry/Spring-HashMap-json-mapping</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;공공데이터 포털에 가면 오픈 되어 있는 여러가지 공공데이터들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 그 오픈 json데이터를 HashMap으로 mapping 해볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;home.jsp&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728491604789&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;day09&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;h3&amp;gt;JSON : JavaScript Object Notation&amp;lt;/h3&amp;gt;
&amp;lt;h3&amp;gt;자바스크립트에서 객체를 표현하는데 사용하는 문법&amp;lt;/h3&amp;gt;

&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;ex01&quot;&amp;gt;ex01 - 부산 축제 정보 서비스 연습&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;ex02&quot;&amp;gt;ex02 - 부산 축제 정보 서비스 (AJAX)&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;ex01.jsp&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;json 파일을 자바 객체로 변환하여 출력하기&lt;/p&gt;
&lt;pre id=&quot;code_1728491639939&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;ex01.jsp&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
	#root {
		width: 900px;
		margin: 20px auto;
	}
	.item {
		width: 800px;
		margin: 10px auto;
		border: 1px solid grey;
		padding: 10px;
	}
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;ex01 - JSON을 자바 객체로 변환하여 출력하기&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;
	&amp;lt;a href=&quot;${cpath }/ex01/js&quot;&amp;gt;&amp;lt;button&amp;gt;JS로 처리하기&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
&amp;lt;/p&amp;gt;

&amp;lt;div id=&quot;root&quot;&amp;gt;
	&amp;lt;c:forEach var=&quot;dto&quot; items=&quot;${list }&quot;&amp;gt;
	&amp;lt;div class=&quot;item&quot;&amp;gt;
		&amp;lt;div&amp;gt;&amp;lt;h3&amp;gt;${dto.UC_SEQ }. ${dto.TITLE } (${dto.GUGUN_NM })&amp;lt;/h3&amp;gt;&amp;lt;/div&amp;gt;
		&amp;lt;div&amp;gt;${dto.HOMEPAGE_URL }&amp;lt;/div&amp;gt;
		&amp;lt;div&amp;gt;&amp;lt;img src=&quot;${dto.MAIN_IMG_NORMAL }&quot; height=&quot;300&quot;&amp;gt;&amp;lt;/div&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;details&amp;gt;
				&amp;lt;summary&amp;gt;상세보기&amp;lt;/summary&amp;gt;
				&amp;lt;span&amp;gt;${dto.ITEMCNTNTS }&amp;lt;/span&amp;gt;
			&amp;lt;/details&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;/c:forEach&amp;gt;
&amp;lt;/div&amp;gt;


&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 api의 내용을 알고 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드명이 반드시 일치해야 불러올 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;ex01- js.jsp&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;json 파일을 자바 스크립트로 변환하여 출력하기&lt;/p&gt;
&lt;pre id=&quot;code_1728491688312&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;ex01-js&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
	#root {
		width: 900px;
		margin: 20px auto;
	}
	.item {
		width: 800px;
		margin: 10px auto;
		border: 1px solid grey;
		padding: 10px;
	}
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;ex01 - JSON을 자바스크립트로 처리하여 출력하기&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
	const jsonObject = ${json}
	
// 	console.log(jsonObject.getFestivalKr.item)
	const arr = jsonObject.getFestivalKr.item
	const root = document.getElementById('root')
	
	root.innerHTML = ''
	for(let i = 0; i &amp;lt; arr.length; i++) {
		let tag = ''
		tag += '&amp;lt;div class=&quot;item&quot;&amp;gt;'
		tag += '	&amp;lt;div&amp;gt;&amp;lt;h3&amp;gt;' + arr[i].UC_SEQ + '. ' + arr[i].TITLE + '(' + arr[i].GUGUN_NM + ')&amp;lt;/h3&amp;gt;&amp;lt;/div&amp;gt;'
		tag += '	&amp;lt;div&amp;gt;' + arr[i].HOMEPAGE_URL + '&amp;lt;/div&amp;gt;'
		tag += '	&amp;lt;div&amp;gt;&amp;lt;img src=&quot;' + arr[i].MAIN_IMG_NORMAL + '&quot; height=&quot;300&quot;&amp;gt;&amp;lt;/div&amp;gt;'
		tag += '	&amp;lt;div&amp;gt;'
		tag += '		&amp;lt;details&amp;gt;'
		tag += '			&amp;lt;summary&amp;gt;상세보기&amp;lt;/summary&amp;gt;'
		tag += '			&amp;lt;span&amp;gt;' + arr[i].ITEMCNTNTS + '&amp;lt;/span&amp;gt;'
		tag += '		&amp;lt;/details&amp;gt;'
		tag += '	&amp;lt;/div&amp;gt;'
		tag += '&amp;lt;/div&amp;gt;'
		root.innerHTML += tag
	}
&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;&amp;nbsp;Ex01Controller&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728491716077&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itbank.service.Ex01Service;

@Controller
public class Ex01Controller {
	
	@Autowired private Ex01Service service;
	private ObjectMapper objectMapper = new ObjectMapper();
	
	@GetMapping(&quot;/ex01/js&quot;)
	public ModelAndView ex01Js() throws MalformedURLException, IOException {
		ModelAndView mav = ex01();
		mav.setViewName(&quot;ex01-js&quot;);
		return mav;
	}

	@GetMapping(&quot;/ex01&quot;)
	public ModelAndView ex01() throws MalformedURLException, IOException {
		ModelAndView mav = new ModelAndView();
		
		String json = service.getFestivalJson();	// JSON 데이터는 문자열이다
		mav.addObject(&quot;json&quot;, json);
		System.out.println(json);
		
		// JSON 형식의 문자열을 자바 객체로 변환하기 위한 코드
		JsonNode node = objectMapper.readTree(json);
		JsonNode item = node.get(&quot;getFestivalKr&quot;).get(&quot;item&quot;);
		System.out.println(&quot;item : &quot; + item.toPrettyString());
		
//		DTO로 맵핑하기 (필드이름이 복잡하여 제대로 맵핑되지 않았다)
//		List&amp;lt;FestivalDTO&amp;gt; list = Arrays.asList(
//				objectMapper.readValue(item.toPrettyString(), FestivalDTO[].class)
//		);
//		System.out.println(list.get(0).getMAIN_TITLE());
		
//		HashMap으로 맵핑하기
		@SuppressWarnings(&quot;unchecked&quot;)
		List&amp;lt;HashMap&amp;lt;String, Object&amp;gt;&amp;gt; list = Arrays.asList(
				objectMapper.readValue(item.toPrettyString(), HashMap[].class)
		);
		System.out.println(list.get(0).get(&quot;MAIN_TITLE&quot;));
		
		mav.addObject(&quot;list&quot;, list);
		return mav;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;위 코드에서 어려웠던 부분&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;처음에는 DTO로 처리하려고 했으나, 필드명이 너무 복잡해서 mapping이 잘 되지 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;그래서 &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;mapping을 HashMap으로 구현&lt;/b&gt;&lt;/span&gt;하였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;FestivalDTO&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728491837045&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

// JSON데이터에서 DTO에 명시되지 않은 속성(알수없는 속성)은 무시하겠다
@JsonIgnoreProperties(ignoreUnknown = true)

// JSON의 속성이름을 자바 네이밍 컨벤션(camelCase)으로 변환하는 과정이 맞아야 한다
public class FestivalDTO {

	@JsonProperty(&quot;UC_SEQ&quot;)				private int UC_SEQ;
	@JsonProperty(&quot;MAIN_TITLE&quot;)			private String MAIN_TITLE;
	@JsonProperty(&quot;GUGUN_NM&quot;)			private String GUGUN_NM;
	@JsonProperty(&quot;HOMEPAGE_URL&quot;)		private String HOMEPAGE_URL;
	@JsonProperty(&quot;MAIN_IMG_NORMAL&quot;)	private String MAIN_IMG_NORMAL;
	@JsonProperty(&quot;ITEMCNTNTS&quot;)			private String ITEMCNTNTS;
	
	public int getUC_SEQ() {
		return UC_SEQ;
	}
	public void setUC_SEQ(int uC_SEQ) {
		UC_SEQ = uC_SEQ;
	}
	public String getMAIN_TITLE() {
		return MAIN_TITLE;
	}
	public void setMAIN_TITLE(String mAIN_TITLE) {
		MAIN_TITLE = mAIN_TITLE;
	}
	public String getGUGUN_NM() {
		return GUGUN_NM;
	}
	public void setGUGUN_NM(String gUGUN_NM) {
		GUGUN_NM = gUGUN_NM;
	}
	public String getHOMEPAGE_URL() {
		return HOMEPAGE_URL;
	}
	public void setHOMEPAGE_URL(String hOMEPAGE_URL) {
		HOMEPAGE_URL = hOMEPAGE_URL;
	}
	public String getMAIN_IMG_NORMAL() {
		return MAIN_IMG_NORMAL;
	}
	public void setMAIN_IMG_NORMAL(String mAIN_IMG_NORMAL) {
		MAIN_IMG_NORMAL = mAIN_IMG_NORMAL;
	}
	public String getITEMCNTNTS() {
		return ITEMCNTNTS;
	}
	public void setITEMCNTNTS(String iTEMCNTNTS) {
		ITEMCNTNTS = iTEMCNTNTS;
	}
	

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;Ex01Service&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Ex01Controller의 호출을 받고,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청된 내용을 처리한 후&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과값을 호출한 장소로 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728491869995&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Scanner;

import org.springframework.stereotype.Service;

@Service
public class Ex01Service {
	// 공공데이터포털, 부산 축제 정보

	private final String serviceKey = &quot;K7G5hCA%2FRqnmALDK%2F7POZXDGSgTgQFRIcOqpF8HUf9rqLn17QSaJ4Q0Ox732h%2BF%2FgxuyB3bXrdEWApNVwrOtWA%3D%3D&quot;;
	
	public String getFestivalJson() throws MalformedURLException, IOException {
		// 1) 요청 주소 및 파라미터 준비
		String url = &quot;https://apis.data.go.kr/6260000/FestivalService/getFestivalKr&quot;;
		HashMap&amp;lt;String, String&amp;gt; param = new HashMap&amp;lt;&amp;gt;();
		param.put(&quot;pageNo&quot;, &quot;1&quot;);
		param.put(&quot;numOfRows&quot;, &quot;10&quot;);
		param.put(&quot;resultType&quot;, &quot;json&quot;);
		param.put(&quot;serviceKey&quot;, serviceKey);
		url += &quot;?&quot;;
		for(String key : param.keySet()) {
			url += key + &quot;=&quot; + param.get(key) + &quot;&amp;amp;&quot;;
		}
		
		// 2) 요청을 전송하여 응답을 받아서 저장
		Scanner sc = null;
		String response = &quot;&quot;;
		HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
		
		if(conn.getResponseCode() == 200) {	// 200 = 정상
			sc = new Scanner(conn.getInputStream());
			while(sc.hasNextLine()) {
				response += sc.nextLine();
			}
			sc.close();
			conn.disconnect();
		}
		
		return response;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;여기서 주의할 점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;공공데이터 포털 사이트에서 url과 serviceKey 오타를 항상 조심해야 함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DTO로 매핑을 계속 시도 했는데 잘 되지도 않고, 비효율적인 것 같아서 Chat gpt의 도움을 조금 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 HashMap을 사용하게 되었고, 훨씬 효율적인 로직이 완성되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제나 간결성, 가독성 그리고 효율성이 좋은 코드를 작성하도록 노력이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 02.11. 16:24';
&lt;/script&gt;
&lt;/p&gt;</description>
      <category>Spring</category>
      <category>HashMap</category>
      <category>JavaScript</category>
      <category>Mapping</category>
      <category>공공데이터포털</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/50</guid>
      <comments>https://cases.tistory.com/entry/Spring-HashMap-json-mapping#entry50comment</comments>
      <pubDate>Thu, 10 Oct 2024 01:41:28 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] Search Filter</title>
      <link>https://cases.tistory.com/entry/JavaScript-Search-Filter</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 사이트를 이용할 때 흔히 볼 수 있는 검색 필터 기능을 한 번 자바스크립트로 구현해볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드 풀이&lt;/b&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter는 boolean 형식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;form태그를 쓴다면, form 이 제출될때 이벤트가 발생하도록 해야함.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: input 에 대한 key 이벤트 사용 X&lt;br /&gt;submit 에 대한 click 이벤트 사용 X&lt;br /&gt;form 에 대한 submit 이벤트를 사용한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;form이 submit 되면 새로운 요청이 발생하고, 이후의 자바 스크립트는 무시한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;form 에 대한 submit 이벤트는 반드시 !! 첫줄에 이벤트 기본작동을 막아야함.&lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;event.preventDefault()&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;확실하게 하기 위해서는 console.log 를 이용하는 것이 가장 바람직하다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;console.log(searchValue)&amp;nbsp; : 확인이 완료되면, 해당 코드는 아예 지우거나, 또는 주석처리로 남겨둔다&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색어 가져오기&lt;/b&gt;&lt;br /&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const searchValue = event.target.querySelector('input').value&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대상을 배열 형태로 불러옴&lt;/b&gt;&lt;br /&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const arr&amp;nbsp;&amp;nbsp;= Array.from(document.querySelectorAll('ul &amp;gt; li'))&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배열의 각각에 있는 hidden 을 모두 지워서 초기화&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;arr.forEach(e =&amp;gt; e.classList.remove('hidden'))&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색어를 포함하지 않는 배열을 별도로 불러와서 모두 hidden 처리하기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;: arr.filter 는 새로운 배열을 반환하기 때문에 arr2로 저장&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;nbsp; arr.filter의 콜백함수는 boolean 타입을 반환하면 됨&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; const arr2 = arr.filter(e =&amp;gt; e.innerText.includes(searchValue) == false)&amp;nbsp; &amp;nbsp;&lt;br /&gt;arr2.forEach(e =&amp;gt; e.classList.add('hidden'))&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728490873585&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
        .hidden {
            display: none;
        }

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

    &amp;lt;h1&amp;gt;내가 좋아하는 남자배우&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;

    &amp;lt;form id=&quot;searchForm&quot;&amp;gt;
        &amp;lt;p&amp;gt;
            &amp;lt;input id=&quot;search&quot; type=&quot;search&quot; autocomplete=&quot;off&quot; autofocus&amp;gt;
            &amp;lt;input type=&quot;submit&quot; value=&quot;검색&quot;&amp;gt;
        &amp;lt;/p&amp;gt;

    &amp;lt;/form&amp;gt;

    &amp;lt;ul&amp;gt;
        &amp;lt;li&amp;gt;차은우&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;김수현&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;서강준&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;변우석&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;위하준&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;김우빈&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;김건우&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;이민기&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;송강&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;강태오&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;이민기&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;우도환&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;조정석&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;


    &amp;lt;script&amp;gt;

        const searchForm = document.getElementById('searchForm')
        const ul = document.querySelector('ul')

        searchForm.onsubmit = function(event) {
            event.preventDefault()
            
            const searchValue = event.target.querySelector('input').value

            const arr  = Array.from(document.querySelectorAll('ul &amp;gt; li'))
            arr.forEach(e =&amp;gt; e.classList.remove('hidden'))


            //  검색어를 포함하지 않는 배열을 별도로 불러와서 모두 hidden 처리하기
            //  arr.filter 는 새로운 배열을 반환함(arr2 로 저장)
            //  arr.filter의 콜백함수는 boolean 타입을 반환하면 됨
            //  (조건을 만족하는 내용만 남기고 모두 제거)
            const arr2 = arr.filter(e =&amp;gt; e.innerText.includes(searchValue) == false)    
            arr2.forEach(e =&amp;gt; e.classList.add('hidden'))    
        }
    &amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boolean형식으로 되어 있어서 조건을 처리하기가 까다롭지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색필터를 다른 언어로도 코드를 짜보니까 새롭기도 하고 재밌었다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 02.08. 23:58';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>Filter</category>
      <category>JavaScript</category>
      <category>search</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/49</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-Search-Filter#entry49comment</comments>
      <pubDate>Thu, 10 Oct 2024 01:23:51 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] ClickEvent</title>
      <link>https://cases.tistory.com/entry/JavaScript-ClickEvent</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 자바스크립트의 클릭이벤트를 활용하는 방법을 알아볼거다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 버튼을 이용하여, 오름차순 정렬과 내림차순 정렬 모두 수행할 수 있도록 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 홀수번째로 눌렀을때는 오름차순 정렬&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;짝수번째로 눌렀을때는 내림차순 정렬로 바뀌도록 할 것이다&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;script 를 해당 폴더에 넣어두고, 선언해서 사용한다&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; &amp;lt;script src=&quot;parking.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;arrow 라는 클래스를 만들어서 여기에  또는  가 나오도록 할것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt; : 오름차순을 의미&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;  : 내림차순을 의미&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;order 값이 1이라면, 버튼을 한번 더 눌렀을때에는 값이 -1로 바뀐다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; const order = +target.getAttribute('order')&lt;br /&gt;target.setAttribute('order', -order)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문자열이 아닌, 정수형태로 비교하기 위해서 + 부호를 붙인다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;arr3.sort((e1, e2)=&amp;gt; {&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;const v1 = +e1.children[idx].innerText&amp;nbsp;&amp;nbsp;&lt;br /&gt;const v2 = +e2.children[idx].innerText&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기본적으로는 오름차순으로 정렬해둔다.&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/b&gt;&lt;br /&gt;return (v1 &amp;lt;= v2 ? 1 : -1) * order&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&amp;nbsp;핵심&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;: 클릭이벤트는 3번째 컬럼부터 적용시키기&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; columns.slice(3, 7).forEach(e =&amp;gt; e.onclick = sortHandler)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1728490442501&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;style&amp;gt;
        .item {
            display: flex;
            max-width: 1200px;
            margin: auto;
        }
        .item &amp;gt; div {
            flex: 2;
            border: 1px solid grey;
            padding: 5px 10px;
        }
        .item &amp;gt; div:nth-child(1) { flex: 5; }
        .item &amp;gt; div:nth-child(2) { flex: 4; }
        .columns {
            background-color: #eee;
            position: sticky;
            top: 0;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

    &amp;lt;h1&amp;gt;부산광역시 주차장 요금 현황&amp;lt;/h1&amp;gt;
    &amp;lt;hr&amp;gt;

    &amp;lt;div id=&quot;root&quot;&amp;gt;
        &amp;lt;div class=&quot;columns item&quot;&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;pkNam&quot;&amp;gt;&amp;lt;span class=&quot;text&quot;&amp;gt;주차장이름&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;guNm&quot;&amp;gt;&amp;lt;span class=&quot;text&quot;&amp;gt;지역&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;pkFm&quot;&amp;gt;&amp;lt;span class=&quot;text&quot;&amp;gt;형식&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;tenMin&quot;&amp;lt;span class=&quot;text&quot;&amp;gt;10분주차요금&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;ftDay&quot;&amp;gt;&amp;lt;span class=&quot;text&quot;&amp;gt;일주차요금&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;ftMon&quot;&amp;gt;&amp;lt;span class=&quot;text&quot;&amp;gt;월주차요금&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;pkCnt&quot;&amp;gt;&amp;lt;span class=&quot;text&quot;&amp;gt;전체주차대수&amp;lt;/span&amp;gt;&amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;

        &amp;lt;div class=&quot;main&quot;&amp;gt;&amp;lt;/div&amp;gt;  &amp;lt;!-- 내용이 나와야할 곳--&amp;gt;

    &amp;lt;/div&amp;gt;
    
    &amp;lt;script src=&quot;parking.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        
        //  JSON 데이터 출력하기
        const arr2 = arr.slice(0, 300)
        console.log(arr2)

        const main = document.querySelector('.main')
        arr2.forEach(e =&amp;gt; {
            let tag = ''
            tag += `&amp;lt;div class=&quot;item&quot;&amp;gt;`
            tag += `    &amp;lt;div class=&quot;pkNam&quot;&amp;gt;${e.pkNam }&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div class=&quot;guNm&quot;&amp;gt;${e.guNm }&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div class=&quot;pkFm&quot;&amp;gt;${e.pkFm }&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div class=&quot;tenMin&quot;&amp;gt;${e.tenMin == '-' ? 0 : e.tenMin}&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div class=&quot;ftDay&quot;&amp;gt;${e.ftDay == '-' ? 0 : e.ftDay }&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div class=&quot;ftMon&quot;&amp;gt;${e.ftMon == '-' ? 0 : e.ftMon }&amp;lt;/div&amp;gt;`
            tag += `    &amp;lt;div class=&quot;pkCnt&quot;&amp;gt;${e.pkCnt }&amp;lt;/div&amp;gt;`
            tag += `&amp;lt;/div&amp;gt;`
            main.innerHTML += tag
        })
        
        //  클릭이벤트 설정
        const columns = Array.from(document.querySelectorAll('div.columns &amp;gt; div'))  //  배열로 만들기

        //  클릭이벤트
        function sortHandler(event) {
   
            //  내가 클릭한 대상은 columns에서 몇번째 인덱스를 가지는가?
            let target = event.target
            while(target.tagName != 'DIV') {
                target = target.parentNode
            }

            const idx = columns.indexOf(target)
            console.log(idx)	//  idx가 -1 이라면 대상을 찾지 못했다는 뜻.

            const order = +target.getAttribute('order')	//  order가 1이라면
            
            target.setAttribute('order', -order)  //  그 다음은 -1 이 나올것임(이것을 반환할때 곱해주면 함수실행할때마다 값이 바뀜)
            
            document.querySelectorAll('span.arrow').forEach(e =&amp;gt; e.innerText = '')

            target.querySelector('span.arrow').innerText = order &amp;gt; 0 ? ' ' : ' '

            const arr3 = Array.from(document.querySelectorAll('div.main &amp;gt; div.item'))

            arr3.sort((e1, e2)=&amp;gt; {
                //  문자열이 아닌, 정수형태로 비교
                const v1 = +e1.children[idx].innerText   //  각 컬럼을 [0] [1] ...  == [idx]
                const v2 = +e2.children[idx].innerText
                
                return (v1 &amp;lt;= v2 ? 1 : -1) * order
                
                //  children :  HTMLCollection 형태, index 는 존재함
                //  저번에 +a.querySelector('td:nth-child(2)').innerText 처럼
                //  원하는 컬럼의 데이터만 뽑아올 수 있도록 한다

            })
            arr3.forEach(e =&amp;gt; main.appendChild(e))
        }
        columns.slice(3, 7).forEach(e =&amp;gt; e.onclick = sortHandler)

    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘도 코드를 작성하면서 삼항연산자를 사용하여 코드의 길이를 더 줄어든 일이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 작성하는 코드이지만 어떻게 하면 더 간결하고, 가독성이 좋게 코드를 작성할 수 있을지 생각하는게 관건인 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 02.05. 20:17';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>clickevent</category>
      <category>JavaScript</category>
      <category>클릭이벤트</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/48</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-ClickEvent#entry48comment</comments>
      <pubDate>Thu, 10 Oct 2024 01:18:29 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 정렬</title>
      <link>https://cases.tistory.com/entry/JavaScript-%EC%A0%95%EB%A0%AC</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 JavaScript의 정렬에 대해서 한 번 알아보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;출력할 js파일을 vscode에 넣어두어야한다.&lt;/b&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1728482212623&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const arr = [
    {
        &quot;user&quot;: &quot;TrynMst&quot;,
        &quot;champion&quot;: &quot;퀸&quot;,
        &quot;team&quot;: &quot;블루&quot;,
        &quot;kill&quot;: 7,
        &quot;death&quot;: 7,
        &quot;assist&quot;: 7,
        &quot;damage&quot;: 27176,
        &quot;cs&quot;: 173
    },
    {
        &quot;user&quot;: &quot;강승우의 제발PLZ&quot;,
        &quot;champion&quot;: &quot;마오카이&quot;,
        &quot;team&quot;: &quot;블루&quot;,
        &quot;kill&quot;: 4,
        &quot;death&quot;: 6,
        &quot;assist&quot;: 17,
        &quot;damage&quot;: 22744,
        &quot;cs&quot;: 144
    },
    {
        &quot;user&quot;: &quot;Hide on bush&quot;,
        &quot;champion&quot;: &quot;산드라&quot;,
        &quot;team&quot;: &quot;블루&quot;,
        &quot;kill&quot;: 8,
        &quot;death&quot;: 3,
        &quot;assist&quot;: 10,
        &quot;damage&quot;: 42702,
        &quot;cs&quot;: 274
    },
    {
        &quot;user&quot;: &quot;128482031&quot;,
        &quot;champion&quot;: &quot;바루스&quot;,
        &quot;team&quot;: &quot;블루&quot;,
        &quot;kill&quot;: 14,
        &quot;death&quot;: 5,
        &quot;assist&quot;: 8,
        &quot;damage&quot;: 31350,
        &quot;cs&quot;: 277
    },

...중략 
]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전체코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728482246027&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;style&amp;gt;
        #root {
            border: 2px solid black;
            max-width: 1200px;
            margin: auto;
        }
        .item {
            display: flex;
            border-bottom: 1px solid grey;
        }
        .item &amp;gt; div {
            padding: 10px 20px;
            flex: 2;
        }
        .item &amp;gt; div:first-child {
            flex: 4;
        }
        .item &amp;gt; div:nth-child(2) {
            flex: 3;
        }
        .columns {
            background-color: #eee;
            cursor: pointer;
            user-select: none;      /*  사용자가 드래그로 글자 및 그림을 선택할 수 없음 */
        }
        .item.blue {
            background-color: skyblue;
        }
        .item.red {
            background-color: lightpink;
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div id=&quot;root&quot;&amp;gt;
        &amp;lt;div class=&quot;columns item&quot;&amp;gt;  &amp;lt;!--    .columns : item 클래스를 추가로 작성 --&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;user&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;유저&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;champion&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;챔피언&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;team&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;팀&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;kill&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;킬&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;death&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;데스&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;assist&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;어시스트&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;damage&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;데미지&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div order=&quot;1&quot; class=&quot;cs&quot;&amp;gt;
                &amp;lt;span class=&quot;text&quot;&amp;gt;CS&amp;lt;/span&amp;gt;
                &amp;lt;span class=&quot;arrow&quot;&amp;gt;&amp;lt;/span&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;main&quot;&amp;gt;  &amp;lt;!--    .main : 정렬의 대상 --&amp;gt;

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

    &amp;lt;script src=&quot;opgg_result.js&quot;&amp;gt;&amp;lt;/script&amp;gt;	&amp;lt;!-- 사용할 파일 --&amp;gt;


    &amp;lt;!--        div class=&quot;item ${e.team == '블루' ? 'blue' : 'red'}&quot;
                : class 이름에다가 조건에 따라서 blue 또는 red 라는 문자열을 더 추가한다
    --&amp;gt;
    &amp;lt;script&amp;gt;
        console.log(arr)

        //  일반 출력
        function load() {
            const main = document.querySelector('div.main')
            arr.forEach(e =&amp;gt; {
                let tag = ''
                tag += `&amp;lt;div class=&quot;item ${e.team == '블루' ? 'blue' : 'red'}&quot;&amp;gt;`
                tag += `    &amp;lt;div class=&quot;user&quot;&amp;gt;${e.user}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;champion&quot;&amp;gt;${e.champion}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;team&quot;&amp;gt;${e.team}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;kill&quot;&amp;gt;${e.kill}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;death&quot;&amp;gt;${e.death}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;assist&quot;&amp;gt;${e.assist}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;damage&quot;&amp;gt;${e.damage}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;cs&quot;&amp;gt;${e.cs}&amp;lt;/div&amp;gt;`
                tag += `&amp;lt;/div&amp;gt;`
                main.innerHTML += tag
            })
        }
        window.addEventListener('DOMContentLoaded', load)

        


        //  먼저, 클릭이벤트를 적용할 부분을 추출하자
        const columns = document.querySelectorAll('.columns &amp;gt; div') //  모든 컬럼을 불러와서
        const sort = Array.from(columns).slice(3, 8)                //  3부터 8까지만 잘라낸다
        const main = document.querySelector('.main')                //  클릭이벤트 정렬 후 main에 내용을 넣는다
        
        
        function sortHandler(event) {   //  클릭이벤트
            let target = event.target   //  클릭 대상 (클릭한 대상이 target 으로 지정됨)
            //  이벤트는 div 에 걸어도, span이 타깃이 될수도 있음(span을 클릭할수도 있기 때문)



            while(target.tagName != 'DIV') {    // 대상의 태그이름이 DIV가 아니라면 반복(div를 가리킬때까지 반복한다)
                target = target.parentNode      // 상위 요소를 대상으로 지정한다(div가 가장 상위요소임)
            }


            const className = target.className    // 태그의 클래스이름을 문자열로 불러온다
            const order = +target.getAttribute('order') // getAttribute('속성이름')     : order값을 정수로 불러온다 (+기호를 붙이면 정수로 불러옴)
            target.setAttribute('order', -order)        // setAttribute('속성이름', 값) : 부호반전시켜서 새로 저장한다
            console.log(className, order)



            //  모든 span.arrow 의 내부 글자를 지운다  (삼각형 기호들이 지워질 것임)
            //  항상 무엇을 클릭하든간에 모든 값을 없애버림 (kill컬럼을 클릭 후에 또 다른 컬럼을 클릭하면 kill 컬럼의 삼각형이 없어질 수 있는 이유임)
            document.querySelectorAll('span.arrow').forEach(span =&amp;gt; span.innerText = '')



            // 클릭된 대상의 span.arrow 에는 order(정렬순서)에 따라서 삼각형 기호를 넣어준다
            target.querySelector('span.arrow').innerText = order &amp;gt; 0 ? '▲' : '▼'    //  order가  1이면 ▲
                                                                                    //  order가 -1이면 ▼



            // main의 item들을 불러와서
            const arr2 = Array.from(document.querySelectorAll('.main &amp;gt; .item'))
            arr2.sort((e1, e2) =&amp;gt; {
                const v1 = +e1.querySelector('.' + className).innerText     // 태그 내부 값을 정수로 변환
                const v2 = +e2.querySelector('.' + className).innerText


                const ret = v1 - v2 &amp;gt;= 0 ? 1 : -1   //  정렬에 필요한 정수를 반환하기 위해 준비한다  (정렬을 위한 코드)
                return ret * order                  //  반환값의 부호가 반전되도록 1 or -1을 곱한다
                //  order ==  1일때  arrow 는 ▼(내림차순)
                //  order == -1일때  arrow 는 ▲(오름차순)
            })


            //  정렬이 끝나면 appendChild
            arr2.forEach(e =&amp;gt; main.appendChild(e))
        }

        //  클릭이벤트 실행
        sort.forEach(e =&amp;gt; e.onclick = sortHandler)
    &amp;lt;/script&amp;gt;
    
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;단순 출력 함수 (load)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팀명에 따라 배경색을 다르게 해주고 싶어서&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;팀명에 조건을 넣어 주었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;삼항연산자에 의하여 div class 명이 달라지니까&amp;nbsp;style에는 class명에 따라서&amp;nbsp;색깔을 다르게 넣어주자.&lt;/p&gt;
&lt;pre id=&quot;code_1728482318772&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;script&amp;gt;
        console.log(arr)

        //  일반 출력
        function load() {
            const main = document.querySelector('div.main')
            arr.forEach(e =&amp;gt; {
                let tag = ''
                tag += `&amp;lt;div class=&quot;item ${e.team == '블루' ? 'blue' : 'red'}&quot;&amp;gt;`
                tag += `    &amp;lt;div class=&quot;user&quot;&amp;gt;${e.user}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;champion&quot;&amp;gt;${e.champion}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;team&quot;&amp;gt;${e.team}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;kill&quot;&amp;gt;${e.kill}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;death&quot;&amp;gt;${e.death}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;assist&quot;&amp;gt;${e.assist}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;damage&quot;&amp;gt;${e.damage}&amp;lt;/div&amp;gt;`
                tag += `    &amp;lt;div class=&quot;cs&quot;&amp;gt;${e.cs}&amp;lt;/div&amp;gt;`
                tag += `&amp;lt;/div&amp;gt;`
                main.innerHTML += tag
            })
        }
        window.addEventListener('DOMContentLoaded', load)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;`&amp;lt;div&amp;nbsp;class=&quot;item&amp;nbsp;${e.team&amp;nbsp;==&amp;nbsp;'블루'&amp;nbsp;?&amp;nbsp;'blue'&amp;nbsp;:&amp;nbsp;'red'}&quot;&amp;gt;`&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;team의 내용이 '블루' 라면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;div의 클래스명이 item blue가 됨.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;'블루'가 아니라면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;div의 클래스명이 item red가 됨.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;팀의 이름에 따라, div 클래스명을 바꿔주고,&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클래스명을 이용하여 style 태그에 background-color 를 바꿔준다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;클릭 이벤트 함수 (clickHandler)&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728482369339&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//  먼저, 클릭이벤트를 적용할 부분을 추출하자
   const columns = document.querySelectorAll('.columns &amp;gt; div') //  모든 컬럼을 불러와서
   const sort = Array.from(columns).slice(3, 8)                //  3부터 8까지만 잘라낸다
   const main = document.querySelector('.main')                //  클릭이벤트 정렬 후 main에 내용을 넣는다
        
        
   function sortHandler(event) {   //  클릭이벤트
       let target = event.target   //  클릭 대상 (클릭한 대상이 target 으로 지정됨)
       //  이벤트는 div 에 걸어도, span이 타깃이 될수도 있음(span을 클릭할수도 있기 때문)



        while(target.tagName != 'DIV') {    // 대상의 태그이름이 DIV가 아니라면 반복(div를 가리킬때까지 반복한다)
            target = target.parentNode      // 상위 요소를 대상으로 지정한다(div가 가장 상위요소임)
        }


       const className = target.className    // 태그의 클래스이름을 문자열로 불러온다
       const order = +target.getAttribute('order') // getAttribute('속성이름')     : order값을 정수로 불러온다 (+기호를 붙이면 정수로 불러옴)
       target.setAttribute('order', -order)        // setAttribute('속성이름', 값) : 부호반전시켜서 새로 저장한다
       console.log(className, order)



       //  모든 span.arrow 의 내부 글자를 지운다  (삼각형 기호들이 지워질 것임)
       //  항상 무엇을 클릭하든간에 모든 값을 없애버림 (kill컬럼을 클릭 후에 또 다른 컬럼을 클릭하면 kill 컬럼의 삼각형이 없어질 수 있는 이유임)
      document.querySelectorAll('span.arrow').forEach(span =&amp;gt; span.innerText = '')



      // 클릭된 대상의 span.arrow 에는 order(정렬순서)에 따라서 삼각형 기호를 넣어준다
      target.querySelector('span.arrow').innerText = order &amp;gt; 0 ? '▲' : '▼'    
      //  order가  1이면 ▲
      //  order가 -1이면 ▼



      // main의 item들을 불러와서
      const arr2 = Array.from(document.querySelectorAll('.main &amp;gt; .item'))
      arr2.sort((e1, e2) =&amp;gt; {
          const v1 = +e1.querySelector('.' + className).innerText     // 태그 내부 값을 정수로 변환
          const v2 = +e2.querySelector('.' + className).innerText


          const ret = v1 - v2 &amp;gt;= 0 ? 1 : -1   //  정렬에 필요한 정수를 반환하기 위해 준비한다  (정렬을 위한 코드)
          return ret * order                  //  반환값의 부호가 반전되도록 1 or -1을 곱한다
                //  order ==  1일때  arrow 는 ▼(내림차순)
                //  order == -1일때  arrow 는 ▲(오름차순)
      })


      //  정렬이 끝나면 appendChild
         arr2.forEach(e =&amp;gt; main.appendChild(e))
      }

     //  클릭이벤트 실행
     sort.forEach(e =&amp;gt; e.onclick = sortHandler)
     
     
    &amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML을 공부할 때와 마찬가지로 JavaScript도 F12를 눌러&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자도구와 함께 확인해보는 것이 중요한 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 02.02. 20:42';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>arr</category>
      <category>JavaScript</category>
      <category>정렬</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/47</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-%EC%A0%95%EB%A0%AC#entry47comment</comments>
      <pubDate>Wed, 9 Oct 2024 23:01:56 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] JavaScript 기본 다지기</title>
      <link>https://cases.tistory.com/entry/JavaScript-JavaScript-%EA%B8%B0%EB%B3%B8-%EB%8B%A4%EC%A7%80%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 JavaScript를 접한 나를 위해 기본을 한 번 다져볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;유의사항&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;- JavaScript는 자료형을 쓰지 않아도 된다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;동적 타입을 지원해주기 때문.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;같은 변수에 서로 다른 값을 넣어도, 에러가 나지 않고 값과 타입이 변한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;- 변수로는 var와 let이 있다. 그러나, 일반적으로 let을 많이 사용한다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;var는 재선언이 가능해서, 변수가 어디에 선언되어 있는지 판단하기가 어렵기 때문에&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;프로그램상의 오류를 일으킬 수 있어서 잘 사용하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;- 상수로는 const가 있다. 상수는 변하지 않고 일정한 값을 갖는 수를 의미&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;상수로 지정한 후에는 나중에 값을 바꿀 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;변수&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;let으로 선언&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;gt; let apple = 'yummy'&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;상수&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;const로 선언&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&amp;gt; const arr = [ 1, 2, 3 ]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;숫자로 구성된 배열 정렬&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배열을 만든다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; const arr = [4, 8, 2, 7, 6, 1, 10]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배열을 정렬하는 함수 toSorted&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;람다 함수식을 사용하여, 오름차순으로 정렬한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; const arr2 = arr.toSorted((a, b) =&amp;gt; a - b)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(+) 만약 내림차순 정렬이라면?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; cosnt arr2 = toSorted((a, b) =&amp;gt; b - a)&lt;/p&gt;
&lt;pre id=&quot;code_1728481405710&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;ul class=&quot;test1&quot;&amp;gt;&amp;lt;/ul&amp;gt;

    &amp;lt;script&amp;gt;
        const arr = [4, 8, 2, 7, 6, 1, 10]
        console.log(arr)

        // const arr2 = arr.toSorted((a, b) =&amp;gt; { return a - b })
        const arr2 = arr.toSorted((a, b) =&amp;gt; a - b)
        console.log(arr2)

        const test1 = document.querySelector('ul.test1')
        arr2.forEach((num) =&amp;gt; {
            test1.innerHTML += '&amp;lt;li&amp;gt;' + num + '&amp;lt;/li&amp;gt;'
        })
    &amp;lt;/script&amp;gt;
    
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;1&lt;/li&gt;
&lt;li&gt;2&lt;/li&gt;
&lt;li&gt;4&lt;/li&gt;
&lt;li&gt;6&lt;/li&gt;
&lt;li&gt;7&lt;/li&gt;
&lt;li&gt;8&lt;/li&gt;
&lt;li&gt;10&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문자열 배열 정렬&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바스크립트는 문자열도 크기 비교가 가능하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 기준 함수를 전달하지 않아도, 문자열로 정렬해주지만 이후 객체 혹은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTMLElement를 정렬하려면 직접 정렬 기준식을 작성할 줄 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬함수에 전달되는 콜백함수는 정수를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환되는 정수가 0보다 큰지, 작은지 판별하여 정렬을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 작성하는 콜백함수에는 항상 1 혹은 -1을 반환할 수 있도록 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;만약 비교기준이 정수라면 -연산을 이용하여 두 수의 차이만 반환해도 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;삼항연사자를 이용한 오름차순 정렬&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;gt; const arr2 = arr.toSorted((a, b) =&amp;gt; &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;{return (a &amp;gt; b) ? 1 : -1 })&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(+) 만약 내림차순 정렬을 하려면?&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const arr2 = arr.toSorted((a, b) =&amp;gt; {return (a &amp;lt; b) ? 1 : -1 })&lt;/p&gt;
&lt;pre id=&quot;code_1728481627972&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; &amp;lt;ul class=&quot;test&quot;&amp;gt;&amp;lt;/ul&amp;gt;

    &amp;lt;script&amp;gt;
        const arr = ['유빈', '유진', '현웅', '민정']
        // 위 배열을 정렬하여 ul.test에 출력하세요

        console.log('apple' &amp;gt; 'banana') // &quot;apple&quot;.compareTo(&quot;banana&quot;) &amp;gt; 0
        console.log('apple' &amp;lt; 'banana') // &quot;apple&quot;.compareTo(&quot;banana&quot;) &amp;lt; 0

        const arr2 = arr.toSorted((a, b) =&amp;gt; {return (a &amp;gt; b) ? 1 : -1})

        const ul = document.querySelector('ul')
        arr2.forEach(e =&amp;gt; ul.innerHTML += '&amp;lt;li&amp;gt;' + e + '&amp;lt;/li&amp;gt;')
    &amp;lt;/script&amp;gt;
    
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;민정&lt;/li&gt;
&lt;li&gt;유빈&lt;/li&gt;
&lt;li&gt;유진&lt;/li&gt;
&lt;li&gt;현웅&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;버튼을 클릭했을때 정렬되도록 하기&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버튼의 id 가 sortAsc 인 요소를 getElementById 를 이용하여 불러온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; const&amp;nbsp;sortAsc&amp;nbsp;=&amp;nbsp;document.getElementById('sortAsc')&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정렬 대상을 불러온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; const&amp;nbsp;ul&amp;nbsp;=&amp;nbsp;document.querySelector('ul')&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CSS선택자로 모든 요소를 NodeList 형태로 불러온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; const&amp;nbsp;liList&amp;nbsp;=&amp;nbsp;document.querySelectorAll('ul&amp;nbsp;&amp;gt;&amp;nbsp;li')&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NodeList타입에는 sort()가 없기 때문에, 배열로 바꿔준다.&lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const liArray = Array.from(liList)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정렬을 수행할 대상들은 &amp;lt;li&amp;gt; 이고, 그 기준은 &amp;lt;li&amp;gt;의 내부 텍스트이다.&lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; const resultArray = liArray.toSorted((a, b) =&amp;gt; { return a.innerText &amp;gt; b.innerText ? 1 : -1&amp;nbsp;})&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;정렬을 수행했지만 스크립트내부에서만 적용되기 때문에&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;문서에 반영하기 위해 appendChild를 수행한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;br /&gt;기존에 있는 요소이므로, 추가하지 않고 자리만 바꾼다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;resultArray.forEach(e =&amp;gt; ul.appendChild(e))&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728481793252&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
        &amp;lt;li&amp;gt;유빈&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;민정&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;진호&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;재영&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;민서&amp;lt;/li&amp;gt;
        &amp;lt;li&amp;gt;형주&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;button id=&quot;sortAsc&quot;&amp;gt;오름차순 정렬&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        const sortAsc = document.getElementById('sortAsc')

        const ul = document.querySelector('ul')

        sortAsc.onclick = function() {
            const liList = document.querySelectorAll('ul &amp;gt; li')

            const liArray = Array.from(liList)

            const resultArray = liArray.toSorted((a, b) =&amp;gt; {
                return a.innerText &amp;gt; b.innerText ? 1 : -1
            })

            console.log(resultArray)

            resultArray.forEach(e =&amp;gt; ul.appendChild(e)) 
        }
    &amp;lt;/script&amp;gt;
    
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 자바스크립트는 익숙치 않은 언어이지만 나름 재미도 있고, 별로 어렵게 느껴지지 않아서 다행이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 01.30. 21:17';
&lt;/script&gt;</description>
      <category>JavaScript</category>
      <category>const</category>
      <category>JavaScript</category>
      <category>Let</category>
      <category>VAR</category>
      <category>변수</category>
      <category>상수</category>
      <category>정렬</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/46</guid>
      <comments>https://cases.tistory.com/entry/JavaScript-JavaScript-%EA%B8%B0%EB%B3%B8-%EB%8B%A4%EC%A7%80%EA%B8%B0#entry46comment</comments>
      <pubDate>Wed, 9 Oct 2024 22:51:28 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 매장 포스기 시스템</title>
      <link>https://cases.tistory.com/entry/Spring-%EB%A7%A4%EC%9E%A5-%ED%8F%AC%EC%8A%A4%EA%B8%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;매장 같은 곳에서 사용하는 포스기들의 시스템을 실제로 비슷하게 구현하여 보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 테이블로 구성해야 하는 항목&amp;nbsp;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;= CRUD 작업 대상&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 상품 테이블 (Product)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 매출 테이블 (Sales)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 각 테이블에 대한 스키마 구성&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 테이블&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;상품번호&lt;br /&gt;(primary key)&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;상품명&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;이미지&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;단가&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;수량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;NUMBER&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;VARCHAR2&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;VARCHAR2&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;NUMBER&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;NUMBER (default 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;매출테이블&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;매출번호&lt;br /&gt;(primary key)&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;날짜&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;상품번호&lt;br /&gt;(foreign key)&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;판매수량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;NUMBER&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;DATE&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;NUMBER&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;NUMBER&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 각 테이블에 대해서 구현할 CRUD 기능&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;상품테이블&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;insert&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;상품 등록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;select&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;상품 조회&lt;br /&gt;(전체 목록 / 단일 조회) -- 2가지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;update&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;수량 변경&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;delete&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center;&quot;&gt;상품 삭제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;매출테이블&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 21.9767%;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 77.907%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 21.9767%;&quot;&gt;insert&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 77.907%;&quot;&gt;매출 등록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 21.9767%;&quot;&gt;select&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 77.907%;&quot;&gt;매출 목록&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 21.9767%;&quot;&gt;update&lt;/td&gt;
&lt;td style=&quot;color: #333333; text-align: center; width: 77.907%;&quot;&gt;매출 취소&lt;br /&gt;(반품)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;header.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;모든 페이지에서 사용할 태그들을 선언해둔다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 home 에 링크들을 생성해둘 것이기 때문에&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;header.jsp 에는 자주 사용하는 스타일과 태그만 작성해 줄 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1728479499063&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;%@ taglib prefix=&quot;fmt&quot; uri=&quot;http://java.sun.com/jsp/jstl/fmt&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;

&amp;lt;style&amp;gt;
	.frame {
		width: 900px;
		justify-content: center;
	}

	.flex {
		display: flex;
	}
	
	.bold {
		font-weight: bold;
	}

&amp;lt;/style&amp;gt;

&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;home.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728479577410&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ include file=&quot;header.jsp&quot; %&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;


&amp;lt;h1&amp;gt;상품 매출 관리&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;h3&amp;gt;오늘은 &amp;lt;fmt:formatDate value=&quot;${today }&quot; pattern=&quot;yyyy년  MM월 dd일&quot; /&amp;gt; 입니다&amp;lt;/h3&amp;gt;


&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;${cpath }/product/list&quot;&amp;gt;상품 목록&amp;lt;/a&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;${cpath }/product/add&quot;&amp;gt;상품 추가&amp;lt;/a&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;${cpath }/sales/list&quot;&amp;gt;매출 목록&amp;lt;/a&amp;gt;

&amp;lt;/ul&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ProductDTO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728479602494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.web.multipart.MultipartFile;

public class ProductDTO {
	
//	PRODUCT 테이블
	
//	이름    	널?         유형             
//	----- -------- -------------- 
//	IDX   NOT NULL 	NUMBER         
//	NAME  NOT NULL 	VARCHAR2(500)  
//	IMG            	VARCHAR2(1000) 
//	PRICE NOT NULL 	NUMBER         
//	COUNT          	NUMBER 
//	SAVE_IMG				
	
	private int idx;
	private String name;
	private String img;
	private int price;
	private int count;
	
	//	UUID로 변경한 값 
	private String save_img;
	
	//	파일 업로드
	private MultipartFile upload;
	
	public String getSave_img() {
		return save_img;
	}
	public void setSave_img(String save_img) {
		this.save_img = save_img;
	}

	public MultipartFile getUpload() {
		return upload;
	}
	public void setUpload(MultipartFile upload) {
		this.upload = upload;
	}
	public int getIdx() {
		return idx;
	}
	public void setIdx(int idx) {
		this.idx = idx;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getImg() {
		return img;
	}
	public void setImg(String img) {
		this.img = img;
	}
	public int getPrice() {
		return price;
	}
	public void setPrice(int price) {
		this.price = price;
	}
	public int getCount() {
		return count;
	}
	public void setCount(int count) {
		this.count = count;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SalesDTO&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Join 사용해서 다른 테이블에 있는&amp;nbsp;&amp;nbsp;필드의 내용을 가져왔을때에는&lt;br /&gt;결과물의 필드명과 일치해야함으로, DTO에만 필드를 하나 추가하자.&amp;nbsp;&lt;br /&gt;&lt;/b&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;private String s_name;&lt;/p&gt;
&lt;pre id=&quot;code_1728479622271&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Date;

public class SalesDTO {

	
//	Sales 테이블
	
//	이름             널?      유형     
//	------------- -------- ------ 
//	S_IDX         NOT NULL NUMBER 
//	S_DATE                 DATE   
//	S_PRODUCT_IDX          NUMBER 
//	S_COUNT       NOT NULL NUMBER 
//	S_DELETE               NUMBER 
	
	
	private int s_idx;
	private Date s_date;
	private int s_product_idx;
	private int s_count;
	private int s_delete;
	
    // join 사용시에 쓰일 s_name (상품명)
	private String s_name;
	

	public String getS_name() {
		return s_name;
	}
	public void setS_name(String s_name) {
		this.s_name = s_name;
	}
	public int getS_idx() {
		return s_idx;
	}
	public void setS_idx(int s_idx) {
		this.s_idx = s_idx;
	}
	public Date getS_date() {
		return s_date;
	}
	public void setS_date(Date s_date) {
		this.s_date = s_date;
	}
	public int getS_product_idx() {
		return s_product_idx;
	}
	public void setS_product_idx(int s_product_idx) {
		this.s_product_idx = s_product_idx;
	}
	public int getS_count() {
		return s_count;
	}
	public void setS_count(int s_count) {
		this.s_count = s_count;
	}
	public int getS_delete() {
		return s_delete;
	}
	public void setS_delete(int s_delete) {
		this.s_delete = s_delete;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ProductController&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728479649320&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

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.servlet.ModelAndView;

import com.itbank.model.ProductDTO;
import com.itbank.service.ProductService;

@Controller
@RequestMapping(&quot;/product&quot;)
public class ProductController {

	@Autowired private ProductService service;
	
	@GetMapping(&quot;/list&quot;)
	public ModelAndView list() {
		
		ModelAndView mav = new ModelAndView(&quot;/product/list&quot;);
		List&amp;lt;ProductDTO&amp;gt; list = service.getList();
		
		mav.addObject(&quot;list&quot;, list);
		
		return mav;
		
	}
	
	@GetMapping(&quot;/add&quot;)
	public void add() {}
	
	
	@PostMapping(&quot;/add&quot;)
	public String product_add(ProductDTO dto) {
		
		int row = service.add(dto);
		
		System.out.println(row != 0 ? &quot;상품 등록 성공&quot; : &quot;상품 등록 실패&quot;);
		
		return &quot;redirect:/product/list&quot;;
	}
	
	
	@GetMapping(&quot;/update/{idx}&quot;)
	public ModelAndView update(@PathVariable(&quot;idx&quot;) int idx) {
		ModelAndView mav = new ModelAndView(&quot;/product/update&quot;);
		
		ProductDTO dto = service.getIdx(idx);
		
		mav.addObject(&quot;dto&quot;, dto);
		
		return mav;
	}
	
	@PostMapping(&quot;/update/{idx}&quot;)
	public String update(ProductDTO dto) {
		
		int row = service.update(dto);
		
		System.out.println(row != 0 ? &quot;수정 성공&quot; : &quot;수정 실패&quot;);
		
		return &quot;redirect:/product/list&quot;;
	}
	
	@GetMapping(&quot;/delete/{idx}&quot;)
	public String delete(@PathVariable(&quot;idx&quot;) int idx) {
		
		int row = service.delete(idx);
		
		System.out.println(row != 0 ? &quot;삭제 성공&quot; : &quot;삭제 실패&quot;);
		
		return &quot;redirect:/product/list&quot;;
		
	}

	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ProductService&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728479666188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;

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

import com.itbank.model.ProductDTO;
import com.itbank.repository.ProductDAO;

@Service
public class ProductService {

	@Autowired private ProductDAO dao;
	
	private String saveDirectory = &quot;C:\\upload&quot;;
	
	public ProductService() {
		
		File dir = new File(saveDirectory);
		if(dir.exists() == false) {
			dir.mkdirs();
		}
	}
	
	public List&amp;lt;ProductDTO&amp;gt; getList() {
		return dao.selectList();
	}

	public int add(ProductDTO dto) {
		String originalFileName1 = dto.getUpload().getOriginalFilename();
		
		//	파일 확장자만 출력하기 		마지막 .의 위치부터 끝까지 잘라냄 
		String ext1 = originalFileName1.substring(originalFileName1.lastIndexOf(&quot;.&quot;));
		
		//	새로 저장될 이름은 중복되지 않도록 UUID 를 사용
		String storedFileName1 = UUID.randomUUID().toString().replace(&quot;-&quot;, &quot;&quot;);
		storedFileName1 += ext1;
		
		File f1 = new File(saveDirectory, storedFileName1);
		
		try {
			dto.getUpload().transferTo(f1);		
			
		} catch (IllegalStateException | IOException e) {
			e.printStackTrace();
		}
		
		dto.setImg(originalFileName1);
		dto.setSave_img(storedFileName1);

		
		return dao.insertFile(dto);
	}

	public int update(ProductDTO dto) {
		return dao.update(dto);
	}

	public ProductDTO getIdx(int idx) {
		return dao.SelectOne(idx);
	}

	public int delete(int idx) {
		return dao.delete(idx);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ProductDAO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728479682731&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

import com.itbank.model.ProductDTO;

public interface ProductDAO {

	
	@Select(&quot;select * from product1 order by idx desc&quot;)
	List&amp;lt;ProductDTO&amp;gt; selectList();

	@Insert(&quot;insert into product1(name, img, price, save_img) values(#{name}, #{img}, #{price}, #{save_img})&quot;)
	int insertFile(ProductDTO dto);

	@Update(&quot;update product1 set count = #{count} where idx = #{idx}&quot;)
	int update(ProductDTO dto);

	@Select(&quot;select * from product1 where idx = #{idx}&quot;)
	ProductDTO SelectOne(int idx);

	@Delete(&quot;delete from product1 where idx = #{idx}&quot;)
	int delete(int idx);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;b&gt;list.jsp&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 상품 목록&lt;/p&gt;
&lt;pre id=&quot;code_1728479711125&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ include file=&quot;../header.jsp&quot; %&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;상품 목록&amp;lt;/h1&amp;gt;

&amp;lt;div class=&quot;frame productList&quot;&amp;gt;
&amp;lt;c:forEach var=&quot;dto&quot; items=&quot;${list }&quot;&amp;gt;
	&amp;lt;div class=&quot;flex box&quot;&amp;gt;
		&amp;lt;img src=&quot;${cpath }/upload/${dto.save_img}&quot; height=&quot;250px&quot;&amp;gt;
		&amp;lt;p class=&quot;bold&quot;&amp;gt;${dto.name }&amp;lt;/p&amp;gt;
		&amp;lt;p class=&quot;sp&quot;&amp;gt;${dto.price }&amp;lt;/p&amp;gt;
		&amp;lt;p class=&quot;sp&quot;&amp;gt;${dto.count }&amp;lt;/p&amp;gt;
		&amp;lt;p&amp;gt;&amp;lt;a href=&quot;${cpath }/product/update/${dto.idx}&quot;&amp;gt;&amp;lt;button&amp;gt;수량 변경&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
		&amp;lt;p&amp;gt;&amp;lt;a href=&quot;${cpath }/product/delete/${dto.idx}&quot;&amp;gt;&amp;lt;button&amp;gt;삭제&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
		
	&amp;lt;/div&amp;gt;
&amp;lt;/c:forEach&amp;gt;
&amp;lt;/div&amp;gt;


&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;&lt;b&gt;코드 풀이&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;ProductController 를 보면, ModelAndView 객체가 list 라는 이름으로 저장됨.&lt;/b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;mav.addObject(&quot;list&quot;, list)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이때, list 는 ProductService 와 ProductDAO 를 거쳐서 sql 문을 만나게 되고, 그의 결과가 list로 넘어온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;select * from product1 order by idx desc&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;list 라는 mav 객체는 product/list.jsp 에서 EL태그를 이용하여 사용할 수 있으며,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이를 반복문으로 출력한다면&amp;nbsp;&lt;/b&gt;&lt;b&gt;모든 목록이 나오게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;&amp;lt;c:forEach&amp;nbsp;var=&quot;dto&quot;&amp;nbsp;items=&quot;${list&amp;nbsp;}&quot;&amp;gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;add.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 등록&lt;/p&gt;
&lt;pre id=&quot;code_1728479782253&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ include file=&quot;../header.jsp&quot; %&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;상품 추가&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;form method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;name&quot; placeholder=&quot;상품명&quot; required autofocus&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;file&quot; name=&quot;upload&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;number&quot; name=&quot;price&quot; placeholder=&quot;가격&quot; required&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;submit&quot; value=&quot;상품 등록&quot;&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;/form&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;&lt;b&gt;코드 풀이&amp;nbsp;&lt;/b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;form 을 제출하면, method POST로 인하여, ProductController 에 있는 PostMapping이 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; @PostMapping(&quot;/add&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이미지도 추가했다면, ProductService 에 있는 add() 함수에 의하여, 원본 파일명의 확장자만 떼어내서 새로운 변수에 저장하고,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; String&amp;nbsp;ext1&amp;nbsp;=&amp;nbsp;originalFileName1.substring(originalFileName1.lastIndexOf(&quot;.&quot;));&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UUID 를 이용한 새로운 파일명을 만들어내서 확장자를 더한다. (파일명 중복을 피하기 위함)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; String&amp;nbsp;storedFileName1&amp;nbsp;=&amp;nbsp;UUID.randomUUID().toString().replace(&quot;-&quot;,&amp;nbsp;&quot;&quot;);&lt;br /&gt;storedFileName1&amp;nbsp;+=&amp;nbsp;ext1;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;미리 선언한 saveDirectory 를 이용하여, 해당 디렉토리에 바뀐 파일명으로 생성한 이미지를 저장한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; File&amp;nbsp;f1&amp;nbsp;=&amp;nbsp;new&amp;nbsp;File(saveDirectory,&amp;nbsp;storedFileName1);&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;dto.getUpload().transferTo(f1);&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DB의 내용도 변경하기 위해 setter 를 이용한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(DB를 통해서 원본파일명과 바뀐 파일명을 알수 있음)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; dto.setImg(originalFileName1);&lt;br /&gt;dto.setSave_img(storedFileName1);&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ProductDAO 를 거쳐서 sql문을 통하여, insert 가 이루어진다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; @Insert(&quot;insert&amp;nbsp;into&amp;nbsp;product1(name,&amp;nbsp;img,&amp;nbsp;price,&amp;nbsp;save_img)&amp;nbsp;values(#{name},&amp;nbsp;#{img},&amp;nbsp;#{price},&amp;nbsp;#{save_img})&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;insert 의 결과를 콘솔창을 통해 확인할 수 있으며,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;System.out.println(row != 0 ? &quot;상품 등록 성공&quot; : &quot;상품 등록 실패&quot;);&lt;b&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;성공여부와 관계없이 redirect 를 이용하여 list 페이지로 이동된다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; return &quot;redirect:/product/list&quot;;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;update.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 수량 수정&lt;/p&gt;
&lt;pre id=&quot;code_1728479853268&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ include file=&quot;../header.jsp&quot; %&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;상품 수량 수정&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;form method=&quot;POST&quot;&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;name&quot; value=&quot;${dto.name }&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;number&quot; min=&quot;0&quot; max=&quot;500&quot; step=&quot;1&quot; name=&quot;count&quot; value=&quot;${dto.count }&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;submit&quot; value=&quot;수정&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;input type=&quot;hidden&quot; name=&quot;idx&quot; value=&quot;${dto.idx }&quot;&amp;gt;

&amp;lt;/form&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;코드 풀이&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;list 에서 수량변경 버튼을 누르면 해당 상품의 idx 를 포함한채로 update.jsp 로 넘어오게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; @GetMapping(&quot;/update/{idx}&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;form을 제출하면 ProductController 에 있는 PostMapping 이 실행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; @PostMapping(&quot;/update/{idx}&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ProductDAO 를 거쳐서 sql문이 수행된다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;@Update(&quot;update product1 set count = #{count} where idx = #{idx}&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sql 문 실행결과를 콘솔창에 띄우고,&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;System.out.println(row != 0 ? &quot;수정 성공&quot; : &quot;수정 실패&quot;);&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실행결과와는 상관없이 redirect 를 이용하여 list 페이지로 이동.&lt;/b&gt;&lt;br /&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; return &quot;redirect:/product/list&quot;;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;delete.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 삭제&lt;/p&gt;
&lt;pre id=&quot;code_1728479934005&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ include file=&quot;../header.jsp&quot; %&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 보여줄 필요가 없기 때문에 아무 코드도 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;코드 풀이&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;list.jsp 에서 상품삭제 버튼을 누르면 상품의 idx 와 함께 delete.jsp로 넘어온다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp; &amp;lt;p&amp;gt;&amp;lt;a&amp;nbsp;href=&quot;${cpath&amp;nbsp;}/product/delete/${dto.idx}&quot;&amp;gt;&amp;lt;button&amp;gt;삭제&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Controller , Service , DAO 를 거쳐서 sql 문을 만난다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; @Delete(&quot;delete&amp;nbsp;from&amp;nbsp;product1&amp;nbsp;where&amp;nbsp;idx&amp;nbsp;=&amp;nbsp;#{idx}&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;sql문의 결과를 콘솔창에 띄운다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;System.out.println(row != 0 ? &quot;삭제 성공&quot; : &quot;삭제 실패&quot;);&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결과와는 상관없이 redirect 를 이용하여 list 로 이동.&lt;/b&gt;&lt;br /&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;return &quot;redirect:/product/list&quot;;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;글을 작성하며 다시 코드들을 살펴보니 몇 가지 수정하고 싶은 부분이 존재했다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저, delete.jsp에서 아무 코드도 작성하지 않고 그냥 둘거면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;차라리 alert창이라도 띄우게 해서 사용자가 이에 대한 요청을 확인할 수 있게 했으면 싶었다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 데이터 수정, 추가, 삭제 후 결과와 상관없이 바로 list.jsp로 이동 시킨 것도 조금 아쉬웠다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자의 관점으로 봤을때는 이런 사소한 부분이 조금 불편하게 느껴질 것 같았고, 다음에는 좀 더&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 사소한 부분도 사용자의 관점에서 볼 수 있도록 신경써야 할 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 01.28. 12:32';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>oracle</category>
      <category>POS</category>
      <category>product</category>
      <category>Spring</category>
      <category>SQL</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/45</guid>
      <comments>https://cases.tistory.com/entry/Spring-%EB%A7%A4%EC%9E%A5-%ED%8F%AC%EC%8A%A4%EA%B8%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C#entry45comment</comments>
      <pubDate>Wed, 9 Oct 2024 22:25:23 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] RestController</title>
      <link>https://cases.tistory.com/entry/Spring-RestController</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;json과 ajax가 무엇인지 알아보자. 또한, @RestController를 사용해 보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;JSON : JavaScript Object Notation&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;자바스크립트 토대로 개발되었으며, 여러 프로그래밍언어에도 사용할 수 있는 독립형 언어이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;데이터 객체의 형태는 속성 - 값 쌍 형태.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;주로, 웹 브라우저와 웹 서버간 비동기 통신, 데이터 교환 등에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;home.jsp&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728478431618&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
	table {
		border : 2px solid black;
		border-collapse: collapse;
	}
	td {
		padding: 5px 10px;
		border: 1px solid grey;
	}
&amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;day10&amp;lt;/h1&amp;gt;

&amp;lt;hr&amp;gt;


&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;ex01&quot;&amp;gt;ex01 - ajax(1) 정수반환&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;ex02&quot;&amp;gt;ex02 - ajax(2) 문자열 반환&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;ex03&quot;&amp;gt;ex03 - ajax(3) map 반환&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;a href=&quot;ex04&quot;&amp;gt;ex04 - ajax(4) List&amp;amp;lt; DTO 반환(DB 이용 O)&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;



&amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
	let url = &quot;https://apis.data.go.kr/6260000/BusanRainfalldepthInfoService/getRainfallInfo&quot;;
	
	const param = {
			serviceKey : 'fPywZzdX80yEsvW3nt%2F4DyY9NZVrqZDUOFBN5Kuw7UkhbMsuXNXY%2FzsT4iAZm6Z1ILayJYsElZPCr4JWUooiQg%3D%3D',
			pageNo : '1',
			numOfRows : '25',
			resultType: 'json',
	}
	
	url += '?'
	
	for(let key in param) {
		url += key + '=' + param[key] + '&amp;amp;'
	}
	
	
	fetch(url)				//	지정한 요청 주소로 보낸다
		.then(resp =&amp;gt; resp.json())	//	요청 이후 돌아오는 응답을 JSON 객체로 변환한다
		.then(json =&amp;gt; {			//	변환된 JSON 객체를 이용하여, { } 블럭 내부 코드를 수행한다 
		//	console.log(json.getRainfallInfo.body.items.item)
	
		const arr = json.getRainfallInfo.body.items.item.map(e =&amp;gt; {
			const ob = {}
			ob.clientName = e.clientName
			ob.level6 = e.level6
			return ob
		})
		//	console.log(arr)
		const tr1 = document.createElement('tr')
		const tr2 = document.createElement('tr')
	
		
		for(let i in arr) {
			const td1 = document.createElement('td')
			td1.innerText = arr[i].clientName
			tr1.appendChild(td1)
			
			const td2 = document.createElement('td')
			td2.innerText = arr[i].level6
			tr2.appendChild(td2)
		}
		
		const table = document.createElement('table')
		table.appendChild(tr1)
		table.appendChild(tr2)
		
		const root = document.getElementById('root')
		root.appendChild(table)
		
	})

&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Ex01Controller&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728478466153&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Ex01Controller {

	@GetMapping(&quot;/ex01&quot;)
	public void ex01() {}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;ex01.jsp&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 정수 덧셈의 결과를 보여 줌.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 사용된 url 은 ex01Ajax 를 이용하는데, 이것은 AjaxController 에 GetMapping 으로 선언되어있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;const url = '${cpath}/ex01Ajax?n1=' + n1 + '&amp;amp;n2=' + n2&lt;/p&gt;
&lt;pre id=&quot;code_1728478499805&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;ex01 - 두 정수의 덧셈&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;
	&amp;lt;input type=&quot;number&quot; name=&quot;n1&quot; min=&quot;0&quot; max=&quot;20&quot; placeholder=&quot;n1&quot;&amp;gt;
	+
	&amp;lt;input type=&quot;number&quot; name=&quot;n2&quot; min=&quot;0&quot; max=&quot;20&quot; placeholder=&quot;n2&quot;&amp;gt;
	&amp;lt;button id=&quot;btn&quot;&amp;gt;=&amp;lt;/button&amp;gt;
	&amp;lt;span id=&quot;result&quot;&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;/p&amp;gt;

&amp;lt;script&amp;gt;
	const btn = document.getElementById('btn')
	const clickHandler = function() {
		const n1 = document.querySelector('input[name=&quot;n1&quot;]').value
		const n2 = document.querySelector('input[name=&quot;n2&quot;]').value
		const url = '${cpath}/ex01Ajax?n1=' + n1 + '&amp;amp;n2=' + n2
				
		fetch(url)
			.then(resp =&amp;gt; resp.text())
			.then(text =&amp;gt; {
				//	실행할 내용 (문서 요소에 응답 내용을 반환한다)
				const result = document.getElementById('result')
				result.innerText = text
			})
	}
	btn.onclick = clickHandler

&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;AjaxController&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;: @RestController 이용&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Ajax 이용한 코드들은 @RestController 선언한&amp;nbsp;&amp;nbsp;Controller에 모아두어야 한다.&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@RestController&lt;br /&gt;: 문서 내용을 응답하지 않고, 순수 데이터를 응답하기 위한 AJAX 전용 컨트롤러&lt;br /&gt;&lt;/b&gt;모든 함수에 자동으로 @ResponseBody 가 적용된다 (포워드 및 리다이렉트가 기본값이 아님)&lt;br /&gt;포워딩을 하지 않는다. 요청 받은 그대로를 전달한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728478562596&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.itbank.model.MemberDTO;
import com.itbank.service.MemberService;




@RestController
public class AjaxController {

	
	@Autowired private MemberService service;
	
	
	//	AJAX 요청에 대응하는 컨트롤러 함수는 기본 자료형 배열, 리스트, 맵, DTO 등 여러타입을 반환할 수 있다
	//	반환값이 viewName 이 아닌, 응답 그 자체임을 명시해야한다 
	@GetMapping(&quot;/ex01Ajax&quot;)
	@ResponseBody
	public int ex01Ajax(int n1, int n2) {
		return n1 + n2;
	}
	
	
	
	@PostMapping(&quot;/ex03Ajax&quot;)
	@ResponseBody		//	@ResponseBody : 반환하는 내용 그 자체가 응답이다
	public HashMap&amp;lt;String, Object&amp;gt; ex03Ajax(@RequestBody HashMap&amp;lt;String, Object&amp;gt; param) {	//	@RequestBody : 요청에 담긴 내용이 파라미터로 그대로 사용
																							//	즉, 요청 그 자체가 파라미터이다
		System.out.println(&quot;userid : &quot; + param.get(&quot;userid&quot;));
		HashMap&amp;lt;String, Object&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
		int count = service.checkDuplicate(param);
		map.put(&quot;count&quot;, count);
		map.put(&quot;msg&quot;, count != 0 ? &quot;이미 사용중인 아이디 입니다&quot; : &quot;사용가능한 아이디 입니다&quot;);
		
//		String result = objectMapper.writeValueAsString(map);
//		원래는, 자바 객체를 JSON 규칙에 맞춰 문자열로 변환한 후에 반환해야하지만,
//		ObjectMapper 가 자동으로 변환을 처리함 
		
		return map;
	}
	
	
	@GetMapping(&quot;/ex04Ajax&quot;)
	public List&amp;lt;MemberDTO&amp;gt; ex04Ajax() {
		return service.getMemberList();
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Ex03Controller&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;objectMapper&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 얘가 자동으로 mapping해서 변환 해주고 있다 생각하면 됨 ( jackson-databind )&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;objectMapper 를 이용하면 JSON 데이터를 java객체로 변환하거나&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;java 객체를 JSON 형식으로 변환할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1728478626767&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
public class Ex03Controller {

	private ObjectMapper objectMapper = new ObjectMapper();		
    
	@GetMapping(&quot;/ex03&quot;)
	public void ex03() {}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ex03.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 회원가입 중복체크&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;&lt;b&gt;&amp;nbsp;유의할 점&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바스크립트에서는 -(대시) 를 사용하지 않기 때문에&amp;nbsp; 따옴표(&lt;b&gt;''&lt;/b&gt;)로 묶어서 처리 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; 'Content-Type'&amp;nbsp;:&amp;nbsp;'application/json;&amp;nbsp;charset=utf-8'&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ result 요소 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;userid가 중복이 아니라면, 파란색 글자로 결과표시&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;userid 가 중복이라면, 빨강색 글자로 결과표시&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AjaxController 에 msg 로 결과 텍스트&lt;/b&gt;&lt;br /&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;map.put(&quot;msg&quot;, count != 0 ? &quot;이미 사용중인 아이디 입니다&quot; : &quot;사용가능한 아이디 입니다&quot;);&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, 중복이 아니라면 '사용가능한 아이디 입니다' (파란색 글씨로)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중복이라면 '이미 사용중인 아이디 입니다' (빨간색 글씨로)&lt;/p&gt;
&lt;pre id=&quot;code_1728478695636&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;ex03 - 회원 중복 가입 체크&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;p&amp;gt;
	&amp;lt;input type=&quot;text&quot; name=&quot;userid&quot; placeholder=&quot;ID&quot; required autofocus&amp;gt;
	&amp;lt;button id=&quot;btn&quot;&amp;gt;중복 확인&amp;lt;/button&amp;gt;
&amp;lt;/p&amp;gt;

&amp;lt;h3 id=&quot;result&quot;&amp;gt;&amp;lt;/h3&amp;gt;
&amp;lt;!-- 주소창에 내용이 노출되지 않는다는 이점  --&amp;gt;
&amp;lt;script&amp;gt;

	//	1) 이벤트의 대상이 될 요소를 불러온다
	const btn = document.getElementById('btn')

	//	2) 이벤트 발생 시 실행할 함수를 정의한다
	const clickHandler = function() {
		const userid = document.querySelector('input[name=&quot;userid&quot;]').value
		const url = '${cpath}/ex03Ajax'		//	주소지정
		const opt = {				//	요청의 옵션들을 지정
				method : 'POST',	//	옵션 1) 요청 메서드는 POST
				
                //	옵션 2) POST의 전달 내용은 userid 를 포함하는 객체를 JSON 문자열 형식으로 전달
				body : JSON.stringify({	
					userid: userid
				}),					
				
                //	옵션 3) 헤더, 전송하는 데이터의 형식 및 인코딩을 지정
				headers : {				
					'Content-Type' : 'application/json; charset=utf-8'		
				},
		}
		
		fetch(url, opt)		//	요청 주소와 옵션을 함께 전달한다
			.then(resp =&amp;gt; resp.json())
			.then(json =&amp;gt; {
				console.log(json)
				const result = document.getElementById('result')
				result.innerText = json.msg
				if(json.count == 0) {
					result.style.color = 'blue'
				}
				else {
					result.style.color = 'red'
				}
			})
	}
	
	//	3) 이벤트 대상의 특정 상황 (클릭, 키, 스크롤 등)을 지정하여 이벤트 함수를 연결
	btn.onclick = clickHandler
&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ex04Controller&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728478722648&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Ex04Controller {

	@GetMapping(&quot;/ex04&quot;)
	public void ex04() {}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ex04.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 회원목록&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DOM (Document Object Model)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;: 문서의 내용을 하나하나 객체화하여, 객체의 모델관계로 해석하는 구조&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;HTML 문서 내부의 태그를 '객체'로 취급하여, 여러 객체가 'Tree' 구조로 나열되어 있는 관계로 해석한다.&lt;br /&gt;&amp;nbsp;문서를 구성하는 각 요소(element) 혹은 객체(object) 를 DOM 이라고 부르기도 함.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;document.addEventListener('DOMContentLoaded', loadHandler)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;window.onload 와 loadHandler는 역할이 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1728478769769&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;

&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;h1&amp;gt;ex04 - 회원 목록&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;div id=&quot;root&quot;&amp;gt;&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
	function loadHandler() {
		const root = document.getElementById('root')		
		const url = '${cpath}/ex04Ajax'

		fetch(url) 
			.then(resp =&amp;gt; resp.json()) 
			.then(json =&amp;gt; {
				
				const arr = json
				let tag = '&amp;lt;table border=&quot;1&quot; cellpadding=&quot;10&quot; cellspacing=&quot;0&quot;&amp;gt;'
				for(let i = 0; i &amp;lt; arr.length; i++) {	//	i : 0, 1, 2, 3 ...
					const dto = arr[i]
					
					tag += '&amp;lt;tr&amp;gt;'
					for(let key in dto) {
						if(key != 'userpw') {
						tag += '&amp;lt;td&amp;gt;' + dto[key] + '&amp;lt;/td&amp;gt;'
						}
					}
					tag += '&amp;lt;/tr&amp;gt;'
				}
				tag += '&amp;lt;/table&amp;gt;'
				root.innerHTML = tag
			})
	}
	
	document.addEventListener('DOMContentLoaded', loadHandler)
	//	window.onload == loadHandler


&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RestController와 그냥 일반적으로 쓰는 Controller를 나누는 기준은 그냥 단순히&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ajax를 처리하는 부분만 RestController에 작성해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 내가 아직 정확히 이해가 되지 않는 부분은 @ResponseBody이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseBody 어노테이션을 사용하면 http요청 body를 자바 객체로 전달 받을 수 있다는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 정확하게 잘 와닿지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 꾸준히 사용해보면서 공부를 더 해봐야 할 것 같다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 01.25. 18:20';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>requestBody</category>
      <category>responseBody</category>
      <category>RestController</category>
      <category>Spring</category>
      <category>어노테이션</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/44</guid>
      <comments>https://cases.tistory.com/entry/Spring-RestController#entry44comment</comments>
      <pubDate>Wed, 9 Oct 2024 22:04:47 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 설문 투표 및 결과 보기</title>
      <link>https://cases.tistory.com/entry/Spring-%EC%84%A4%EB%AC%B8-%ED%88%AC%ED%91%9C-%EB%B0%8F-%EA%B2%B0%EA%B3%BC-%EB%B3%B4%EA%B8%B0</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 투표를 할 수 있는 설문지를 만들어 보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SurveyDTO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728467059417&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.springframework.web.multipart.MultipartFile;

// 	TABLE : SURVEY
//	IDX     NOT NULL NUMBER        
//	TITLE   NOT NULL VARCHAR2(500) 
//	WRITER           VARCHAR2(500) 
//	OPTION1 NOT NULL VARCHAR2(500) 
//	OPTION2 NOT NULL VARCHAR2(500) 
//	IMAGE1  NOT NULL VARCHAR2(500) 
//	IMAGE2  NOT NULL VARCHAR2(500) 

public class SurveyDTO {

	private int idx;
	private String title;
	private String writer;
	private String option1;
	private String option2;
	private String image1;
	private String image2;
	
	private MultipartFile upload1;
	private MultipartFile upload2;
	
	private int responseCount;	// 설문에 참여한 인원의 수
	private int choice1Count;
	private int choice2Count;
	private double choice1Rate;
	private double choice2Rate;
	
	public double getChoice1Rate() {
		return choice1Rate;
	}
	public void setChoice1Rate(double choice1Rate) {
		this.choice1Rate = choice1Rate;
	}
	public double getChoice2Rate() {
		return choice2Rate;
	}
	public void setChoice2Rate(double choice2Rate) {
		this.choice2Rate = choice2Rate;
	}
	public int getChoice1Count() {
		return choice1Count;
	}
	public void setChoice1Count(int choice1Count) {
		this.choice1Count = choice1Count;
	}
	public int getChoice2Count() {
		return choice2Count;
	}
	public void setChoice2Count(int choice2Count) {
		this.choice2Count = choice2Count;
	}
	public int getIdx() {
		return idx;
	}
	public void setIdx(int idx) {
		this.idx = idx;
	}
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getOption1() {
		return option1;
	}
	public void setOption1(String option1) {
		this.option1 = option1;
	}
	public String getOption2() {
		return option2;
	}
	public void setOption2(String option2) {
		this.option2 = option2;
	}
	public String getImage1() {
		return image1;
	}
	public void setImage1(String image1) {
		this.image1 = image1;
	}
	public String getImage2() {
		return image2;
	}
	public void setImage2(String image2) {
		this.image2 = image2;
	}
	public MultipartFile getUpload1() {
		return upload1;
	}
	public void setUpload1(MultipartFile upload1) {
		this.upload1 = upload1;
	}
	public MultipartFile getUpload2() {
		return upload2;
	}
	public void setUpload2(MultipartFile upload2) {
		this.upload2 = upload2;
	}
	public int getResponseCount() {
		return responseCount;
	}
	public void setResponseCount(int responseCount) {
		this.responseCount = responseCount;
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SurveyController&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@RequestMapping (@GetMapping, @PostMapping)&lt;br /&gt;&lt;/b&gt;: 특정 주소, 특정 메서드로 요청을 받으면 자동으로 실행되는 이벤트 함수의 성격을 가진다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;@ExceptionHandler&lt;/b&gt;&lt;br /&gt;: 예외가 발생하면 @ExceptionHandler 어노테이션이 붙은 함수가 자동으로 실행된다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, 발생하는 예외의 타입에 따라 서로 다른 함수를 실행할 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;컨트롤러 내부에서도&amp;nbsp;&amp;nbsp;ExceptionHandler 를 작성할 수 있지만&lt;br /&gt;컨트롤러는 본래, 요청에 따른 처리를 작성하는 클래스이므로,&lt;br /&gt;별도의 클래스를 만들어서 예외만 전문적으로 처리하는 스프링 빈을 작성할 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1728467106628&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
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.servlet.ModelAndView;

import com.itbank.model.ChoiceDTO;
import com.itbank.model.SurveyDTO;
import com.itbank.service.SurveyService;

@Controller
@RequestMapping(&quot;/survey&quot;)
public class SurveyController {
	
	@Autowired private SurveyService service;
	
	@ExceptionHandler(DuplicateKeyException.class)
	public ModelAndView dupKey() {
		ModelAndView mav = new ModelAndView(&quot;alert&quot;);
		mav.addObject(&quot;msg&quot;, &quot;이미 참여한 투표입니다&quot;);
		return mav;
	}

	@GetMapping(&quot;/add&quot;)
	public void add() {}
	
	@PostMapping(&quot;/add&quot;)
	public String add(SurveyDTO dto) {
		int row = service.add(dto);
		System.out.println(row != 0 ? &quot;등록 성공&quot; : &quot;등록 실패&quot;);
		return &quot;redirect:/&quot;;
	}
	
	@GetMapping(&quot;/list&quot;)
	public ModelAndView list() {
		ModelAndView mav = new ModelAndView();
		List&amp;lt;SurveyDTO&amp;gt; list = service.getList();
		mav.addObject(&quot;list&quot;, list);
		return mav;
	}
	
	@GetMapping(&quot;/vote/{idx}&quot;)
	public ModelAndView view(@PathVariable(&quot;idx&quot;) int idx) {
		ModelAndView mav = new ModelAndView(&quot;/survey/vote&quot;);
		SurveyDTO dto = service.getSurvey(idx);
		mav.addObject(&quot;dto&quot;, dto);
		return mav;
	}
	
	@PostMapping(&quot;/vote/{idx}&quot;)
	public String view(ChoiceDTO dto) {
		int row = service.addChoice(dto);
		System.out.println(row != 0 ? &quot;응답 성공&quot; : &quot;응답 실패&quot;);
		return &quot;redirect:/survey/result/{idx}&quot;;
	}
	
	@GetMapping(&quot;/result/{idx}&quot;)
	public ModelAndView result(@PathVariable(&quot;idx&quot;) int idx) {
		ModelAndView mav = new ModelAndView(&quot;/survey/result&quot;);
		SurveyDTO dto = service.getResult(idx);
		mav.addObject(&quot;dto&quot;, dto);
		return mav;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ServeyService&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728467124395&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

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

import com.itbank.component.FileComponent;
import com.itbank.model.ChoiceDTO;
import com.itbank.model.SurveyDTO;
import com.itbank.repository.SurveyDAO;

@Service
public class SurveyService {
	
	@Autowired private SurveyDAO dao;
	@Autowired private FileComponent fileComponent;

	public int add(SurveyDTO dto) {
		String image1 = fileComponent.upload(dto.getUpload1());
		String image2 = fileComponent.upload(dto.getUpload2());
		dto.setImage1(image1);
		dto.setImage2(image2);
		return dao.insert(dto);
	}

	public List&amp;lt;SurveyDTO&amp;gt; getList() {
		return dao.selectList();
	}

	public SurveyDTO getSurvey(int idx) {
		return dao.selectOne(idx);
	}

	public int addChoice(ChoiceDTO dto) {
		return dao.insertChoice(dto);
	}

	public SurveyDTO getResult(int idx) {
		return dao.selectResult(idx);
	}

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;SurveyDAO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728467142155&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

import com.itbank.model.ChoiceDTO;
import com.itbank.model.SurveyDTO;

public interface SurveyDAO {

	int insert(SurveyDTO dto);

	List&amp;lt;SurveyDTO&amp;gt; selectList();

	SurveyDTO selectOne(int idx);

	int insertChoice(ChoiceDTO dto);

	SurveyDTO selectResult(int idx);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;survey-mapper.xml&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SurveyDAO의 상세&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쿼리문&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728467167913&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE mapper PUBLIC 
	&quot;-//mybatis.org//DTD Mapper 3.0//EN&quot; 
	&quot;https://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;&amp;gt;
	
&amp;lt;mapper namespace=&quot;com.itbank.repository.SurveyDAO&quot;&amp;gt;
	
	&amp;lt;insert id=&quot;insert&quot; parameterType=&quot;survey&quot;&amp;gt;
		insert into survey (writer, title, option1, option2, image1, image2)
			values (
				#{writer},
				#{title},
				#{option1},
				#{option2},
				#{image1},
				#{image2}
			)
	&amp;lt;/insert&amp;gt;
	
	&amp;lt;select id=&quot;selectList&quot; resultType=&quot;survey&quot;&amp;gt;
		select 
		    survey.*,
		    (select count(*) from survey_choice where survey_idx = survey.idx) as responseCount
		        from survey order by survey.idx desc
	&amp;lt;/select&amp;gt;
	
	&amp;lt;select id=&quot;selectOne&quot; parameterType=&quot;int&quot; resultType=&quot;survey&quot;&amp;gt;
		select * from survey
			where
				idx = #{idx}
	&amp;lt;/select&amp;gt;
	
	&amp;lt;insert id=&quot;insertChoice&quot; parameterType=&quot;choice&quot;&amp;gt;
		insert into survey_choice (writer, survey_idx, choice) 
			values (#{writer}, #{survey_idx}, #{choice})
	&amp;lt;/insert&amp;gt;
	
	&amp;lt;select id=&quot;selectResult&quot; parameterType=&quot;int&quot; resultType=&quot;survey&quot;&amp;gt;
		select 
		    A.*, 
		    trunc(choice1Count * 100 / responseCount, 2) as choice1Rate, 
		    trunc(choice2Count * 100 / responseCount, 2) as choice2Rate
		    from (
		    select 
		    	S.*,
		        (select count(*) from survey_choice where survey_idx = #{idx}) as responseCount,
		        (select count(*) from survey_choice where survey_idx = #{idx} and choice = 1) as choice1Count,    
		        (select count(*) from survey_choice where survey_idx = #{idx} and choice = 2) as choice2Count
		            from survey S
		            where S.idx = #{idx}
		    ) A
	&amp;lt;/select&amp;gt;
	
&amp;lt;/mapper&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;add.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설문조사 추가 , FileComponent 를 이용하여 이미지 업로드도 가능하도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1728467193042&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ include file=&quot;../header.jsp&quot; %&amp;gt;
&amp;lt;style&amp;gt;
	input[type=&quot;text&quot;] {
		all: unset;
		border-bottom: 1px solid #dadada;
		padding: 10px 0;
		margin: 10px auto;
	}
	input[name=&quot;title&quot;] {
		font-size: 24px;	
		width: 500px;
	}
	div.flex {
		display: flex;
		justify-content: space-around;
		width: 800px;
		margin: 20px auto;
	}
	form div.flex &amp;gt; div {
		box-shadow: 5px 5px 5px grey;
		background-color: #eee;
		padding: 20px;
	}
	form &amp;gt; *:not(.flex) {
		display: flex;
		justify-content: center;
	} 
	
&amp;lt;/style&amp;gt;
&amp;lt;h3&amp;gt;추가&amp;lt;/h3&amp;gt;

&amp;lt;form method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
	&amp;lt;div&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;title&quot; placeholder=&quot;설문 제목&quot; required autofocus&amp;gt;&amp;lt;/div&amp;gt;
	&amp;lt;div class=&quot;flex&quot;&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;div&amp;gt;&amp;lt;input type=&quot;file&quot; name=&quot;upload1&quot; required&amp;gt;&amp;lt;/div&amp;gt;
			&amp;lt;div&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;option1&quot; placeholder=&quot;문항1&quot; required&amp;gt;&amp;lt;/div&amp;gt;
		&amp;lt;/div&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;div&amp;gt;&amp;lt;input type=&quot;file&quot; name=&quot;upload2&quot; required&amp;gt;&amp;lt;/div&amp;gt;
			&amp;lt;div&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;option2&quot; placeholder=&quot;문항2&quot; required&amp;gt;&amp;lt;/div&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;input type=&quot;hidden&quot; name=&quot;writer&quot; value=&quot;${login.userid }&quot; readonly&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;submit&quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;list.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설문 리스트&lt;/p&gt;
&lt;pre id=&quot;code_1728467220457&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
	table.surveyList {
		border: 1px solid black;
		border-collapse: collapse;
		width: 800px;
		margin: 20px auto;
	}
	td, th {
		padding: 5px 10px;
	}
	tr {
		border-bottom: 1px solid grey;
	}
	table tr:first-child {
		background-color: #dadada;
	}
	.sb {
		display: flex;
		justify-content: space-between;
	}
&amp;lt;/style&amp;gt;

&amp;lt;h3&amp;gt;설문 목록&amp;lt;/h3&amp;gt;

&amp;lt;table class=&quot;surveyList&quot;&amp;gt;
	&amp;lt;tr&amp;gt;
		&amp;lt;th&amp;gt;번호&amp;lt;/th&amp;gt;
		&amp;lt;th&amp;gt;제목&amp;lt;/th&amp;gt;
		&amp;lt;th&amp;gt;작성자&amp;lt;/th&amp;gt;
		&amp;lt;th&amp;gt;참여인원&amp;lt;/th&amp;gt;
	&amp;lt;/tr&amp;gt;
	&amp;lt;c:forEach var=&quot;dto&quot; items=&quot;${list }&quot;&amp;gt;
	&amp;lt;tr&amp;gt;
		&amp;lt;td&amp;gt;${dto.idx }&amp;lt;/td&amp;gt;
		&amp;lt;td&amp;gt;
			&amp;lt;div class=&quot;sb&quot;&amp;gt;
				&amp;lt;div&amp;gt;${dto.title }&amp;lt;/div&amp;gt;
				&amp;lt;div&amp;gt;
					&amp;lt;a href=&quot;${cpath }/survey/vote/${dto.idx}&quot;&amp;gt;&amp;lt;button&amp;gt;설문참여&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
					&amp;lt;a href=&quot;${cpath }/survey/result/${dto.idx}&quot;&amp;gt;&amp;lt;button&amp;gt;결과보기&amp;lt;/button&amp;gt;&amp;lt;/a&amp;gt;
				&amp;lt;/div&amp;gt;
			&amp;lt;/div&amp;gt;
		&amp;lt;/td&amp;gt;
		&amp;lt;td&amp;gt;${dto.writer }&amp;lt;/td&amp;gt;
		&amp;lt;td&amp;gt;${dto.responseCount }&amp;lt;/td&amp;gt;
	&amp;lt;/tr&amp;gt;
	&amp;lt;/c:forEach&amp;gt;
&amp;lt;/table&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;vote.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설문 참여&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;input radio 와 value 를 이용하여, choice 값을 선택할 수 있도록 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설문 참여를 하면, 결과값이 insert 되어야한다.&lt;/p&gt;
&lt;pre id=&quot;code_1728467250367&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;style&amp;gt;
	div.flex {
		display: flex;
		justify-content: space-around;
		width: 800px;
		margin: 20px auto;
	}
	form div.flex &amp;gt; div {
		box-shadow: 5px 5px 5px grey;
		background-color: #eee;
		padding: 20px;
		border: 3px solid transparent; 
	}
	form &amp;gt; *:not(.flex) {
		display: flex;
		justify-content: center;
	} 
	form div.flex &amp;gt; div.selected {
		border: 3px solid lime; 
	}
	input[type=&quot;radio&quot;] {
		display: none;
	}
&amp;lt;/style&amp;gt;

&amp;lt;h3&amp;gt;${dto.title }&amp;lt;/h3&amp;gt;

&amp;lt;form method=&quot;POST&quot;&amp;gt;
	&amp;lt;input type=&quot;hidden&quot; name=&quot;writer&quot; value=&quot;${login.userid }&quot;&amp;gt;
	&amp;lt;input type=&quot;hidden&quot; name=&quot;survey_idx&quot; value=&quot;${dto.idx }&quot;&amp;gt;
	&amp;lt;div class=&quot;flex&quot;&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;h4&amp;gt;${dto.option1 }&amp;lt;/h4&amp;gt;
			&amp;lt;label&amp;gt;
				&amp;lt;img src=&quot;${cpath }/upload/${dto.image1}&quot; height=&quot;200&quot;&amp;gt;
				&amp;lt;input type=&quot;radio&quot; name=&quot;choice&quot; value=&quot;1&quot;&amp;gt;
			&amp;lt;/label&amp;gt;
		&amp;lt;/div&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;h4&amp;gt;${dto.option2 }&amp;lt;/h4&amp;gt;
			&amp;lt;label&amp;gt;
				&amp;lt;img src=&quot;${cpath }/upload/${dto.image2}&quot; height=&quot;200&quot;&amp;gt;
				&amp;lt;input type=&quot;radio&quot; name=&quot;choice&quot; value=&quot;2&quot;&amp;gt;
			&amp;lt;/label&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;
	&amp;lt;p align=&quot;center&quot;&amp;gt;&amp;lt;input type=&quot;submit&quot;&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;script&amp;gt;
	const itemList = document.querySelectorAll('.flex &amp;gt; div')

	itemList.forEach(item =&amp;gt; item.onclick = function() {
		itemList.forEach(e =&amp;gt; e.classList.remove('selected'))
		item.classList.add('selected')
	})
&amp;lt;/script&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;result.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설문 결과&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[&amp;nbsp; choiceCount 구하는 쿼리문 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;select count(*) from survey_choice where survey_idx = #{idx} and choice =&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;1&lt;/span&gt;) as choice&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;1&lt;/span&gt;Count&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ choiceRate 구하는 쿼리문 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &amp;nbsp;trunc(choice&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;1&lt;/span&gt;Count * 100 / responseCount, 2) as choice&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;1&lt;/span&gt;Rate&lt;/p&gt;
&lt;pre id=&quot;code_1728467289150&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;%@ include file=&quot;../header.jsp&quot; %&amp;gt;

&amp;lt;style&amp;gt;
	div.flex {
		display: flex;
		justify-content: space-around;
		width: 800px;
		margin: 20px auto;
	}
&amp;lt;/style&amp;gt;

&amp;lt;h3&amp;gt;${dto.title }&amp;lt;/h3&amp;gt;

&amp;lt;div class=&quot;flex&quot;&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;h4&amp;gt;${dto.option1 }&amp;lt;/h4&amp;gt;
			&amp;lt;img src=&quot;${cpath }/upload/${dto.image1}&quot; height=&quot;200&quot;&amp;gt;
			&amp;lt;div&amp;gt;득표 수 : ${dto.choice1Count } (${dto.choice1Rate }%)&amp;lt;/div&amp;gt;
		&amp;lt;/div&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;h4&amp;gt;${dto.option2 }&amp;lt;/h4&amp;gt;
			&amp;lt;img src=&quot;${cpath }/upload/${dto.image2}&quot; height=&quot;200&quot;&amp;gt;
			&amp;lt;div&amp;gt;득표 수 : ${dto.choice2Count } (${dto.choice2Rate }%)&amp;lt;/div&amp;gt;
		&amp;lt;/div&amp;gt;
	&amp;lt;/div&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExceptionHandler가 초면이라 너무 생소하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고보니까 JdbcTemplate 가 있어서 사용할 수 있는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비율을 구하는 쿼리문을 작성하는 것이 조금 어려웠다.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 01.22. 23:15';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>Spring</category>
      <category>설문</category>
      <category>설문조사</category>
      <category>투표</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/43</guid>
      <comments>https://cases.tistory.com/entry/Spring-%EC%84%A4%EB%AC%B8-%ED%88%AC%ED%91%9C-%EB%B0%8F-%EA%B2%B0%EA%B3%BC-%EB%B3%B4%EA%B8%B0#entry43comment</comments>
      <pubDate>Wed, 9 Oct 2024 18:50:55 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] FileComponent</title>
      <link>https://cases.tistory.com/entry/Spring-FileComponent</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번 HashComponent에 이어, FileComponent를 이용하여 다중 파일 업로드를 구현 해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;FileComponent&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;유의할 점&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;@component 어노테이션 붙이고 나서 이 클래스에 s 가 붙는지 꼭 확인하자&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 스프링빈으로 등록되었는지 확인&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;saveDirectory 지정해두기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1728466523407&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.io.File;
import java.io.IOException;
import java.util.UUID;

import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

@Component
public class FileComponent {
	
	private String saveDirectory = &quot;C:\\upload&quot;;
	
	public FileComponent() {
		File f = new File(saveDirectory);
		if(f.exists() == false) {
			f.mkdirs();
		}
	}
	
	public String upload(MultipartFile f) {
		String originalFileName = f.getOriginalFilename();
		String storedFileName = UUID.randomUUID().toString().replace(&quot;-&quot;, &quot;&quot;);
		String ext = originalFileName.substring(originalFileName.lastIndexOf(&quot;.&quot;));
		storedFileName += ext;
		
		File dest = new File(saveDirectory, storedFileName);
		
		try {
			f.transferTo(dest);
		} catch (IllegalStateException | IOException e) {
			e.printStackTrace();  
		}
		return storedFileName;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ex04Controller&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;b&gt;중첩 커맨드 객체&lt;/b&gt;&lt;br /&gt;&lt;/b&gt;form 에서 같은 name 을 가지는 input 이 여러개 넘어올때,&lt;br /&gt;dto(= 커맨트 객체) 내부에 필드가 List 타입이라면&lt;br /&gt;같은 이름의 여러값을 리스트 형태로 자동으로 받는다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이러한 형식을 중첩 커맨드 객체라고 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;중첩 커맨드 객체는 checkbox 를 List&amp;lt;String&amp;gt; 으로 받을 때도 사용할 수 있다.&lt;br /&gt;단, dto 없이 (== 커맨드 객체를 사용하지 않고) 매개변수에 List를 지정하면 처리할 수 없다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;요청 처리가 완료되고 나면 redirect 를 이용하여 ex04.jsp로 보냄.&lt;/p&gt;
&lt;pre id=&quot;code_1728466648607&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import com.itbank.model.Upload2DTO;
import com.itbank.service.Ex04Service;

@Controller
@RequestMapping(&quot;/ex04&quot;)
public class Ex04Controller {

	
	@Autowired private Ex04Service service;
	
	
	@GetMapping
	public void ex04() {}
	
	
	@PostMapping
	public String ex04(Upload2DTO dto) {
		List&amp;lt;MultipartFile&amp;gt; list = dto.getUpload(); // 중첩 커맨드 객체
		
		System.out.println(&quot;파일의 개수 : &quot; + list.size());
		System.out.println(&quot;각 파일의 이름&quot;);
		System.out.println(&quot;===================================&quot;);
		list.forEach(f -&amp;gt; System.out.println(f.getOriginalFilename()));
		System.out.println(&quot;===================================&quot;);
		
		int row = service.uploadMultiple(dto);
		
		System.out.println(&quot;등록된 레코드 개수 : &quot; + row);
				
		return &quot;redirect:/ex04&quot;;
	}	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ex04Service&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;원본 파일명과 저장할 파일명을 다르게 구분하고 set(저장)&lt;/p&gt;
&lt;pre id=&quot;code_1728466674730&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.List;

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

import com.itbank.component.FileComponent;
import com.itbank.model.Upload2DTO;
import com.itbank.repository.Ex04DAO;

@Service
public class Ex04Service {

	@Autowired private FileComponent fc;
	@Autowired private Ex04DAO dao;
	
	
	public int uploadMultiple(Upload2DTO dto) {
		int row = 0;
		
		List&amp;lt;MultipartFile&amp;gt; list = dto.getUpload();
		String result = &quot;&quot;;
		String ori = &quot;&quot;;
		
		for(MultipartFile f : list) {
			ori += f.getOriginalFilename() + &quot; : &quot;;
			result += fc.upload(f) + &quot; : &quot;;
		}
		ori = ori.substring(0, ori.length() - 1);
        
        	//	마지막 콜론 없애기 
		result = result.substring(0, result.length() - 1);	
		
		dto.setOriginalFileName(ori);
		dto.setStoredFileName(result);
		
		row = dao.insertMultiple(dto);
		
		return row;
	}	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Upload2DTO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728466703587&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.sql.Date;
import java.util.List;

import org.springframework.web.multipart.MultipartFile;

//	upload2 테이블 

//	IDX              NOT NULL NUMBER(38)    
//	MEMO             NOT NULL VARCHAR2(100) 
//	ORIGINALFILENAME NOT NULL VARCHAR2(500) 
//	STOREDFILENAME   NOT NULL VARCHAR2(500) 
//	UPLOADDATE                DATE  


public class Upload2DTO {
	
	private int idx;
	private String memo;
	private String originalFileName;
	private String storedFileName;
	private Date uploadDate;
	
	private List&amp;lt;MultipartFile&amp;gt; upload;

	public int getIdx() {
		return idx;
	}

	public void setIdx(int idx) {
		this.idx = idx;
	}

	public String getMemo() {
		return memo;
	}

	public void setMemo(String memo) {
		this.memo = memo;
	}

	public String getOriginalFileName() {
		return originalFileName;
	}

	public void setOriginalFileName(String originalFileName) {
		this.originalFileName = originalFileName;
	}

	public String getStoredFileName() {
		return storedFileName;
	}

	public void setStoredFileName(String storedFileName) {
		this.storedFileName = storedFileName;
	}

	public Date getUploadDate() {
		return uploadDate;
	}

	public void setUploadDate(Date uploadDate) {
		this.uploadDate = uploadDate;
	}

	public List&amp;lt;MultipartFile&amp;gt; getUpload() {
		return upload;
	}

	public void setUpload(List&amp;lt;MultipartFile&amp;gt; upload) {
		this.upload = upload;
	}
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Ex04DAO&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1728466719353&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.apache.ibatis.annotations.Insert;

import com.itbank.model.Upload2DTO;

public interface Ex04DAO {

	@Insert(&quot;insert into upload2(memo, originalFileName, storedFileName) &quot;
			+ &quot;	values(#{memo} , #{originalFileName}, #{storedFileName})&quot;)
	int insertMultiple(Upload2DTO dto);

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;ex04.jsp&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[ 다중 파일 업로드 ]&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) enctype 은 multipart 로 되어있으면서&amp;nbsp; &amp;nbsp;2)&amp;nbsp; input file 은 multiple 로 되어있어야 한다.\&lt;/p&gt;
&lt;pre id=&quot;code_1728466764048&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;form method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
&amp;lt;input type=&quot;file&quot; name=&quot;upload&quot; multiple required&amp;gt;

&amp;lt;%@ page language=&quot;java&quot; contentType=&quot;text/html; charset=UTF-8&quot;
    pageEncoding=&quot;UTF-8&quot;%&amp;gt;
&amp;lt;%@ taglib prefix=&quot;c&quot; uri=&quot;http://java.sun.com/jsp/jstl/core&quot; %&amp;gt;
&amp;lt;&amp;lt;c:set var=&quot;cpath&quot; value=&quot;${pageContext.request.contextPath }&quot; /&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
&amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
&amp;lt;title&amp;gt;Insert title here&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;


&amp;lt;h1&amp;gt;ex04 - 다중 파일 업로드(multiple)&amp;lt;/h1&amp;gt;
&amp;lt;hr&amp;gt;

&amp;lt;form method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot;&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;file&quot; name=&quot;upload&quot; multiple required&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;text&quot; name=&quot;memo&quot; placeholder=&quot;내용 설명&quot;&amp;gt;&amp;lt;/p&amp;gt;
	&amp;lt;p&amp;gt;&amp;lt;input type=&quot;submit&quot; value=&quot;업로드&quot;&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;/form&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스프링으로 프로젝트를 진행할 때는 스프링 빈으로 등록이 잘 되어있는지 확인을 하는 것이 중요하다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;빈 등록이 제대로 되어 있지 않다면 오류가 발생할 수 있기 때문에, 위에 작성한 내용대로 확인을 한 번씩 해보자.&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 01.21. 19:46';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>filecomponent</category>
      <category>Spring</category>
      <category>SpringBean</category>
      <category>다중파일업로드</category>
      <category>파일업로드</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/42</guid>
      <comments>https://cases.tistory.com/entry/Spring-FileComponent#entry42comment</comments>
      <pubDate>Wed, 9 Oct 2024 18:41:27 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] annotaion</title>
      <link>https://cases.tistory.com/entry/Spring-annotaion</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;spring에서 자주 사용되는 어노테이션들을 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스프링 빈 등록 시 사용&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;924&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333;&quot; width=&quot;135&quot; height=&quot;23&quot;&gt;@Controller&lt;/td&gt;
&lt;td style=&quot;color: #333333;&quot; width=&quot;764&quot;&gt;지정한 클래스를 스프링 빈으로 등록하고, MVC구조의 컨트롤러 역할을 수행하도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333;&quot; height=&quot;23&quot;&gt;@Service&lt;/td&gt;
&lt;td style=&quot;color: #333333;&quot;&gt;지정한 클래스를 스프링 빈으로 등록하고, MVC구조의 서비스 역할을 수행하도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333;&quot; height=&quot;48&quot;&gt;@Repository&lt;/td&gt;
&lt;td style=&quot;color: #333333;&quot; width=&quot;764&quot;&gt;지정한 클래스를 스프링 빈으로 등록하고, MVC구조의 DAO 역할을 수행하도록 한다&lt;br /&gt;해당 클래스에서 발생하는 예외는 SQLException 형식으로 변경하여 throws 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333;&quot; height=&quot;24&quot;&gt;@Component&lt;/td&gt;
&lt;td style=&quot;color: #333333;&quot; width=&quot;764&quot;&gt;지정한 클래스를 스프링 빈으로 등록한다. 주로 특정 요소에 대한 작업을 전담하는 모듈로 구성한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333;&quot; height=&quot;24&quot;&gt;@ControllerAdvice&lt;/td&gt;
&lt;td style=&quot;color: #333333;&quot; width=&quot;764&quot;&gt;지정한 클래스를 스프링 빈으로 등록하고, 컨트롤러에서 발생하는 예외를 모아서 처리하도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333;&quot; height=&quot;24&quot;&gt;@RestController&lt;/td&gt;
&lt;td style=&quot;color: #333333;&quot; width=&quot;764&quot;&gt;지정한 클래스를 스프링 빈으로 등록하고, 컨트롤러이면서 내부 메서드는 @ResponseBody를 적용받게 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참조 및 연결&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;924&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 16.3953%;&quot; height=&quot;48&quot;&gt;@Autowired&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 83.4884%;&quot; width=&quot;764&quot;&gt;필드, setter, 생성자에 붙어서 스프링 빈 간의 의존관계를 자동으로 연결한다&lt;br /&gt;타입(자료형)을 우선으로 탐색하고, 타입이 일치하는 요소가 있다면 id로 판별할 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 16.3953%;&quot; height=&quot;24&quot;&gt;@Value&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 83.4884%;&quot; width=&quot;764&quot;&gt;특정 경로의 자원을 Resource 타입으로 받아서 파일을 참조할 수 있도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메서드 관련&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;924&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4884%;&quot; height=&quot;96&quot;&gt;@RequestMapping&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3953%;&quot; width=&quot;764&quot;&gt;value에는 요청 주소를 지정하고, method에는 요청 메서드를 지정한다&lt;br /&gt;일치하는 요청이 발생하면, 메서드를 호출한다&lt;br /&gt;컨트롤러 클래스에 지정하면, 해당 컨트롤러 하위 모든 메서드가 공통 주소를 적용받는다&lt;br /&gt;문자열만 작성하면 value를 지정하는 것이 된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4884%;&quot; height=&quot;23&quot;&gt;@GetMapping&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3953%;&quot;&gt;@RequestMapping(value=&quot;&quot;, method=RequestMethod.GET) 과 같다 (스프링 4.1.0 이상에서만 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4884%;&quot; height=&quot;23&quot;&gt;@PostMapping&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3953%;&quot;&gt;@RequestMapping(value=&quot;&quot;, method=RequestMethod.POST) 과 같다 (스프링 4.1.0 이상에서만 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파라미터 관련&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;924&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4883%;&quot; height=&quot;48&quot;&gt;@RequestParam&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3954%;&quot; width=&quot;764&quot;&gt;컨트롤러 메서드에서 파라미터를 전달받을 때 사용한다&lt;br /&gt;보통 생략가능하지만, HashMap으로 파라미터들을 받을때는 반드시 명시해야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4883%;&quot; height=&quot;23&quot;&gt;@PathVariable&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3954%;&quot;&gt;@RequestMapping 상의 특정 구간 주소를 파라미터로 치환해서 받을 수 있다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4883%;&quot; height=&quot;23&quot;&gt;@RequestBody&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3954%;&quot;&gt;파라미터 앞에 붙어서 JSON형식의 파라미터를 객체형태로 변환하여 받도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4883%;&quot; height=&quot;23&quot;&gt;@ResponseBody&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3954%;&quot;&gt;메서드에 붙어서 반환형이 viewName으로 사용되지 않고, 내용 그대로 반환하도록 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 18.4883%;&quot; height=&quot;23&quot;&gt;@CrossOrigin&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 81.3954%;&quot;&gt;컨트롤러 클래스 혹은 내부 메서드에 붙어서 CORS에 대한 정책을 설정한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;쿼리문 관련&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #ffffff; color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; width=&quot;924&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 15.9302%;&quot; height=&quot;23&quot;&gt;@Select&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 83.9535%;&quot;&gt;마이바티스 함수에서 select 쿼리를 전달받아 executeQuery()를 수행하고, 결과를 반환한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 15.9302%;&quot; height=&quot;23&quot;&gt;@Insert&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 83.9535%;&quot;&gt;마이바티스 함수에서 insert 쿼리를 전달받아 executeUpdate()를 수행하고, 정수를 반환한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 15.9302%;&quot; height=&quot;23&quot;&gt;@Update&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 83.9535%;&quot;&gt;마이바티스 함수에서 update 쿼리를 전달받아 executeUpdate()를 수행하고, 정수를 반환한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #333333; width: 15.9302%;&quot; height=&quot;23&quot;&gt;@Delete&lt;/td&gt;
&lt;td style=&quot;color: #333333; width: 83.9535%;&quot;&gt;마이바티스 함수에서 delete 쿼리를 전달받아 executeUpdate()를 수행하고, 정수를 반환한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;script&gt;
document.querySelector('#main &gt; div &gt; div &gt; div.article_header.type_article_header_cover &gt; div &gt; div &gt; p &gt; span.date').innerText = '2024. 01.20. 18:07';
&lt;/script&gt;</description>
      <category>Spring</category>
      <category>annotation</category>
      <category>getmapping</category>
      <category>PostMapping</category>
      <category>Spring</category>
      <category>어노테이션</category>
      <author>Sponge_</author>
      <guid isPermaLink="true">https://cases.tistory.com/41</guid>
      <comments>https://cases.tistory.com/entry/Spring-annotaion#entry41comment</comments>
      <pubDate>Wed, 9 Oct 2024 18:32:39 +0900</pubDate>
    </item>
  </channel>
</rss>