<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>quinton84 님의 블로그</title>
    <link>https://quinton84.tistory.com/</link>
    <description>quinton84 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Tue, 21 Jul 2026 14:35:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>quinton84</managingEditor>
    <item>
      <title>Node.js Express 앱에 Gemini AI 연동하기</title>
      <link>https://quinton84.tistory.com/7</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-7578665530606531&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&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;&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;&lt;/p&gt;
&lt;h1&gt;  오늘 배운 것: Node.js Express 앱에 Gemini AI 연동하기 학습 기록&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 웹 브라우저에서 질문을 입력하면 Gemini AI가 답변을 해주는 간단한 웹 애플리케이션을 만들고 실행하는 과정을 배우셨습니다. 이 과정을 통해 웹 개발의 기초와 AI API 연동의 핵심 개념을 익히셨습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.   Express.js 프로젝트 생성하기&lt;/h2&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;개념&lt;/b&gt;: &lt;code&gt;Express.js&lt;/code&gt;는 Node.js 환경에서 웹 애플리케이션을 더 쉽고 빠르게 만들 수 있도록 도와주는 &lt;code&gt;프레임워크(Framework)&lt;/code&gt;입니다. 미리 정해진 규칙과 도구들을 제공해서 복잡한 웹 서버 코드를 직접 다 작성할 필요 없이 효율적으로 개발할 수 있게 해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용 도구&lt;/b&gt;: &lt;code&gt;Express 제너레이터(Express Generator)&lt;/code&gt;를 사용했습니다. 이것은 Express 프로젝트의 기본 폴더와 파일들을 자동으로 만들어주는 도구입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명령어&lt;/b&gt;:
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;npx express --view=ejs express-gemini-app&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;npx&lt;/code&gt;: &lt;code&gt;Node Package Execute&lt;/code&gt;의 줄임말로, 로컬에 설치되지 않은 Node.js 패키지(여기서는 Express 제너레이터)도 바로 실행할 수 있게 해주는 편리한 도구입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;express&lt;/code&gt;: Express 제너레이터를 호출하는 명령어입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--view=ejs&lt;/code&gt;: 웹 페이지의 디자인을 담당하는 &lt;code&gt;템플릿 엔진&lt;/code&gt;으로 &lt;code&gt;EJS (Embedded JavaScript)&lt;/code&gt;를 사용하겠다고 지정하는 옵션입니다. EJS는 HTML 코드 안에 자바스크립트 코드를 넣어 동적인 웹 페이지를 만들 수 있게 해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;express-gemini-app&lt;/code&gt;: 여러분이 만들 웹 애플리케이션 프로젝트의 이름입니다. 이 이름으로 새로운 폴더가 생성되고, 그 안에 모든 프로젝트 파일들이 들어갑니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오늘 배운 점 (오류 해결)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;npm error could not determine executable to run&lt;/code&gt;&lt;/span&gt;: 이 오류는 &lt;code&gt;npx&lt;/code&gt;가 &lt;code&gt;express-generator&lt;/code&gt;를 찾지 못할 때 발생할 수 있습니다. [cite: 5]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: &lt;code&gt;npm install -g express-generator&lt;/code&gt; 명령어로 Express 제너레이터를 시스템에 &lt;code&gt;글로벌(-g)&lt;/code&gt;하게 미리 설치해주면 해결됩니다.&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;package.json&lt;/code&gt; 파일 누락&lt;/span&gt;: 프로젝트 생성 명령이 제대로 완료되지 않으면 &lt;code&gt;package.json&lt;/code&gt; 파일이 없을 수 있습니다. [cite: 5] 이 파일은 프로젝트의 설정과 필요한 부품(패키지) 목록을 담고 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: 기존에 불완전하게 생성된 프로젝트 폴더를 삭제하고, &lt;code&gt;npx express&lt;/code&gt; 명령을 다시 실행하여 프로젝트를 깔끔하게 재설정합니다.&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;'Express\express-gemini-app' 인수를 허용하는 위치 매개 변수를 찾을 수 없습니다.&lt;/code&gt;&lt;/span&gt;: &lt;code&gt;cd&lt;/code&gt; 명령어로 폴더 이동 시, 폴더 이름에 공백이 포함되어 있으면 발생하는 오류입니다. [cite: 5]&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: &lt;code&gt;cd &quot;C:\Users\김광수\Desktop\파이썬\1.4 Express\express-gemini-app&quot;&lt;/code&gt; 와 같이 &lt;b&gt;경로 전체를 큰따옴표로 묶어주면&lt;/b&gt; 공백이 포함된 경로도 정확히 인식합니다. [cite: 5]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.   프로젝트에 필요한 부품(패키지) 설치하기&lt;/h2&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;개념&lt;/b&gt;: Node.js 프로젝트는 다양한 기능을 제공하는 &lt;code&gt;패키지(Package)&lt;/code&gt;들을 사용합니다. 이 패키지들은 &lt;code&gt;npm(Node Package Manager)&lt;/code&gt;이라는 도구를 통해 설치하고 관리합니다. &lt;code&gt;package.json&lt;/code&gt; 파일에는 우리 프로젝트가 어떤 패키지들을 필요로 하는지 목록이 적혀 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명령어&lt;/b&gt;:
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 명령어는 &lt;code&gt;package.json&lt;/code&gt; 파일을 읽어서 프로젝트에 필요한 모든 패키지들을 &lt;code&gt;node_modules&lt;/code&gt;라는 폴더 안에 자동으로 다운로드하여 설치해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오늘 배운 점 (오류 해결)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;npm error code EJSONPARSE&lt;/code&gt;&lt;/span&gt;: 이 오류는 &lt;code&gt;package.json&lt;/code&gt; 파일의 &lt;code&gt;JSON&lt;/code&gt; 문법이 잘못되었을 때 발생합니다. [cite: 1] [cite_start]특히 &lt;code&gt;JSON&lt;/code&gt; 파일 안에는 주석(&lt;code&gt;//&lt;/code&gt; 또는 &lt;code&gt;/* */&lt;/code&gt;)을 사용할 수 없습니다. [cite: 1]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: &lt;code&gt;package.json&lt;/code&gt; 파일 안에 있는 모든 주석을 제거해야 합니다. [cite_start]예를 들어, &lt;code&gt;&quot;@google/generative-ai&quot;: &quot;^0.1.3&quot; // 이 부분을 추가하거나 변경합니다.&lt;/code&gt; 와 같은 줄이 있다면 &lt;code&gt;// 이 부분을 추가하거나 변경합니다.&lt;/code&gt;를 지워야 합니다. [cite: 1]&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;Error: Cannot find module 'dotenv'&lt;/code&gt;&lt;/span&gt;: &lt;code&gt;dotenv&lt;/code&gt; 모듈을 찾을 수 없다는 오류입니다. [cite: 2] &lt;code&gt;dotenv&lt;/code&gt;는 &lt;code&gt;.env&lt;/code&gt; 파일(환경 변수 파일을 만드는 용도)에 있는 중요한 정보(API 키 등)를 우리 프로그램이 읽을 수 있게 해주는 패키지입니다.&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: &lt;code&gt;npm install dotenv&lt;/code&gt; 명령어로 &lt;code&gt;dotenv&lt;/code&gt; 패키지를 설치해주면 됩니다. [cite: 2]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ⚙️ &lt;code&gt;.env&lt;/code&gt; 파일로 중요 정보(API 키) 관리하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로그램에서 사용할 중요한 정보인 Gemini API 키를 안전하게 보관하고 불러오는 방법입니다.&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;개념&lt;/b&gt;: &lt;code&gt;환경 변수(Environment Variable)&lt;/code&gt;는 프로그램이 실행되는 환경에 따라 값이 달라질 수 있는 변수입니다. API 키와 같은 민감한 정보는 코드 안에 직접 넣는 대신, &lt;code&gt;.env&lt;/code&gt; 파일이라는 별도의 파일에 저장하고 환경 변수로 불러와 사용하는 것이 일반적입니다. 이렇게 하면 코드를 다른 사람과 공유해도 API 키가 노출되지 않아 안전합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;작업&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프로젝트의 가장 바깥 폴더(&lt;code&gt;express-gemini-app&lt;/code&gt;) 안에 &lt;code&gt;.env&lt;/code&gt;라는 이름의 파일을 만듭니다. (파일 탐색기에서 직접 만들면 됩니다.)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env&lt;/code&gt; 파일을 열고 다음과 같이 Gemini API 키를 저장합니다.
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;GOOGLE_API_KEY=여러분이_발급받은_실제_Gemini_API_키&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;=&lt;/code&gt; 양쪽에 공백이 없도록 주의해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;여러분이_발급받은_실제_Gemini_API_키&lt;/code&gt;는 구글 AI 스튜디오 등에서 직접 발급받은 문자열입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4.   웹 애플리케이션 실행하고 확인하기&lt;/h2&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;명령어&lt;/b&gt;:
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;npm start&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 명령어를 실행하면 Node.js 환경에서 Express 웹 서버가 시작됩니다. 서버는 기본적으로 &lt;code&gt;3000번 포트&lt;/code&gt;에서 클라이언트의 요청을 기다립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확인&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;웹 브라우저를 열고 주소창에 &lt;code&gt;http://localhost:3000&lt;/code&gt;을 입력하여 접속합니다.&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;span class=&quot;success&quot;&gt;&lt;code&gt;Gemini Chat App&lt;/code&gt;이라는 제목과 함께 프롬프트를 입력할 수 있는 공간(&lt;code&gt;Prompt:&lt;/code&gt;)과 &lt;code&gt;Submit&lt;/code&gt; 버튼이 보이면 성공입니다.&lt;/span&gt; [cite: 3]&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오늘 배운 점 (오류 해결)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;ERR_CONNECTION_REFUSED&lt;/code&gt;&lt;/span&gt; (사이트에 연결할 수 없음): 웹 서버가 실행되지 않았거나, 방화벽 등의 이유로 연결이 거부될 때 나타납니다. [cite: 4]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;터미널에 &lt;code&gt;npm start&lt;/code&gt; 실행 후 오류 메시지가 없는지 확인&lt;/b&gt;: 서버가 제대로 시작되었는지 먼저 확인합니다.&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;b&gt;방화벽 해제 또는 규칙 추가&lt;/b&gt;: Windows &lt;code&gt;방화벽&lt;/code&gt;이 &lt;code&gt;3000번 포트&lt;/code&gt;로의 접속을 막고 있을 가능성이 큽니다. [cite: 4]
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;[cite_start]
&lt;li&gt;&lt;b&gt;임시 해제 후 테스트&lt;/b&gt;: Windows 방화벽을 임시로 &lt;code&gt;사용 안 함&lt;/code&gt;으로 설정한 후 접속을 시도하여 문제가 방화벽 때문인지 확인합니다. [cite: 4]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인바운드 규칙 추가&lt;/b&gt;: 테스트 후 방화벽을 다시 활성화하고, &lt;code&gt;인바운드 규칙&lt;/code&gt;을 추가하여 &lt;code&gt;TCP 포트 3000&lt;/code&gt;에 대한 연결을 &lt;code&gt;허용&lt;/code&gt;하도록 설정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.   Gemini AI 연동 코드 수정하기 (&lt;code&gt;routes/index.js&lt;/code&gt;)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 웹 애플리케이션의 핵심인 AI 기능을 추가하는 작업입니다. 사용자로부터 받은 질문을 Gemini AI에 보내고, 답변을 받아 웹 페이지에 표시하는 코드를 작성합니다.&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;개념&lt;/b&gt;: Express 앱에서는 &lt;code&gt;라우터(Router)&lt;/code&gt;라는 것을 사용하여 특정 웹 주소(&lt;code&gt;GET /&lt;/code&gt;, &lt;code&gt;POST /&lt;/code&gt; 등)로 들어오는 요청을 처리합니다. 우리는 &lt;code&gt;routes&lt;/code&gt; 폴더 안의 &lt;code&gt;index.js&lt;/code&gt; 파일에서 이러한 요청들을 처리하는 코드를 작성했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 코드 (핵심!)&lt;/b&gt;:
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;
var express = require('express');
var router = express.Router();

// Gemini API를 위한 Google Generative AI SDK import
const { GoogleGenerativeAI } = require('@google/generative-ai');
// dotenv 라이브러리를 사용하여 .env 파일의 환경 변수를 로드
require('dotenv').config();

// .env 파일에서 Gemini API 키 가져오기
const GEMINI_API_KEY = process.env.GOOGLE_API_KEY;

// GoogleGenerativeAI 인스턴스 초기화
// 사용 가능한 모델 이름(예: gemini-pro, gemini-2.0-flash 등)을 확인하여 지정
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);
const model = genAI.getGenerativeModel({ model: &quot;gemini-2.0-flash&quot; }); // 사용 가능한 모델로 변경

/* GET home page. */
router.get('/', function(req, res, next) {
  res.render('index', { 
    title: 'Gemini Chat App', 
    question: null, 
    result: null 
  });
});

/* POST request from form. */
router.post('/', async function(req, res, next) {
  const prompt = req.body[&quot;prompt&quot;];
  let resultText = null;

  if (prompt) {
    try {
      resultText = await accessGeminiAPI(prompt);
    } catch (error) {
      // 여기에서 상세 오류 로깅을 수행합니다.
      console.error(&quot;Gemini API 호출 중 상세 오류:&quot;, error); 
      if (error.response) { // error.response 객체가 존재하는지 확인 
        console.error(&quot;HTTP 상태 코드:&quot;, error.response.status); 
        console.error(&quot;오류 응답 데이터:&quot;, error.response.data); 
        if (error.response.data &amp;amp;&amp;amp; error.response.data.error &amp;amp;&amp;amp; error.response.data.error.message) {
          console.error(&quot;Gemini API 오류 메시지:&quot;, error.response.data.error.message);
        }
      } else if (error.message) { // 일반적인 JavaScript 에러 메시지 
        console.error(&quot;일반 오류 메시지:&quot;, error.message);
      }
      resultText = &quot;죄송합니다. AI 응답을 가져오는 중 오류가 발생했습니다. 터미널을 확인하세요.&quot;;
    }
  }

  res.render('index', { 
    title: 'Gemini Chat App', 
    question: prompt, 
    result: resultText 
  });
});

// Gemini API 접근 함수
async function accessGeminiAPI(userPrompt) {
  const chat = model.startChat({
    history: [], // 여기서는 이전 대화 없이 단일 요청으로 처리합니다.
    generationConfig: {
      maxOutputTokens: 200, // 최대 응답 토큰 수 설정
    },
  });

  const result = await chat.sendMessage(userPrompt);
  const response = await result.response;
  const text = response.text();
  return text;
}

module.exports = router;
            &lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오늘 배운 점 (오류 해결)&lt;/b&gt;:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;title is not defined&lt;/code&gt;&lt;/span&gt;: &lt;code&gt;index.ejs&lt;/code&gt; 템플릿에 &lt;code&gt;title&lt;/code&gt; 변수를 넘겨주지 않아서 발생하는 오류입니다. [cite: 5]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: &lt;code&gt;res.render('index', { title: 'Gemini Chat App', ... });&lt;/code&gt; 와 같이 &lt;code&gt;title&lt;/code&gt; 값을 &lt;code&gt;index.js&lt;/code&gt;에서 명시적으로 전달합니다.&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;API key not valid. [cite_start]Please pass a valid API key.&lt;/code&gt;&lt;/span&gt;: Gemini API 키가 유효하지 않거나 잘못 입력되었을 때 발생합니다. [cite: 6]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: Google AI Studio에서 API 키를 다시 발급받거나, &lt;code&gt;.env&lt;/code&gt; 파일에 정확하게 복사되었는지 꼼꼼히 확인합니다.&lt;/li&gt;
[cite_start]
&lt;li&gt;&lt;span class=&quot;error&quot;&gt;&lt;code&gt;Error fetching from ... [400 Bad Request]&lt;/code&gt;&lt;/span&gt;: API 호출이 유효하지 않거나, 사용하려는 모델(&lt;code&gt;gemini-pro&lt;/code&gt; 등)이 현재 API 키로 접근할 수 없는 모델일 수 있습니다. [cite: 6]&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책&lt;/b&gt;: Google AI Studio에서 현재 사용 가능한 모델 이름을 확인하고, &lt;code&gt;index.js&lt;/code&gt; 파일의 &lt;code&gt;genAI.getGenerativeModel({ model: &quot;사용_가능한_모델명&quot; });&lt;/code&gt; 부분을 정확히 수정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6.   웹 페이지 디자인 파일 수정하기 (&lt;code&gt;views/index.ejs&lt;/code&gt;)&lt;/h2&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;개념&lt;/b&gt;: &lt;code&gt;EJS 템플릿 파일(.ejs)&lt;/code&gt;은 HTML 코드와 자바스크립트 코드를 함께 사용하여 동적인 웹 페이지를 만듭니다. 서버(&lt;code&gt;index.js&lt;/code&gt;)에서 전달받은 &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;question&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt; 같은 값들을 이 파일에서 받아서 웹 페이지에 표시합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;주요 코드&lt;/b&gt;:
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&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 http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=UTF-8&quot;&amp;gt;
  &amp;lt;title&amp;gt;&amp;lt;%= title %&amp;gt;&amp;lt;/title&amp;gt;
  &amp;lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.css&quot;
        rel=&quot;stylesheet&quot; crossorigin=&quot;anonymous&quot;&amp;gt;
  &amp;lt;link rel='stylesheet' href='/stylesheets/style.css' /&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body class=&quot;container&quot;&amp;gt;
  &amp;lt;h1 class=&quot;display-6 py-2&quot;&amp;gt;&amp;lt;%= title %&amp;gt;&amp;lt;/h1&amp;gt;

  &amp;lt;form method=&quot;POST&quot; action=&quot;/&quot;&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;label for=&quot;prompt&quot;&amp;gt;Prompt:&amp;lt;/label&amp;gt;
      &amp;lt;textarea id=&quot;prompt&quot; name=&quot;prompt&quot;
                class=&quot;form-control&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;center class=&quot;py-3&quot;&amp;gt;
      &amp;lt;input type=&quot;submit&quot; value=&quot;Submit&quot;
             class=&quot;btn btn-primary&quot;&amp;gt;
    &amp;lt;/center&amp;gt;
  &amp;lt;/form&amp;gt;

  &amp;lt;% if (question != null) { %&amp;gt;
    &amp;lt;p class=&quot;border border-2 p-3 h6 mt-3&quot;&amp;gt;&amp;lt;%= question %&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;% } %&amp;gt;

  &amp;lt;% if (result != null) { %&amp;gt;
    &amp;lt;p class=&quot;border border-2 p-3 h6 mt-3&quot;&amp;gt;&amp;lt;%= result %&amp;gt;&amp;lt;/p&amp;gt;
  &amp;lt;% } %&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&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&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; 태그: 사용자가 데이터를 입력하고 서버로 전송할 수 있는 양식을 만듭니다. &lt;code&gt;method=&quot;POST&quot;&lt;/code&gt;와 &lt;code&gt;action=&quot;/&quot;&lt;/code&gt;을 통해 &lt;code&gt;index.js&lt;/code&gt;의 &lt;code&gt;router.post&lt;/code&gt; 부분으로 데이터가 전송되도록 합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;textarea id=&quot;prompt&quot; name=&quot;prompt&quot;&amp;gt;&lt;/code&gt;: 여러 줄의 텍스트를 입력할 수 있는 공간입니다. &lt;code&gt;name=&quot;prompt&quot;&lt;/code&gt; 속성은 서버에서 이 입력값을 &lt;code&gt;req.body.prompt&lt;/code&gt;로 가져올 수 있게 해줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;input type=&quot;submit&quot; value=&quot;Submit&quot;&amp;gt;&lt;/code&gt;: 양식에 입력된 데이터를 서버로 제출하는 버튼입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;%= 변수명 %&amp;gt;&lt;/code&gt;: EJS에서 서버(&lt;code&gt;index.js&lt;/code&gt;)로부터 전달받은 &lt;code&gt;변수명&lt;/code&gt;의 값을 웹 페이지에 표시할 때 사용하는 문법입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;% if (조건) { %&amp;gt; ... &amp;lt;% } %&amp;gt;&lt;/code&gt;: EJS에서 특정 &lt;code&gt;조건&lt;/code&gt;이 참일 때만 해당 HTML 코드를 표시하도록 하는 제어문입니다. 질문과 답변이 있을 때만 화면에 나타나게 하는 용도로 사용되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;success&quot;&gt;&lt;b&gt;오늘의 학습 목표를 달성하셨습니다!&lt;/b&gt;&lt;/span&gt; 이제 이 정리 내용을 티스토리에 잘 기록해 두시면 훌륭한 학습 자료가 될 것입니다. 코딩 여정을 계속 응원하겠습니다!&lt;/p&gt;</description>
      <category>dev</category>
      <category>express</category>
      <category>GeminiAPI</category>
      <category>node</category>
      <category>node.js</category>
      <category>코딩</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/7</guid>
      <comments>https://quinton84.tistory.com/7#entry7comment</comments>
      <pubDate>Fri, 11 Jul 2025 09:38:37 +0900</pubDate>
    </item>
    <item>
      <title>AI와 함께 보고용 PPT 제작기: Gemini와 skywork.ai 활용법</title>
      <link>https://quinton84.tistory.com/6</link>
      <description>&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-7578665530606531&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신입사원 교육 시즌, 교육 담당자들의 가장 큰 고민은 무엇일까요? 바로 '어떻게 하면 짧은 시간 안에 업무의 핵심을 효과적으로 전달할 수 있을까?'일 것입니다. 저희 팀 역시 같은 고민에 빠졌고, '일잘러'의 대명사 박소연 작가님의 인사이트를 빌리기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 방대한 자료였습니다. 박소연 작가님의 베스트셀러 시리즈는 물론, 수많은 유튜브 강의와 인터뷰까지. 이 모든 것을 사람이 직접 보고 20분 분량의 교육 자료로 압축하는 것은 엄청난 시간과 노력이 필요한 작업이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 저희는 이 과정을 AI와 함께하기로 결정했습니다. Gemini의 딥리서치 기능으로 핵심 교재를 만들고, skywork.ai를 통해 보고용 PPT까지 완성하는 전 과정을 공유합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1단계: Gemini 딥리서치로 교육 교재의 뼈대 세우기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 자료를 검색하는 것을 넘어, 여러 소스에 흩어져 있는 정보를 종합하고 하나의 논리적인 결과물로 만들어내는 것이 필요했습니다. 저는 Gemini에게 다음과 같은 구체적인 연구 계획을 지시했습니다.&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;&lt;/span&gt;&lt;/span&gt;&lt;span&gt; &amp;nbsp; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과물 형태 정의:&lt;/b&gt; 최종 결과물은 '신입사원을 대상으로 하는 20분 분량의 강의안' 형태로 정리해달라고 명시했습니다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;핵심 내용 강조:&lt;/b&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;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini는 이 요청에 따라 체계적이고 깊이 있는 교육 원고 초안을 신속하게 생성해주었습니다. 이 원고는 이후 PPT 제작의 튼튼한 기반이 되었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2단계: skywork.ai로 보고용 PPT 제작 및 디자인 고도화&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 단계는 텍스트 원고를 시각적인 PPT로 만드는 것이었습니다. 저희는 AI 프레젠테이션 제작 툴인 &lt;b&gt;skywork.ai&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;&lt;b&gt;데이터 시각화 강화:&lt;/b&gt; 차트/그래프용 색상 가이드라인, 명확한 표 스타일, 중요한 수치를 강조하는 방식을 정의하여 데이터의 가독성을 높였습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보고서 특화 구조 적용:&lt;/b&gt; 모든 슬라이드에 일관된 페이지 번호와 섹션 구분을 적용하고, 출처 및 각주 표기 방식을 명확히 하여 보고서의 신뢰도를 더했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;임원진 보고 수준의 간결함:&lt;/b&gt; 과도한 아이콘 사용을 제한하고, 텍스트 중심의 명료한 정보 전달에 집중하도록 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;템플릿 유형별 표준화:&lt;/b&gt; 커버, 목차, 내용, 데이터, 요약 등 페이지 유형별로 레이아웃을 표준화하여 전체적인 통일성과 전문성을 확보했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 정교한 디자인 가이드를 적용하자, skywork.ai는 단순한 PPT를 넘어 실제 보고에 바로 사용할 수 있는 수준의 결과물을 만들어냈습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;최종 결과물: 12장의 핵심 장표&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;&lt;b&gt;1. 단순함의 원칙: 일잘러를 위한 성장 가이드&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;&lt;b&gt;제목:&lt;/b&gt; 단순함의 원칙&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부제목:&lt;/b&gt; 일잘러를 위한 성장 가이드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설명:&lt;/b&gt; '무조건 열심히' 하는 **'성실함의 함정'**을 넘어, '제대로' 일하는 방법을 고민하는 구성원을 위한 핵심 가이드&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 도입: '열심히'의 함정을 넘어 '제대로' 일하기&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;&lt;b&gt;제목:&lt;/b&gt; '열심히'의 함정을 넘어 '제대로' 일하기&lt;/li&gt;
&lt;li&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&gt;조직에 대한 열정은 성장의 동력&lt;/li&gt;
&lt;li&gt;하지만, 이 열정이 **'오래 일하기', '무조건 열심히 하기'**와 같은 **'성실함의 함정'**으로 이어질 수 있음&lt;/li&gt;
&lt;li&gt;인정을 받기 위해 스스로를 몰아붙이는 경향에서 비롯됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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&gt;진정한 가치는 투입된 시간(How Long)이 아닌, **명확한 결과물(How Clear)**로 평가&lt;/li&gt;
&lt;li&gt;목표: 업무 만족도와 효율성을 극대화하여 퇴근 후 삶의 질을 높이는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. '일잘러' vs '일못러': 생각의 중심을 바꿔라&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;&lt;b&gt;제목:&lt;/b&gt; '일잘러' vs '일못러': 생각의 중심을 바꿔라&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심:&lt;/b&gt; 효과적으로 일하는 '일잘러'와 비효율적으로 일하는 '일못러'의 사고방식 비교&lt;/li&gt;
&lt;li&gt;&lt;b&gt;근본적인 차이: '생각의 중심'&lt;/b&gt;&lt;/li&gt;
&lt;li&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&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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&gt;생각의 중심: '상대방'&lt;/li&gt;
&lt;li&gt;고객, 상사, 회사의 문제와 목표에 집중&lt;/li&gt;
&lt;li&gt;**'왜(WHY)'**에서 시작하며 핵심 문제에 집중&lt;/li&gt;
&lt;li&gt;결론부터 말하고, 구체적인 수치와 명확한 질문을 활용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론:&lt;/b&gt; 관점의 차이가 업무의 모든 영역에서 다른 결과를 만들어냄&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. '일잘러'의 핵심 비결: 세 가지 마인드셋 요약&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;&lt;b&gt;제목:&lt;/b&gt; '일잘러'의 핵심 비결: 세 가지 마인드셋 요약&lt;/li&gt;
&lt;li&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&gt;의미: 게으름이나 요령 피우기와는 정반대의 개념. 불필요한 것을 과감히 걷어내고, 가장 중요한 본질에 집중하기 위한 치열한 노력의 결과물&lt;/li&gt;
&lt;li&gt;&quot;단순함은 궁극의 정교함이다&quot; - 레오나르도 다빈치&lt;/li&gt;
&lt;li&gt;&quot;단순하게 만들기 위해 엄청나게 노력해야 한다&quot; - 스티브 잡스&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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&gt;**'왜(WHY)'**라는 질문으로 일의 뼈대를 세워라&lt;/li&gt;
&lt;li&gt;상대방이 해결해야 할 문제에 집중하라&lt;/li&gt;
&lt;li&gt;거대한 업무를 작은 조약돌로 쪼개라&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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&gt;결론부터 말하라 (30초의 법칙)&lt;/li&gt;
&lt;li&gt;형용사 대신 숫자로 말하라&lt;/li&gt;
&lt;li&gt;'당연함'이라는 착각을 버리고 전부 확인하라&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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&gt;'좋은 사람'이 아닌 **'좋은 동료'**를 지향하라&lt;/li&gt;
&lt;li&gt;프로페셔널한 거리를 유지하라&lt;/li&gt;
&lt;li&gt;당신의 경계선을 명확하게 알려라&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론:&lt;/b&gt; 이것이 바로 최고의 성과를 내는 사람들, 소위 '일잘러'의 핵심 비결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 원칙 1: '어떻게'가 아닌 '왜'에서 시작하라 (건축가의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 1: '어떻게'가 아닌 '왜'에서 시작하라 (건축가의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구성원이 빠지기 쉬운 함정:&lt;/b&gt; 업무 지시 시 **'어떻게(HOW)'**에 매몰되어 보고서 템플릿, PPT 디자인, 자료 수집부터 시작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'일잘러'의 접근법:&lt;/b&gt; 행동하기 전에 반드시 '왜(WHY)'를 먼저 묻습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기획의 3가지 질문:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우리의 목표(원하는 미래)는 무엇인가?&lt;/li&gt;
&lt;li&gt;그 목표를 가로막는 '진짜' 문제는 무엇인가?&lt;/li&gt;
&lt;li&gt;그 문제를 해결할 최적의 행동은 무엇인가?&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나쁜 예시 ('일못러'):&lt;/b&gt; 팀장님이 &quot;새로운 SNS 캠페인을 기획해 보세요&quot;라고 말하면, 구성원은 곧바로 어떤 콘텐츠를 만들지, 어떤 채널에 올릴지, 즉 **'어떻게'**에 대한 아이디어를 쏟아냄&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 예시 ('일잘러'):&lt;/b&gt; 구성원은 질문합니다. &quot;왜 새로운 캠페인이 필요한가요? 이번 캠페인의 핵심 목표가 브랜드 인지도 상승인가요, 특정 제품의 매출 증대인가요, 아니면 새로운 고객층 확보인가요?&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'WHY'로 시작하는 습관의 이점:&lt;/b&gt; 불안감에서 해방, 노력의 핵심 집중&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6. 원칙 2: 당신의 고객을 위해 일하라 (건축가의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 2: 당신의 고객을 위해 일하라 (건축가의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 개념:&lt;/b&gt; 모든 업무에는 '고객'이 존재 (상사, 의사결정권자 등)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사고방식 전환:&lt;/b&gt; '내가 얼마나 많이 아는가'가 아닌, '내 보고를 받는 사람이 무엇을 알아야 하는가'를 생각하는 프로의 사고방식으로 전환&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나쁜 예시 ('일못러'):&lt;/b&gt; 자신의 노력을 보여주기 위해, 찾은 모든 정보를 담아 50페이지짜리 보고서 제출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 예시 ('일잘러'):&lt;/b&gt; '상사가 이 정보를 가지고 무엇을 할 것인가?'를 생각하고, 핵심 위협과 기회 3가지를 담은 한 페이지 요약본을 먼저 제출&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커리어의 핵심:&lt;/b&gt; 다른 사람의 문제를 해결해 주는 일은 인정받고 보상받습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7. 원칙 3: 태산을 조약돌로 쪼개라 (건축가의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 3: 태산을 조약돌로 쪼개라 (건축가의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;거대한 업무의 진실:&lt;/b&gt; 아무리 거대하고 막막해 보이는 업무라도 결국은 작은 일들의 집합체일 뿐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일잘러의 문제 쪼개기 과정:&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;목표 정의 (WHY)&lt;/li&gt;
&lt;li&gt;대상 분류 (MECE)&lt;/li&gt;
&lt;li&gt;구성요소 나열 (MECE)&lt;/li&gt;
&lt;li&gt;체크리스트화&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론:&lt;/b&gt; 막막한 업무를 받으면, 당황하지 말고 쪼개고, 나누고, 목록을 만드십시오.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8. 원칙 4: 30초의 법칙 &amp;mdash; 결론부터 말하라 (통역사의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 4: 30초의 법칙 &amp;mdash; 결론부터 말하라 (통역사의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;왜 결론부터 말해야 할까요?:&lt;/b&gt; 상사는 매우 바쁘고, 한 가지에 오래 집중하기 어려움&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'안심 첫 문장'의 힘:&lt;/b&gt; 상대방이 &quot;그래서 결론이 뭔데?&quot;라고 묻기 전에, 먼저 답을 제시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나쁜 예시 ('일못러'):&lt;/b&gt; &quot;팀장님, 지난 분기 데이터를 분석해 봤는데요...&quot; (과정부터 설명)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 예시 ('일잘러'):&lt;/b&gt; &quot;팀장님, 결론부터 말씀드리면 마케팅 예산을 영상 콘텐츠에 집중해야 합니다...&quot; (결론부터 제시)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이점:&lt;/b&gt; 상대방의 시간을 존중하고, 유능한 사람으로 보이게 만드는 가장 빠른 길&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;9. 원칙 5: 형용사가 아닌 숫자로 말하라 (통역사의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 5: 형용사가 아닌 숫자로 말하라 (통역사의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 개념:&lt;/b&gt; &quot;좋았습니다&quot;, &quot;많이 늘었습니다&quot;와 같은 모호한 표현 대신 명확한 숫자로 객관적인 사실을 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;숫자 + 해석의 중요성:&lt;/b&gt; 숫자가 왜 중요한지, 무엇을 의미하는지를 설명하는 **'해석'**을 덧붙여야 성과를 제대로 '자랑'할 수 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최고의 예시 ('일잘러'의 해석):&lt;/b&gt; &quot;이번 블로그 포스팅은 조회수 1만을 기록했습니다. 이는 기존 평균의 5배 높은 수치이며, 경쟁사 최고 인기 아티클의 성과를 뛰어넘는 기록입니다.&quot; (데이터 + 해석 = 강력한 임팩트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 포인트:&lt;/b&gt; 숫자에 해석을 더하는 능력은 '실무자'를 넘어 **'전략가'**로 보이게 만듬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;10. 원칙 6: 당연하다고 생각 말고, 전부 확인하라 (통역사의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 6: 당연하다고 생각 말고, 전부 확인하라 (통역사의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오해의 원인:&lt;/b&gt; 언어의 임의성, 지식의 저주&lt;/li&gt;
&lt;li&gt;&lt;b&gt;해결책:&lt;/b&gt; 모든 오해를 막는 유일한 방법은 **'질문'**하는 것. 질문은 무능함이 아니라 프로페셔널리즘의 증거&lt;/li&gt;
&lt;li&gt;&lt;b&gt;질문의 목적 재구성:&lt;/b&gt; &quot;내가 몰라서&quot;가 아니라 &quot;당신이 원하는 것을 정확히 드리기 위해서&quot;로 바꾸어 표현&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 예시 ('일잘러'):&lt;/b&gt; &quot;팀장님, 원하시는 결과물을 정확히 드리기 위해 여쭙니다. 이메일에 핵심만 몇 줄로 요약해 드릴까요? 지금 바로 필요하신 건가요?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;11. 원칙 7: 건강한 관계를 위한 동료 지향 및 거리두기 (외교관의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 7: 건강한 관계를 위한 동료 지향 및 거리두기 (외교관의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 메시지:&lt;/b&gt; '모든 사람에게 좋은 사람'이 되려 하지 말고, **'함께 일하기 좋은 동료'**를 지향하라.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나쁜 예시 ('일못러' - 투머치 고민러):&lt;/b&gt; 동료 표정이 안 좋았다는 이유로 온종일 자책하며 감정 소모&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 예시 ('일잘러' - 프로페셔널):&lt;/b&gt; 개인적으로 받아들이지 않고, 프로페셔널한 태도를 유지하며 일에 집중&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 깨달음:&lt;/b&gt; 관계 단순화는 가장 중요한 일에 집중하기 위해 에너지를 보존하는 현명한 자기관리 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;12. 원칙 8: 당신의 경계선을 친절하고 단호하게 알려라 (외교관의 마인드셋)&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;&lt;b&gt;제목:&lt;/b&gt; 원칙 8: 당신의 경계선을 친절하고 단호하게 알려라 (외교관의 마인드셋)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경계선의 의미:&lt;/b&gt; 다른 사람이 나를 어떻게 대할지는 내가 나의 '선'을 명확하게 알려주는 것을 통해 결정됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;'아니오'의 의미:&lt;/b&gt; 상대를 거절하는 공격이 아니라, 나의 상황과 우선순위를 '친절하게' 알려주는 명확한 소통&lt;/li&gt;
&lt;li&gt;&lt;b&gt;나쁜 예시 ('일못러'):&lt;/b&gt; 갈등을 피하고 싶어 무리한 부탁을 수락한 뒤, 결국 모든 일의 퀄리티가 떨어짐&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좋은 예시 ('일잘러'):&lt;/b&gt; &quot;도와드리고 싶은데, 제가 지금 X 프로젝트 마감이 3시까지라 먼저 집중해야 할 것 같아요. 혹시 3시 이후에 봐드려도 괜찮을까요?&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경계선 설정의 중요성:&lt;/b&gt; '만만한 구성원'이 아닌 **'자기 일에 책임감을 가진 프로'**로 인식하게 만드는 장기적인 평판 관리 전략&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;결론: AI와 함께 똑똑하게 일하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 몇 날 며칠이 걸렸을 교육 자료 제작이, AI 덕분에 단 몇 시간 만에 끝났습니다. Gemini의 딥리서치 기능은 방대한 자료 속에서 핵심을 꿰뚫는 통찰력을 제공했고, skywork.ai는 정교한 디자인 가이드에 따라 그 통찰력을 시각적으로 구현해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해 AI는 더 이상 단순 반복 업무를 대체하는 도구가 아님을 깨달았습니다. 어떻게 질문하고 지시하느냐에 따라, 인간의 창의적인 기획을 돕고 그 결과물을 증폭시키는 강력한 '협업 파트너'가 될 수 있습니다. 교육 자료 준비로 고민하고 계신다면, AI와 함께 똑똑하게 일해보시는 건 어떨까요?&lt;/p&gt;</description>
      <category>dev</category>
      <category>GeminiAPI</category>
      <category>skywork.ai</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/6</guid>
      <comments>https://quinton84.tistory.com/6#entry6comment</comments>
      <pubDate>Wed, 9 Jul 2025 16:58:25 +0900</pubDate>
    </item>
    <item>
      <title>  테니스 NTRP 레벨 테스트</title>
      <link>https://quinton84.tistory.com/5</link>
      <description>&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;599&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biFJ1V/btsO6rnGm5h/3Yd3a8yetKM2p4ykeX289K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biFJ1V/btsO6rnGm5h/3Yd3a8yetKM2p4ykeX289K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biFJ1V/btsO6rnGm5h/3Yd3a8yetKM2p4ykeX289K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiFJ1V%2FbtsO6rnGm5h%2F3Yd3a8yetKM2p4ykeX289K%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;916&quot; height=&quot;561&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;367&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;h1 style=&quot;background-color: #ffffff; color: #6a11cb; text-align: center;&quot;&gt;당신의 테니스 레벨은?&lt;/h1&gt;
&lt;p style=&quot;background-color: #ffffff; color: #555555; text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;간단한 테스트로 지금 바로 확인하세요!&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #777777; text-align: center;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 테니스 실력이 궁금하신가요?&lt;br /&gt;이 테스트는 당신의 NTRP 레벨을 쉽고 재미있게 진단해 드립니다.&lt;br /&gt;몇 가지 질문에 답하고,&lt;br /&gt;포핸드부터 멘탈까지 당신의 강점과 개선점을 한눈에 확인해보세요!&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/p&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;&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;code_1751812593407&quot; data-ke-type=&quot;html&quot; data-source=&quot;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&amp;quot;ko&amp;quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;
    &amp;lt;title&amp;gt;  테니스 NTRP 레벨 테스트&amp;lt;/title&amp;gt;
    &amp;lt;!-- Google Fonts: Poppins --&amp;gt;
    &amp;lt;link href=&amp;quot;https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&amp;amp;display=swap&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;gt;
    &amp;lt;style&amp;gt;
        :root {
            --primary-gradient: linear-gradient(135deg, #6A11CB 0%, #2575FC 100%);
            --primary-color-start: #6A11CB;
            --primary-color-end: #2575FC;
            --text-dark: #222;
            --text-medium: #555;
            --text-light: #777;
            --bg-light: #f8f9fa;
            --border-light: #e0e0e0;
            --shadow-light: 0 10px 30px rgba(0, 0, 0, 0.08);
            --shadow-medium: 0 15px 45px rgba(0, 0, 0, 0.15);
            --shadow-button-hover: 0 10px 25px rgba(106, 17, 203, 0.4);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Poppins', sans-serif;
            background: var(--primary-gradient);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            background: white;
            border-radius: 25px;
            padding: 40px;
            box-shadow: var(--shadow-medium);
            max-width: 650px;
            width: 100%;
            text-align: center;
            margin: 20px;
            min-height: 85vh;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }

        h1 {
            color: var(--text-dark);
            margin-bottom: 30px;
            font-size: 2.8rem;
            font-weight: 700;
            line-height: 1.2;
            letter-spacing: -0.03em;
        }
        
        h1 .emoji {
            display: inline-block;
            margin-right: 5px;
        }

        .progress-bar {
            width: 100%;
            height: 10px;
            background: var(--border-light);
            border-radius: 5px;
            margin-bottom: 35px;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: var(--primary-gradient);
            transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
            border-radius: 5px;
        }

        /* Question card and option spacing/sizing adjustments */
        .question-card {
            margin-bottom: 20px;
            flex-grow: 1; 
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
        }

        .question-card h2 {
            color: var(--text-medium);
            margin-bottom: 10px;
            font-size: 1.2rem;
        }

        .question {
            font-size: 1.5rem;
            color: var(--text-dark);
            margin-bottom: 10px;
            line-height: 1.4;
            font-weight: 600;
        }

        .question-description {
            font-size: 1rem;
            color: var(--text-light);
            margin-bottom: 25px;
            line-height: 1.5;
            font-style: normal;
        }

        .options {
            display: flex;
            flex-direction: column;
            gap: 8px; /* Reduced gap */
            margin-bottom: 15px;
        }

        .option {
            display: flex;
            align-items: center; /* 핵심: 세로 중앙 정렬 */
            padding: 12px 20px; /* Reduced padding */
            border: 2px solid var(--border-light);
            border-radius: 12px;
            background: white;
            cursor: pointer;
            transition: all 0.3s ease;
            text-align: left;
            min-height: 50px; /* Reduced min-height */
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.04);
        }

        .option:hover {
            border-color: var(--primary-color-start);
            transform: translateY(-2px);
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08);
        }

        .option.selected {
            border-color: var(--primary-color-end);
            background: var(--primary-gradient);
            color: white;
            transform: translateY(-1px);
            box-shadow: 0 4px 10px rgba(106, 17, 203, 0.3);
        }

        /* Score display removed, so .option .score style is now unused */
        /* .option .score { display: none; } */ 

        .option .description {
            flex: 1; /* Takes up remaining space */
            font-size: 0.95rem; /* Slightly reduced font size */
            line-height: 1.4;
            display: flex;
            align-items: center; /* Ensures text remains vertically centered */
        }

        .navigation {
            display: flex;
            justify-content: space-between;
            gap: 15px;
            margin-top: 15px;
        }

        .nav-btn {
            padding: 12px 25px;
            border: none;
            border-radius: 10px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            flex: 1;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .nav-btn.prev {
            background: var(--bg-light);
            color: var(--text-medium);
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
        }

        .nav-btn.prev:hover {
            background: #e0e0e0;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.09);
        }

        .nav-btn.next,
        .nav-btn.submit {
            background: var(--primary-gradient);
            color: white;
            box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);
        }

        .nav-btn.next:hover,
        .nav-btn.submit:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(106, 17, 203, 0.4);
        }

        .nav-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .result-card {
            background: var(--primary-gradient);
            color: white;
            border-radius: 20px;
            padding: 30px;
            margin-bottom: 20px;
            text-align: center;
            box-shadow: var(--shadow-medium);
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .result-card h2 {
            margin-bottom: 20px;
            font-size: 1.6rem;
        }

        .ntrp-level {
            font-size: 4.5rem;
            font-weight: 800;
            margin: 20px 0;
            text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
            line-height: 1;
        }

        .description {
            font-size: 1.2rem;
            margin-bottom: 20px;
            line-height: 1.5;
        }

        .stats {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            padding: 15px;
            margin-top: 15px;
        }

        .stats p {
            margin: 5px 0;
            font-size: 1rem;
        }

        .detailed-analysis {
            background: var(--bg-light);
            border-radius: 20px;
            padding: 30px;
            margin: 20px 0;
            text-align: left;
            box-shadow: var(--shadow-light);
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .detailed-analysis h3 {
            color: var(--text-dark);
            margin-bottom: 25px;
            font-size: 1.6rem;
            font-weight: 600;
            text-align: center;
        }

        .detailed-analysis h4 {
            color: var(--text-medium);
            margin-bottom: 15px;
            font-size: 1.2rem;
            font-weight: 600;
            text-align: center;
        }

        /* Score Grid (2 columns for desktop) */
        .score-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr); /* 2 columns for desktop */
            gap: 12px;
            margin-bottom: 25px;
        }

        .score-item {
            display: flex;
            align-items: center; /* Vertically centers content within item */
            gap: 10px;
            padding: 14px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
        }

        .category-name {
            min-width: 70px;
            font-weight: 600;
            color: var(--text-dark);
            text-align: left;
            flex-shrink: 0;
            font-size: 0.95rem;
        }

        .score-bar {
            flex-grow: 1;
            height: 12px;
            background: var(--border-light);
            border-radius: 6px;
            overflow: hidden;
        }

        .score-fill {
            height: 100%;
            background: var(--primary-gradient);
            transition: width 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
            border-radius: 6px;
        }

        .score-value {
            min-width: 35px;
            font-weight: 600;
            color: var(--text-dark);
            text-align: right;
            flex-shrink: 0;
            font-size: 0.95rem;
        }

        .strengths-weaknesses {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-bottom: 25px;
        }

        .strengths, .weaknesses {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
            text-align: center;
        }

        .strengths h4 {
            color: #28a745;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .weaknesses h4 {
            color: #dc3545;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .strengths ul, .weaknesses ul {
            list-style: none;
            padding: 0;
        }

        .strengths li, .weaknesses li {
            padding: 8px 0;
            border-bottom: 1px solid #eee;
            font-size: 1rem;
            color: var(--text-medium);
            line-height: 1.4;
        }

        .strengths li:last-child, .weaknesses li:last-child {
            border-bottom: none;
        }

        .improvement-suggestions {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
            text-align: center;
            margin-top: 20px;
        }

        .improvement-suggestions h4 {
            color: #007bff;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .improvement-suggestions ul {
            list-style: none;
            padding: 0;
        }

        .improvement-suggestions li {
            padding: 10px 0;
            border-bottom: 1px solid #eee;
            line-height: 1.6;
            font-size: 1rem;
            color: var(--text-medium);
        }

        .improvement-suggestions li:last-child {
            border-bottom: none;
        }

        .restart-btn {
            background: var(--primary-gradient);
            color: white;
            border: none;
            padding: 15px 40px;
            border-radius: 12px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            margin-top: 30px;
            box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .restart-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(106, 17, 203, 0.4);
        }

        /* Welcome Screen Styles */
        .welcome-screen {
            text-align: center;
            max-width: 800px;
            margin: 0 auto;
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .welcome-title {
            font-size: 3rem;
            font-weight: 800;
            color: var(--primary-color-start);
            margin-bottom: 15px;
            line-height: 1.2;
            letter-spacing: -0.04em;
            text-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
        }

        .welcome-subtitle {
            font-size: 1.3rem;
            color: var(--text-medium);
            margin-bottom: 20px;
            line-height: 1.6;
            font-weight: 400;
        }

        .welcome-description {
            font-size: 1rem;
            color: var(--text-light);
            line-height: 1.6;
            margin-bottom: 30px;
            text-align: center;
            max-width: 500px;
        }

        .start-btn {
            background: var(--primary-gradient);
            color: white;
            border: none;
            padding: 16px 45px;
            border-radius: 15px;
            font-size: 1.2rem;
            font-weight: 700;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 8px 25px rgba(106, 17, 203, 0.35);
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
            letter-spacing: 0.02em;
        }

        .start-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 30px rgba(106, 17, 203, 0.45);
        }

        /* Coupang Ad Style */
        .coupang-ad-container {
            margin-top: 25px;
            margin-bottom: 5px;
            text-align: center;
            width: 100%;
        }
        .coupang-ad-container iframe {
            max-width: 100%;
            height: 44px; /* Default height for search bar */
            border-radius: 8px;
            overflow: hidden;
        }
        /* Style for banner ad iframe */
        .coupang-banner-ad-container {
            margin-top: 20px; /* Spacing from previous content */
            margin-bottom: 10px; /* Spacing to next content */
            text-align: center;
            width: 100%;
        }
        .coupang-banner-ad-container iframe {
            max-width: 100%; /* Ensure responsiveness */
            height: 140px; /* Fixed height for banner */
            border-radius: 8px;
            overflow: hidden;
            border: none; /* Remove default iframe border */
        }


        /* Coupang Disclosure Style */
        .coupang-disclosure {
            font-size: 0.75rem;
            color: var(--text-light);
            margin-top: 10px;
            text-align: center;
            line-height: 1.3;
        }


        /* Responsive Design */
        @media (max-width: 768px) {
            .container {
                padding: 20px;
                margin: 10px;
                min-height: 95vh;
            }
            
            h1 {
                font-size: 2rem;
                margin-bottom: 20px;
            }
            
            .question-card h2 {
                font-size: 1rem;
            }
            
            .question {
                font-size: 1.3rem;
                margin-bottom: 8px;
            }
            
            .question-description {
                font-size: 0.85rem;
                margin-bottom: 20px;
            }
            
            .options {
                gap: 8px;
                margin-bottom: 15px;
            }
            
            .option {
                padding: 12px 15px;
                min-height: 50px;
                border-radius: 10px;
            }
            
            .option .description { /* score가 display: none이므로 직접 접근 */
                font-size: 0.85rem;
            }
            
            .nav-btn {
                padding: 10px 20px;
                font-size: 0.9rem;
            }
            
            .result-card {
                padding: 25px;
            }
            
            .result-card h2 {
                font-size: 1.3rem;
            }
            
            .ntrp-level {
                font-size: 3rem;
            }
            
            .description {
                font-size: 1rem;
            }
            
            .stats p {
                font-size: 0.85rem;
            }
            
            .detailed-analysis {
                padding: 20px;
            }
            
            .detailed-analysis h3 {
                font-size: 1.4rem;
                margin-bottom: 15px;
            }
            
            .detailed-analysis h4 {
                font-size: 1rem;
                margin-bottom: 10px;
            }
            
            /* 모바일에서 score-grid는 다시 1열로 */
            .score-grid {
                grid-template-columns: 1fr;
                gap: 8px;
            }
            
            /* 모바일에서 score-item 내부 정렬 다시 설정 */
            .score-item {
                flex-direction: row;
                align-items: center;
                justify-content: space-between;
                gap: 8px;
                padding: 10px 15px;
            }
            
            .category-name {
                min-width: auto;
                text-align: left;
                margin-bottom: 0;
                font-size: 0.9rem;
            }
            
            .score-bar {
                flex-grow: 1;
                height: 10px;
            }
            
            .score-value {
                min-width: auto;
                text-align: right;
                margin-top: 0;
                font-size: 0.9rem;
            }

            .strengths-weaknesses {
                grid-template-columns: 1fr;
                gap: 15px;
            }
            
            .strengths, .weaknesses, .improvement-suggestions {
                padding: 15px;
            }
            
            .strengths h4, .weaknesses h4, .improvement-suggestions h4 {
                font-size: 1rem;
            }
            
            .strengths ul, .weaknesses ul, .improvement-suggestions ul {
                font-size: 0.85rem;
                line-height: 1.4;
            }
            
            .restart-btn {
                padding: 12px 25px;
                font-size: 1rem;
                margin-top: 20px;
            }

            /* Welcome Screen Responsive */
            .welcome-title {
                font-size: 1.8rem;
                margin-bottom: 10px;
            }
            
            .welcome-subtitle {
                font-size: 1rem;
                margin-bottom: 15px;
            }
            
            .welcome-description {
                font-size: 0.85rem;
                margin-bottom: 25px;
            }
            
            .start-btn {
                padding: 12px 30px;
                font-size: 1rem;
            }
            .coupang-disclosure {
                font-size: 0.7rem;
            }
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&amp;quot;container&amp;quot; id=&amp;quot;app&amp;quot;&amp;gt;
        &amp;lt;!-- 앱 내용이 여기에 동적으로 로드됩니다 --&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        // 앱 상태 관리
        let currentState = 'welcome'; // 'welcome', 'quiz', 'result'
        let currentQuestionIndex = 0;
        let answers = {};

        // NTRP 질문 데이터
        const questions = {
            &amp;quot;forehand&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;포핸드를 원하는 곳으로 보낼 수 있나요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;포핸드의 방향 제어와 깊이 조절 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;공 때리기도 급급! 라켓에 맞으면 다행이죠.  &amp;quot;,
                    2: &amp;quot;간단한 방향(대충 저~쪽)은 어쩌다 가능해요. 가끔 예상치 못한 곳으로!  &amp;quot;, 
                    3: &amp;quot;중간 속도 공은 원하는 곳으로 척척! 나름 컨트롤 가능!  &amp;quot;,
                    4: &amp;quot;깊이와 방향을 자유자재로 조절! 상대 코트 구석구석을 노려요.  &amp;quot;, 
                    5: &amp;quot;어떤 상황에서도 정확한 포핸드로 게임 지배! 역시 필승!  &amp;quot;
                }
            },
            &amp;quot;backhand&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;백핸드를 원하는 곳으로 보낼 수 있나요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;백핸드의 일관성과 방향 제어 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;백핸드만 오면 몸부터 돌려 포핸드로 냅다 쳐요. 나의 숙제!  &amp;quot;,
                    2: &amp;quot;백핸드를 치긴 하는데 네트행이거나 엉뚱한 방향으로 가요. 삐끗삐끗!  &amp;quot;,
                    3: &amp;quot;중간 속도 공은 백핸드로도 꽤 안정적! 이제 좀 폼이 나는 듯?  &amp;quot;,
                    4: &amp;quot;백핸드로도 방향/깊이 자유자재! 상대방이 약점인 줄 알았대요.  &amp;quot;,
                    5: &amp;quot;포핸드만큼 강력하고 정확한 백핸드로 상대를 압도! 백핸드마스터!  &amp;quot;
                }
            },
            &amp;quot;serve&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;서브의 정확성과 파워는 어느 정도인가요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;첫 서브와 세컨드 서브의 성공률과 효과를 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;일단 서브 넣기만 해도 감지덕지! 더블 폴트는 기본이죠.  &amp;quot;,
                    2: &amp;quot;세컨드 서브는 어떻게든 코트 안에 넣어요. 첫 서브는 모험!  &amp;quot;,
                    3: &amp;quot;첫 서브도 가끔 번개처럼 꽂히고, 세컨드 서브도 안정적! ⚡&amp;quot;,
                    4: &amp;quot;강력한 파워와 날카로운 정확성! 서브 에이스 제조기!  &amp;quot;,
                    5: &amp;quot;압박감 속에서도 에이스 꽂는 믿음직한 서브! 서브는 곧 득점!  &amp;quot;
                }
            },
            &amp;quot;volley&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;발리와 네트 플레이를 할 수 있나요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;네트에서의 편안함과 발리 득점 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;네트 근처 가는 건 좀... 공이 무서워요. �&amp;quot;,
                    2: &amp;quot;간단하게 네트 앞에서 공 받아 넘기는 정도는 해요. 툭!  &amp;quot;,
                    3: &amp;quot;포핸드 발리는 꽤 자신 있어요! 백핸드는 아직 연습 중...  &amp;quot;,
                    4: &amp;quot;발리로 포인트를 직접 따낼 수 있어요! 네트는 나의 구역!  &amp;quot;,
                    5: &amp;quot;네트 앞에서 상대를 압박하고 발리로 게임을 끝냅니다! 발리의 마법사!  &amp;quot;
                }
            },
            &amp;quot;footwork&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;풋워크와 코트 커버리지는 어느 정도인가요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;코트 이동과 포지셔닝 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;공이 오면 멍... 공 따라가는 것도 버거워요.  &amp;quot;,
                    2: &amp;quot;기본적인 좌우 이동은 하지만, 빠르진 않아요. 영차영차!  &amp;quot;,
                    3: &amp;quot;코트 넓이를 어느 정도 커버해요. 이제 좀 테니스 선수 같아요!  &amp;quot;,
                    4: &amp;quot;날렵한 풋워크로 코트 구석구석을 누벼요! 휙휙!  &amp;quot;,
                    5: &amp;quot;프로 선수처럼 코트 전체를 지배하는 풋워크와 포지셔닝! 코트의 지배자!  &amp;quot;
                }
            },
            &amp;quot;strategy&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;게임 전략을 세우고 실행할 수 있나요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;상대방 분석과 전략적 사고 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;그냥 공만 치면 되는 거 아닌가요? 전략은 뭔가요?  &amp;quot;,
                    2: &amp;quot;상대방 약점을 대충 노려볼까...? 정도는 생각해요.  &amp;quot;,
                    3: &amp;quot;상대 약점 파악하고 거기에 맞춰 공격 시도! 제법 전략적!  &amp;quot;,
                    4: &amp;quot;게임을 예측하고 상황에 따라 전략을 유연하게 바꿔요! 이기는 게임은 전략 싸움!  &amp;quot;,
                    5: &amp;quot;어떤 상대방도 완벽 분석, 맞춤 전략으로 승리 쟁취! 테니스 브레인!  &amp;quot;
                }
            },
            &amp;quot;consistency&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;일관성 있게 랠리를 이어갈 수 있나요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;안정적인 샷과 에러 관리 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;3번 이상 랠리 이어가면 기적! 내 공은 어디로...  &amp;zwj; &amp;quot;,
                    2: &amp;quot;공은 넘기는데, 속도는 좀 느려요  &amp;quot;, // 문구 수정 및 이모지 변경
                    3: &amp;quot;중간 속도 랠리는 꽤 안정적! 이제 좀 랠리 맛이 나네요!  &amp;quot;,
                    4: &amp;quot;빠른 페이스에서도 안정적! 좀처럼 실수하지 않죠.  ️&amp;quot;,
                    5: &amp;quot;어떤 상황에서도 흔들림 없는 완벽한 일관성! 벽치기 장인!  &amp;quot;
                }
            },
            &amp;quot;mental&amp;quot;: {
                &amp;quot;question&amp;quot;: &amp;quot;게임 중 멘탈 관리가 잘 되나요?&amp;quot;,
                &amp;quot;description&amp;quot;: &amp;quot;압박감과 스트레스 상황에서의 대처 능력을 평가합니다.&amp;quot;,
                &amp;quot;levels&amp;quot;: {
                    1: &amp;quot;실수 한 번 하면 라켓 던지고 싶어져요. 바로 멘탈 붕괴!  &amp;quot;,
                    2: &amp;quot;간단한 게임은 괜찮은데, 지고 있으면 표정 관리가 안 돼요.  &amp;quot;,
                    3: &amp;quot;어느 정도 압박감에 대처 가능! 그래도 속은 부글부글...  &amp;quot;,
                    4: &amp;quot;중요한 순간에도 침착하고 냉정함 유지! 강철 멘탈!  &amp;quot;,
                    5: &amp;quot;어떤 상황에서도 흔들리지 않는 완벽 멘탈로 상대를 압도! 멘탈 갑!  &amp;quot;
                }
            }
        };

        // NTRP 레벨별 위트 있는 설명
        const ntrpDescriptions = {
            1.0: &amp;quot;테니스 신생아: 라켓을 잡고 코트에 첫발을 내디딘 당신! 테니스 세계에 오신 것을 환영합니다!&amp;quot;,
            1.5: &amp;quot;테니스 걸음마: 공을 때리고는 있는데, 어디로 갈지는 공만 아는 단계! 일단 공을 넘기는 게 목표죠.&amp;quot;,
            2.0: &amp;quot;테니스 초급 모험가: 공을 넘기기 시작했지만, 아직 기본기가 시급한 테린이! 랠리는 짧지만, 열정은 만렙!&amp;quot;,
            2.5: &amp;quot;테니스 길찾기 중: 어설프게 공은 치지만, 어디로 갈지, 왜 이렇게 어려운지 고민이 많은 단계. 그래도 랠리의 맛을 조금씩 알아가죠!&amp;quot;,
            3.0: &amp;quot;테니스 기본 다지기: 이제 공은 웬만큼 원하는 곳으로 보내지만, 깊이나 파워는 아직 미완성! 복식에서 '원업 원백'이 익숙한 알뜰 플레이어.&amp;quot;,
            3.5: &amp;quot;테니스 실력 점프 중: 일관성과 컨트롤을 갖추고 슬슬 네트 플레이도 시도하는 단계! '저 사람 꽤 치는데?' 소리 듣기 시작할 때죠.&amp;quot;,
            4.0: &amp;quot;테니스 숙련자 입문: 포핸드, 백핸드 모두 깊이와 컨트롤을 갖추고, 서브도 제법 날카로운 아마추어 고수! 게임의 흐름을 읽기 시작합니다.&amp;quot;,
            4.5: &amp;quot;테니스 전략가 초급: 파워, 스핀, 전략까지 겸비한 테니스 '꾼'! 게임을 이끌어가는 능력이 출중하며, 코트에서 상대방을 괴롭히기 시작합니다.&amp;quot;,
            5.0: &amp;quot;테니스 아마추어 최강자: 강력한 공격과 정교한 기술을 겸비한 아마추어 레벨의 정점! 이제 동네 코트에서는 적수를 찾기 어렵죠.&amp;quot;,
            5.5: &amp;quot;테니스 준프로의 벽: 프로급 기술과 전략을 갖추고 어떤 상황에서도 흔들리지 않는 멘탈의 소유자! 아마추어와 프로의 경계에 서 있습니다.&amp;quot;,
            6.0: &amp;quot;테니스 프로페셔널: 국가대표, 대학 선수, 또는 세계를 무대로 뛰는 진정한 테니스 스타! 이들은 그냥 '테니스 그 자체'입니다.&amp;quot;,
            7.0: &amp;quot;테니스 월드클래스: 세계적 수준의 테니스 스타! 이제 당신은 테니스의 신이 되었습니다.&amp;quot;
        };

        // 앱 상태 관리
        let result = null;

        // 질문 키 배열
        const questionKeys = Object.keys(questions);

        // 앱 렌더링 함수
        function renderApp() {
            const app = document.getElementById('app');
            
            if (currentState === 'welcome') {
                renderWelcome(app);
            } else if (result) {
                renderResult(app);
            } else {
                renderQuestion(app);
            }
        }

        // 환영 화면 렌더링
        function renderWelcome(app) {
            app.innerHTML = &amp;#96;
                &amp;lt;div class=&amp;quot;welcome-screen&amp;quot;&amp;gt;
                    &amp;lt;h1 class=&amp;quot;welcome-title&amp;quot;&amp;gt;&amp;lt;span class=&amp;quot;emoji&amp;quot;&amp;gt; &amp;lt;/span&amp;gt; 당신의 테니스 레벨은?&amp;lt;/h1&amp;gt;
                    &amp;lt;p class=&amp;quot;welcome-subtitle&amp;quot;&amp;gt;간단한 테스트로 지금 바로 확인하세요! &amp;lt;span class=&amp;quot;emoji&amp;quot;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;div class=&amp;quot;welcome-description&amp;quot;&amp;gt;
                        &amp;lt;p&amp;gt;내 테니스 실력이 궁금하신가요?&amp;lt;br&amp;gt;
                        이 테스트는 당신의 NTRP 레벨을 쉽고 재미있게 진단해 드립니다.&amp;lt;br&amp;gt;
                        몇 가지 질문에 답하고,&amp;lt;br&amp;gt;
                        포핸드부터 멘탈까지 당신의 강점과 개선점을 한눈에 확인해보세요! &amp;lt;span class=&amp;quot;emoji&amp;quot;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;button class=&amp;quot;start-btn&amp;quot; onclick=&amp;quot;startTest()&amp;quot;&amp;gt;테스트 시작하기&amp;lt;/button&amp;gt;
                    &amp;lt;!-- Coupang Partners Search Bar Ad --&amp;gt;
                    &amp;lt;div class=&amp;quot;coupang-ad-container&amp;quot;&amp;gt;
                        &amp;lt;iframe src=&amp;quot;https://coupa.ng/ciT9UH&amp;quot; width=&amp;quot;100%&amp;quot; height=&amp;quot;44&amp;quot; frameborder=&amp;quot;0&amp;quot; scrolling=&amp;quot;no&amp;quot; referrerpolicy=&amp;quot;unsafe-url&amp;quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;!-- Coupang Partners Banner Ad --&amp;gt;
                    &amp;lt;div class=&amp;quot;coupang-banner-ad-container&amp;quot;&amp;gt;
                        &amp;lt;iframe src=&amp;quot;https://ads-partners.coupang.com/widgets.html?id=885878&amp;amp;template=carousel&amp;amp;trackingCode=AF6812978&amp;amp;subId=&amp;amp;width=680&amp;amp;height=140&amp;amp;tsource=&amp;quot; width=&amp;quot;100%&amp;quot; height=&amp;quot;140&amp;quot; frameborder=&amp;quot;0&amp;quot; scrolling=&amp;quot;no&amp;quot; referrerpolicy=&amp;quot;unsafe-url&amp;quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;!-- Coupang Partners Disclosure --&amp;gt;
                    &amp;lt;p class=&amp;quot;coupang-disclosure&amp;quot;&amp;gt;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;#96;;
        }

        // 질문 렌더링
        function renderQuestion(app) {
            const currentQuestionKey = questionKeys[currentQuestionIndex];
            const currentQuestion = questions[currentQuestionKey];
            
            app.innerHTML = &amp;#96;
                &amp;lt;h1&amp;gt;  테니스 NTRP 레벨 테스트&amp;lt;/h1&amp;gt;
                
                &amp;lt;div class=&amp;quot;progress-bar&amp;quot;&amp;gt;
                    &amp;lt;div class=&amp;quot;progress-fill&amp;quot; style=&amp;quot;width: ${((currentQuestionIndex + 1) / questionKeys.length) * 100}%&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                
                &amp;lt;div class=&amp;quot;question-card&amp;quot;&amp;gt;
                    &amp;lt;h2&amp;gt;질문 ${currentQuestionIndex + 1} / ${questionKeys.length}&amp;lt;/h2&amp;gt;
                    &amp;lt;p class=&amp;quot;question&amp;quot;&amp;gt;${currentQuestion.question}&amp;lt;/p&amp;gt;
                    &amp;lt;p class=&amp;quot;question-description&amp;quot;&amp;gt;${currentQuestion.description}&amp;lt;/p&amp;gt;
                    
                    &amp;lt;div class=&amp;quot;options&amp;quot;&amp;gt;
                        ${Object.entries(currentQuestion.levels).map(([score, description]) =&amp;gt; &amp;#96;
                            &amp;lt;button class=&amp;quot;option ${answers[currentQuestionKey] === parseInt(score) ? 'selected' : ''}&amp;quot; 
                                    onclick=&amp;quot;selectAnswer('${currentQuestionKey}', ${score})&amp;quot;&amp;gt;
                                &amp;lt;!-- Score display removed --&amp;gt;
                                &amp;lt;span class=&amp;quot;description&amp;quot;&amp;gt;${description}&amp;lt;/span&amp;gt;
                            &amp;lt;/button&amp;gt;
                        &amp;#96;).join('')}
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&amp;quot;navigation&amp;quot;&amp;gt;
                    ${currentQuestionIndex &amp;gt; 0 ? &amp;#96;
                        &amp;lt;button class=&amp;quot;nav-btn prev&amp;quot; onclick=&amp;quot;previousQuestion()&amp;quot;&amp;gt;이전&amp;lt;/button&amp;gt;
                    &amp;#96; : ''}
                    
                    ${currentQuestionIndex &amp;lt; questionKeys.length - 1 ? &amp;#96;
                        &amp;lt;button class=&amp;quot;nav-btn next&amp;quot; onclick=&amp;quot;nextQuestion()&amp;quot; 
                                ${!answers[currentQuestionKey] ? 'disabled' : ''}&amp;gt;다음&amp;lt;/button&amp;gt;
                    &amp;#96; : &amp;#96;
                        &amp;lt;button class=&amp;quot;nav-btn submit&amp;quot; onclick=&amp;quot;calculateResult()&amp;quot; 
                                ${Object.keys(answers).length &amp;lt; questionKeys.length ? 'disabled' : ''}&amp;gt;결과 보기&amp;lt;/button&amp;gt;
                    &amp;#96;}
                &amp;lt;/div&amp;gt;
            &amp;#96;;
        }

        // 결과 렌더링
        function renderResult(app) {
            app.innerHTML = &amp;#96;
                &amp;lt;h1&amp;gt;  NTRP 테스트 결과&amp;lt;/h1&amp;gt;
                &amp;lt;div class=&amp;quot;result-card&amp;quot;&amp;gt;
                    &amp;lt;h2&amp;gt;당신의 NTRP 레벨은&amp;lt;/h2&amp;gt;
                    &amp;lt;div class=&amp;quot;ntrp-level&amp;quot;&amp;gt;${result.ntrp_level}&amp;lt;/div&amp;gt;
                    &amp;lt;p class=&amp;quot;description&amp;quot;&amp;gt;${result.description}&amp;lt;/p&amp;gt;
                    &amp;lt;div class=&amp;quot;stats&amp;quot;&amp;gt;
                        &amp;lt;p&amp;gt;평균 점수: ${result.average_score}/5&amp;lt;/p&amp;gt;
                        &amp;lt;p&amp;gt;응답한 질문: ${result.total_questions}개&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                
                &amp;lt;div class=&amp;quot;detailed-analysis&amp;quot;&amp;gt;
                    &amp;lt;h3&amp;gt;  상세 분석&amp;lt;/h3&amp;gt;
                    
                    &amp;lt;div class=&amp;quot;category-scores&amp;quot;&amp;gt;
                        &amp;lt;h4&amp;gt;영역별 점수&amp;lt;/h4&amp;gt;
                        &amp;lt;div class=&amp;quot;score-grid&amp;quot;&amp;gt;
                            ${Object.entries(result.category_scores).map(([category, score]) =&amp;gt; &amp;#96;
                                &amp;lt;div class=&amp;quot;score-item&amp;quot;&amp;gt;
                                    &amp;lt;span class=&amp;quot;category-name&amp;quot;&amp;gt;${category}&amp;lt;/span&amp;gt;
                                    &amp;lt;div class=&amp;quot;score-bar&amp;quot;&amp;gt;
                                        &amp;lt;div class=&amp;quot;score-fill&amp;quot; style=&amp;quot;width: ${(score / 5) * 100}%&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
                                    &amp;lt;/div&amp;gt;
                                    &amp;lt;span class=&amp;quot;score-value&amp;quot;&amp;gt;${score}/5&amp;lt;/span&amp;gt;
                                &amp;lt;/div&amp;gt;
                            &amp;#96;).join('')}
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                    
                    &amp;lt;div class=&amp;quot;strengths-weaknesses&amp;quot;&amp;gt;
                        ${result.strengths &amp;amp;&amp;amp; result.strengths.length &amp;gt; 0 ? &amp;#96;
                            &amp;lt;div class=&amp;quot;strengths&amp;quot;&amp;gt;
                                &amp;lt;h4&amp;gt;  강점&amp;lt;/h4&amp;gt;
                                &amp;lt;ul&amp;gt;
                                    ${result.strengths.map(strength =&amp;gt; &amp;#96;&amp;lt;li&amp;gt;${strength}&amp;lt;/li&amp;gt;&amp;#96;).join('')}
                                &amp;lt;/ul&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;#96; : ''}
                        
                        ${result.weaknesses &amp;amp;&amp;amp; result.weaknesses.length &amp;gt; 0 ? &amp;#96;
                            &amp;lt;div class=&amp;quot;weaknesses&amp;quot;&amp;gt;
                                &amp;lt;h4&amp;gt;  개선점&amp;lt;/h4&amp;gt;
                                &amp;lt;ul&amp;gt;
                                    ${result.weaknesses.map(weakness =&amp;gt; &amp;#96;&amp;lt;li&amp;gt;${weakness}&amp;lt;/li&amp;gt;&amp;#96;).join('')}
                                &amp;lt;/ul&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;#96; : ''}
                    &amp;lt;/div&amp;gt;
                    
                    ${result.improvement_suggestions ? &amp;#96;
                        &amp;lt;div class=&amp;quot;improvement-suggestions&amp;quot;&amp;gt;
                            &amp;lt;h4&amp;gt;  개선 제안&amp;lt;/h4&amp;gt;
                            &amp;lt;ul&amp;gt;
                                ${result.improvement_suggestions.map(suggestion =&amp;gt; &amp;#96;&amp;lt;li&amp;gt;${suggestion}&amp;lt;/li&amp;gt;&amp;#96;).join('')}
                            &amp;lt;/ul&amp;gt;
                        &amp;lt;/div&amp;gt;
                    &amp;#96; : ''}
                &amp;lt;/div&amp;gt;
                
                &amp;lt;button class=&amp;quot;restart-btn&amp;quot; onclick=&amp;quot;restartTest()&amp;quot;&amp;gt;다시 테스트하기&amp;lt;/button&amp;gt;
                &amp;lt;!-- Coupang Partners Search Bar Ad --&amp;gt;
                &amp;lt;div class=&amp;quot;coupang-ad-container&amp;quot;&amp;gt;
                    &amp;lt;iframe src=&amp;quot;https://coupa.ng/ciT9UH&amp;quot; width=&amp;quot;100%&amp;quot; height=&amp;quot;44&amp;quot; frameborder=&amp;quot;0&amp;quot; scrolling=&amp;quot;no&amp;quot; referrerpolicy=&amp;quot;unsafe-url&amp;quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;!-- Coupang Partners Banner Ad --&amp;gt;
                &amp;lt;div class=&amp;quot;coupang-banner-ad-container&amp;quot;&amp;gt;
                    &amp;lt;iframe src=&amp;quot;https://ads-partners.coupang.com/widgets.html?id=885878&amp;amp;template=carousel&amp;amp;trackingCode=AF6812978&amp;amp;subId=&amp;amp;width=680&amp;amp;height=140&amp;amp;tsource=&amp;quot; width=&amp;quot;100%&amp;quot; height=&amp;quot;140&amp;quot; frameborder=&amp;quot;0&amp;quot; scrolling=&amp;quot;no&amp;quot; referrerpolicy=&amp;quot;unsafe-url&amp;quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;!-- Coupang Partners Disclosure --&amp;gt;
                &amp;lt;p class=&amp;quot;coupang-disclosure&amp;quot;&amp;gt;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&amp;lt;/p&amp;gt;
            &amp;#96;;
        }

        // Test start
        function startTest() {
            currentState = 'quiz';
            currentQuestionIndex = 0;
            answers = {};
            result = null;
            renderApp();
        }

        // Select answer
        function selectAnswer(questionKey, score) {
            answers[questionKey] = score;
            renderApp();
        }

        // Next question
        function nextQuestion() {
            if (currentQuestionIndex &amp;lt; questionKeys.length - 1) {
                currentQuestionIndex++;
                renderApp();
            }
        }

        // Previous question
        function previousQuestion() {
            if (currentQuestionIndex &amp;gt; 0) {
                currentQuestionIndex--;
                renderApp();
                // Restore selected state after rendering
                const currentQuestionKey = questionKeys[currentQuestionIndex];
                const selectedScore = answers[currentQuestionKey];
                if (selectedScore) {
                    const options = document.querySelectorAll('.option');
                    options.forEach(option =&amp;gt; {
                        if (questions[currentQuestionKey].levels[selectedScore] === option.querySelector('.description').innerText) {
                            option.classList.add('selected');
                        } else {
                            option.classList.remove('selected');
                        }
                    });
                }
            }
        }

        // Calculate result
        function calculateResult() {
            const totalScore = Object.values(answers).reduce((sum, score) =&amp;gt; sum + score, 0);
            const averageScore = totalScore / Object.keys(answers).length;
            
            // Determine NTRP level
            let ntrpLevel;
            if (averageScore &amp;lt;= 1.25) ntrpLevel = 1.0;
            else if (averageScore &amp;lt;= 1.75) ntrpLevel = 1.5;
            else if (averageScore &amp;lt;= 2.25) ntrpLevel = 2.0;
            else if (averageScore &amp;lt;= 2.75) ntrpLevel = 2.5;
            else if (averageScore &amp;lt;= 3.25) ntrpLevel = 3.0;
            else if (averageScore &amp;lt;= 3.75) ntrpLevel = 3.5;
            else if (averageScore &amp;lt;= 4.25) ntrpLevel = 4.0;
            else if (averageScore &amp;lt;= 4.75) ntrpLevel = 4.5;
            else ntrpLevel = 5.0; // If average score is above 4.75, it's 5.0

            // Detailed analysis
            const categoryNames = {
                &amp;quot;forehand&amp;quot;: &amp;quot;포핸드&amp;quot;,
                &amp;quot;backhand&amp;quot;: &amp;quot;백핸드&amp;quot;,
                &amp;quot;serve&amp;quot;: &amp;quot;서브&amp;quot;,
                &amp;quot;volley&amp;quot;: &amp;quot;발리&amp;quot;,
                &amp;quot;footwork&amp;quot;: &amp;quot;풋워크&amp;quot;,
                &amp;quot;strategy&amp;quot;: &amp;quot;전략&amp;quot;,
                &amp;quot;consistency&amp;quot;: &amp;quot;일관성&amp;quot;,
                &amp;quot;mental&amp;quot;: &amp;quot;멘탈&amp;quot;
            };

            const categoryScores = {};
            Object.keys(categoryNames).forEach(category =&amp;gt; {
                if (answers[category]) {
                    categoryScores[categoryNames[category]] = answers[category];
                }
            });

            // Analyze strengths and weaknesses
            const strengths = [];
            const weaknesses = [];
            Object.entries(categoryScores).forEach(([category, score]) =&amp;gt; {
                if (score &amp;gt;= 4) strengths.push(category);
                else if (score &amp;lt;= 2) weaknesses.push(category);
            });

            // Improvement suggestions
            const improvementSuggestions = [];
            if (weaknesses.includes(&amp;quot;포핸드&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;포핸드 기본기 연습이 필요합니다. 스윙 폼과 타점을 중점적으로 연습해보세요.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;백핸드&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;백핸드 그립과 준비 동작을 연습해보세요. 백핸드를 피하지 말고 적극적으로 연습하세요.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;서브&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;서브 연습을 통해 정확성과 파워를 향상시켜보세요. 세컨드 서브부터 안정화하세요.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;발리&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;네트 플레이에 익숙해지세요. 간단한 발리부터 시작해서 점진적으로 발전시키세요.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;풋워크&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;기본적인 이동 연습이 필요합니다. 사이드 스텝과 크로스오버 스텝을 연습하세요.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;전략&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;게임 중 상대방을 관찰하고 약점을 찾아보세요. 다양한 샷 조합을 연습하세요.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;일관성&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;안정적인 샷을 위해 기본기를 탄탄히 하세요. 에러를 줄이는 연습이 중요합니다.&amp;quot;);
            }
            if (weaknesses.includes(&amp;quot;멘탈&amp;quot;)) {
                improvementSuggestions.push(&amp;quot;압박감에 대처하는 연습이 필요합니다. 실수를 해도 포기하지 않는 마음가짐이 중요합니다.&amp;quot;);
            }
            if (improvementSuggestions.length === 0) {
                improvementSuggestions.push(&amp;quot;전반적으로 좋은 실력을 보유하고 있습니다. 더 높은 레벨을 위해 고급 기술을 연습해보세요.&amp;quot;);
            }

            result = {
                ntrp_level: ntrpLevel,
                description: ntrpDescriptions[ntrpLevel] || &amp;quot;정의되지 않은 레벨&amp;quot;,
                average_score: Math.round(averageScore * 10) / 10,
                total_questions: Object.keys(answers).length,
                category_scores: categoryScores,
                strengths: strengths,
                weaknesses: weaknesses,
                improvement_suggestions: improvementSuggestions
            };

            renderApp();
        }

        // Restart test
        function restartTest() {
            currentState = 'welcome';
            currentQuestionIndex = 0;
            answers = {};
            result = null;
            renderApp();
        }

        // Initialize app
        renderApp();
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&quot;&gt;&lt;!-- Google Fonts: Poppins --&gt; 
&lt;style&gt;
        :root {
            --primary-gradient: linear-gradient(135deg, #6A11CB 0%, #2575FC 100%);
            --primary-color-start: #6A11CB;
            --primary-color-end: #2575FC;
            --text-dark: #222;
            --text-medium: #555;
            --text-light: #777;
            --bg-light: #f8f9fa;
            --border-light: #e0e0e0;
            --shadow-light: 0 10px 30px rgba(0, 0, 0, 0.08);
            --shadow-medium: 0 15px 45px rgba(0, 0, 0, 0.15);
            --shadow-button-hover: 0 10px 25px rgba(106, 17, 203, 0.4);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Poppins', sans-serif;
            background: var(--primary-gradient);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            background: white;
            border-radius: 25px;
            padding: 40px;
            box-shadow: var(--shadow-medium);
            max-width: 650px;
            width: 100%;
            text-align: center;
            margin: 20px;
            min-height: 85vh;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }

        h1 {
            color: var(--text-dark);
            margin-bottom: 30px;
            font-size: 2.8rem;
            font-weight: 700;
            line-height: 1.2;
            letter-spacing: -0.03em;
        }
        
        h1 .emoji {
            display: inline-block;
            margin-right: 5px;
        }

        .progress-bar {
            width: 100%;
            height: 10px;
            background: var(--border-light);
            border-radius: 5px;
            margin-bottom: 35px;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: var(--primary-gradient);
            transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
            border-radius: 5px;
        }

        /* Question card and option spacing/sizing adjustments */
        .question-card {
            margin-bottom: 20px;
            flex-grow: 1; 
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
        }

        .question-card h2 {
            color: var(--text-medium);
            margin-bottom: 10px;
            font-size: 1.2rem;
        }

        .question {
            font-size: 1.5rem;
            color: var(--text-dark);
            margin-bottom: 10px;
            line-height: 1.4;
            font-weight: 600;
        }

        .question-description {
            font-size: 1rem;
            color: var(--text-light);
            margin-bottom: 25px;
            line-height: 1.5;
            font-style: normal;
        }

        .options {
            display: flex;
            flex-direction: column;
            gap: 8px; /* Reduced gap */
            margin-bottom: 15px;
        }

        .option {
            display: flex;
            align-items: center; /* 핵심: 세로 중앙 정렬 */
            padding: 12px 20px; /* Reduced padding */
            border: 2px solid var(--border-light);
            border-radius: 12px;
            background: white;
            cursor: pointer;
            transition: all 0.3s ease;
            text-align: left;
            min-height: 50px; /* Reduced min-height */
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.04);
        }

        .option:hover {
            border-color: var(--primary-color-start);
            transform: translateY(-2px);
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08);
        }

        .option.selected {
            border-color: var(--primary-color-end);
            background: var(--primary-gradient);
            color: white;
            transform: translateY(-1px);
            box-shadow: 0 4px 10px rgba(106, 17, 203, 0.3);
        }

        /* Score display removed, so .option .score style is now unused */
        /* .option .score { display: none; } */ 

        .option .description {
            flex: 1; /* Takes up remaining space */
            font-size: 0.95rem; /* Slightly reduced font size */
            line-height: 1.4;
            display: flex;
            align-items: center; /* Ensures text remains vertically centered */
        }

        .navigation {
            display: flex;
            justify-content: space-between;
            gap: 15px;
            margin-top: 15px;
        }

        .nav-btn {
            padding: 12px 25px;
            border: none;
            border-radius: 10px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            flex: 1;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .nav-btn.prev {
            background: var(--bg-light);
            color: var(--text-medium);
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
        }

        .nav-btn.prev:hover {
            background: #e0e0e0;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.09);
        }

        .nav-btn.next,
        .nav-btn.submit {
            background: var(--primary-gradient);
            color: white;
            box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);
        }

        .nav-btn.next:hover,
        .nav-btn.submit:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(106, 17, 203, 0.4);
        }

        .nav-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .result-card {
            background: var(--primary-gradient);
            color: white;
            border-radius: 20px;
            padding: 30px;
            margin-bottom: 20px;
            text-align: center;
            box-shadow: var(--shadow-medium);
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .result-card h2 {
            margin-bottom: 20px;
            font-size: 1.6rem;
        }

        .ntrp-level {
            font-size: 4.5rem;
            font-weight: 800;
            margin: 20px 0;
            text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
            line-height: 1;
        }

        .description {
            font-size: 1.2rem;
            margin-bottom: 20px;
            line-height: 1.5;
        }

        .stats {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            padding: 15px;
            margin-top: 15px;
        }

        .stats p {
            margin: 5px 0;
            font-size: 1rem;
        }

        .detailed-analysis {
            background: var(--bg-light);
            border-radius: 20px;
            padding: 30px;
            margin: 20px 0;
            text-align: left;
            box-shadow: var(--shadow-light);
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .detailed-analysis h3 {
            color: var(--text-dark);
            margin-bottom: 25px;
            font-size: 1.6rem;
            font-weight: 600;
            text-align: center;
        }

        .detailed-analysis h4 {
            color: var(--text-medium);
            margin-bottom: 15px;
            font-size: 1.2rem;
            font-weight: 600;
            text-align: center;
        }

        /* Score Grid (2 columns for desktop) */
        .score-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr); /* 2 columns for desktop */
            gap: 12px;
            margin-bottom: 25px;
        }

        .score-item {
            display: flex;
            align-items: center; /* Vertically centers content within item */
            gap: 10px;
            padding: 14px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
        }

        .category-name {
            min-width: 70px;
            font-weight: 600;
            color: var(--text-dark);
            text-align: left;
            flex-shrink: 0;
            font-size: 0.95rem;
        }

        .score-bar {
            flex-grow: 1;
            height: 12px;
            background: var(--border-light);
            border-radius: 6px;
            overflow: hidden;
        }

        .score-fill {
            height: 100%;
            background: var(--primary-gradient);
            transition: width 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
            border-radius: 6px;
        }

        .score-value {
            min-width: 35px;
            font-weight: 600;
            color: var(--text-dark);
            text-align: right;
            flex-shrink: 0;
            font-size: 0.95rem;
        }

        .strengths-weaknesses {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-bottom: 25px;
        }

        .strengths, .weaknesses {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
            text-align: center;
        }

        .strengths h4 {
            color: #28a745;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .weaknesses h4 {
            color: #dc3545;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .strengths ul, .weaknesses ul {
            list-style: none;
            padding: 0;
        }

        .strengths li, .weaknesses li {
            padding: 8px 0;
            border-bottom: 1px solid #eee;
            font-size: 1rem;
            color: var(--text-medium);
            line-height: 1.4;
        }

        .strengths li:last-child, .weaknesses li:last-child {
            border-bottom: none;
        }

        .improvement-suggestions {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
            text-align: center;
            margin-top: 20px;
        }

        .improvement-suggestions h4 {
            color: #007bff;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .improvement-suggestions ul {
            list-style: none;
            padding: 0;
        }

        .improvement-suggestions li {
            padding: 10px 0;
            border-bottom: 1px solid #eee;
            line-height: 1.6;
            font-size: 1rem;
            color: var(--text-medium);
        }

        .improvement-suggestions li:last-child {
            border-bottom: none;
        }

        .restart-btn {
            background: var(--primary-gradient);
            color: white;
            border: none;
            padding: 15px 40px;
            border-radius: 12px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            margin-top: 30px;
            box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .restart-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(106, 17, 203, 0.4);
        }

        /* Welcome Screen Styles */
        .welcome-screen {
            text-align: center;
            max-width: 800px;
            margin: 0 auto;
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .welcome-title {
            font-size: 3rem;
            font-weight: 800;
            color: var(--primary-color-start);
            margin-bottom: 15px;
            line-height: 1.2;
            letter-spacing: -0.04em;
            text-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
        }

        .welcome-subtitle {
            font-size: 1.3rem;
            color: var(--text-medium);
            margin-bottom: 20px;
            line-height: 1.6;
            font-weight: 400;
        }

        .welcome-description {
            font-size: 1rem;
            color: var(--text-light);
            line-height: 1.6;
            margin-bottom: 30px;
            text-align: center;
            max-width: 500px;
        }

        .start-btn {
            background: var(--primary-gradient);
            color: white;
            border: none;
            padding: 16px 45px;
            border-radius: 15px;
            font-size: 1.2rem;
            font-weight: 700;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 8px 25px rgba(106, 17, 203, 0.35);
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
            letter-spacing: 0.02em;
        }

        .start-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 30px rgba(106, 17, 203, 0.45);
        }

        /* Coupang Ad Style */
        .coupang-ad-container {
            margin-top: 25px;
            margin-bottom: 5px;
            text-align: center;
            width: 100%;
        }
        .coupang-ad-container iframe {
            max-width: 100%;
            height: 44px; /* Default height for search bar */
            border-radius: 8px;
            overflow: hidden;
        }
        /* Style for banner ad iframe */
        .coupang-banner-ad-container {
            margin-top: 20px; /* Spacing from previous content */
            margin-bottom: 10px; /* Spacing to next content */
            text-align: center;
            width: 100%;
        }
        .coupang-banner-ad-container iframe {
            max-width: 100%; /* Ensure responsiveness */
            height: 140px; /* Fixed height for banner */
            border-radius: 8px;
            overflow: hidden;
            border: none; /* Remove default iframe border */
        }


        /* Coupang Disclosure Style */
        .coupang-disclosure {
            font-size: 0.75rem;
            color: var(--text-light);
            margin-top: 10px;
            text-align: center;
            line-height: 1.3;
        }


        /* Responsive Design */
        @media (max-width: 768px) {
            .container {
                padding: 20px;
                margin: 10px;
                min-height: 95vh;
            }
            
            h1 {
                font-size: 2rem;
                margin-bottom: 20px;
            }
            
            .question-card h2 {
                font-size: 1rem;
            }
            
            .question {
                font-size: 1.3rem;
                margin-bottom: 8px;
            }
            
            .question-description {
                font-size: 0.85rem;
                margin-bottom: 20px;
            }
            
            .options {
                gap: 8px;
                margin-bottom: 15px;
            }
            
            .option {
                padding: 12px 15px;
                min-height: 50px;
                border-radius: 10px;
            }
            
            .option .description { /* score가 display: none이므로 직접 접근 */
                font-size: 0.85rem;
            }
            
            .nav-btn {
                padding: 10px 20px;
                font-size: 0.9rem;
            }
            
            .result-card {
                padding: 25px;
            }
            
            .result-card h2 {
                font-size: 1.3rem;
            }
            
            .ntrp-level {
                font-size: 3rem;
            }
            
            .description {
                font-size: 1rem;
            }
            
            .stats p {
                font-size: 0.85rem;
            }
            
            .detailed-analysis {
                padding: 20px;
            }
            
            .detailed-analysis h3 {
                font-size: 1.4rem;
                margin-bottom: 15px;
            }
            
            .detailed-analysis h4 {
                font-size: 1rem;
                margin-bottom: 10px;
            }
            
            /* 모바일에서 score-grid는 다시 1열로 */
            .score-grid {
                grid-template-columns: 1fr;
                gap: 8px;
            }
            
            /* 모바일에서 score-item 내부 정렬 다시 설정 */
            .score-item {
                flex-direction: row;
                align-items: center;
                justify-content: space-between;
                gap: 8px;
                padding: 10px 15px;
            }
            
            .category-name {
                min-width: auto;
                text-align: left;
                margin-bottom: 0;
                font-size: 0.9rem;
            }
            
            .score-bar {
                flex-grow: 1;
                height: 10px;
            }
            
            .score-value {
                min-width: auto;
                text-align: right;
                margin-top: 0;
                font-size: 0.9rem;
            }

            .strengths-weaknesses {
                grid-template-columns: 1fr;
                gap: 15px;
            }
            
            .strengths, .weaknesses, .improvement-suggestions {
                padding: 15px;
            }
            
            .strengths h4, .weaknesses h4, .improvement-suggestions h4 {
                font-size: 1rem;
            }
            
            .strengths ul, .weaknesses ul, .improvement-suggestions ul {
                font-size: 0.85rem;
                line-height: 1.4;
            }
            
            .restart-btn {
                padding: 12px 25px;
                font-size: 1rem;
                margin-top: 20px;
            }

            /* Welcome Screen Responsive */
            .welcome-title {
                font-size: 1.8rem;
                margin-bottom: 10px;
            }
            
            .welcome-subtitle {
                font-size: 1rem;
                margin-bottom: 15px;
            }
            
            .welcome-description {
                font-size: 0.85rem;
                margin-bottom: 25px;
            }
            
            .start-btn {
                padding: 12px 30px;
                font-size: 1rem;
            }
            .coupang-disclosure {
                font-size: 0.7rem;
            }
        }
    &lt;/style&gt;
&lt;div id=&quot;app&quot; class=&quot;container&quot;&gt;&lt;!-- 앱 내용이 여기에 동적으로 로드됩니다 --&gt;&lt;/div&gt;
&lt;script&gt;
        // 앱 상태 관리
        let currentState = 'welcome'; // 'welcome', 'quiz', 'result'
        let currentQuestionIndex = 0;
        let answers = {};

        // NTRP 질문 데이터
        const questions = {
            &quot;forehand&quot;: {
                &quot;question&quot;: &quot;포핸드를 원하는 곳으로 보낼 수 있나요?&quot;,
                &quot;description&quot;: &quot;포핸드의 방향 제어와 깊이 조절 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;공 때리기도 급급! 라켓에 맞으면 다행이죠.  &quot;,
                    2: &quot;간단한 방향(대충 저~쪽)은 어쩌다 가능해요. 가끔 예상치 못한 곳으로!  &quot;, 
                    3: &quot;중간 속도 공은 원하는 곳으로 척척! 나름 컨트롤 가능!  &quot;,
                    4: &quot;깊이와 방향을 자유자재로 조절! 상대 코트 구석구석을 노려요.  &quot;, 
                    5: &quot;어떤 상황에서도 정확한 포핸드로 게임 지배! 역시 필승!  &quot;
                }
            },
            &quot;backhand&quot;: {
                &quot;question&quot;: &quot;백핸드를 원하는 곳으로 보낼 수 있나요?&quot;,
                &quot;description&quot;: &quot;백핸드의 일관성과 방향 제어 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;백핸드만 오면 몸부터 돌려 포핸드로 냅다 쳐요. 나의 숙제!  &quot;,
                    2: &quot;백핸드를 치긴 하는데 네트행이거나 엉뚱한 방향으로 가요. 삐끗삐끗!  &quot;,
                    3: &quot;중간 속도 공은 백핸드로도 꽤 안정적! 이제 좀 폼이 나는 듯?  &quot;,
                    4: &quot;백핸드로도 방향/깊이 자유자재! 상대방이 약점인 줄 알았대요.  &quot;,
                    5: &quot;포핸드만큼 강력하고 정확한 백핸드로 상대를 압도! 백핸드마스터!  &quot;
                }
            },
            &quot;serve&quot;: {
                &quot;question&quot;: &quot;서브의 정확성과 파워는 어느 정도인가요?&quot;,
                &quot;description&quot;: &quot;첫 서브와 세컨드 서브의 성공률과 효과를 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;일단 서브 넣기만 해도 감지덕지! 더블 폴트는 기본이죠.  &quot;,
                    2: &quot;세컨드 서브는 어떻게든 코트 안에 넣어요. 첫 서브는 모험!  &quot;,
                    3: &quot;첫 서브도 가끔 번개처럼 꽂히고, 세컨드 서브도 안정적! ⚡&quot;,
                    4: &quot;강력한 파워와 날카로운 정확성! 서브 에이스 제조기!  &quot;,
                    5: &quot;압박감 속에서도 에이스 꽂는 믿음직한 서브! 서브는 곧 득점!  &quot;
                }
            },
            &quot;volley&quot;: {
                &quot;question&quot;: &quot;발리와 네트 플레이를 할 수 있나요?&quot;,
                &quot;description&quot;: &quot;네트에서의 편안함과 발리 득점 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;네트 근처 가는 건 좀... 공이 무서워요. �&quot;,
                    2: &quot;간단하게 네트 앞에서 공 받아 넘기는 정도는 해요. 툭!  &quot;,
                    3: &quot;포핸드 발리는 꽤 자신 있어요! 백핸드는 아직 연습 중...  &quot;,
                    4: &quot;발리로 포인트를 직접 따낼 수 있어요! 네트는 나의 구역!  &quot;,
                    5: &quot;네트 앞에서 상대를 압박하고 발리로 게임을 끝냅니다! 발리의 마법사!  &quot;
                }
            },
            &quot;footwork&quot;: {
                &quot;question&quot;: &quot;풋워크와 코트 커버리지는 어느 정도인가요?&quot;,
                &quot;description&quot;: &quot;코트 이동과 포지셔닝 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;공이 오면 멍... 공 따라가는 것도 버거워요.  &quot;,
                    2: &quot;기본적인 좌우 이동은 하지만, 빠르진 않아요. 영차영차!  &quot;,
                    3: &quot;코트 넓이를 어느 정도 커버해요. 이제 좀 테니스 선수 같아요!  &quot;,
                    4: &quot;날렵한 풋워크로 코트 구석구석을 누벼요! 휙휙!  &quot;,
                    5: &quot;프로 선수처럼 코트 전체를 지배하는 풋워크와 포지셔닝! 코트의 지배자!  &quot;
                }
            },
            &quot;strategy&quot;: {
                &quot;question&quot;: &quot;게임 전략을 세우고 실행할 수 있나요?&quot;,
                &quot;description&quot;: &quot;상대방 분석과 전략적 사고 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;그냥 공만 치면 되는 거 아닌가요? 전략은 뭔가요?  &quot;,
                    2: &quot;상대방 약점을 대충 노려볼까...? 정도는 생각해요.  &quot;,
                    3: &quot;상대 약점 파악하고 거기에 맞춰 공격 시도! 제법 전략적!  &quot;,
                    4: &quot;게임을 예측하고 상황에 따라 전략을 유연하게 바꿔요! 이기는 게임은 전략 싸움!  &quot;,
                    5: &quot;어떤 상대방도 완벽 분석, 맞춤 전략으로 승리 쟁취! 테니스 브레인!  &quot;
                }
            },
            &quot;consistency&quot;: {
                &quot;question&quot;: &quot;일관성 있게 랠리를 이어갈 수 있나요?&quot;,
                &quot;description&quot;: &quot;안정적인 샷과 에러 관리 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;3번 이상 랠리 이어가면 기적! 내 공은 어디로...  ‍ &quot;,
                    2: &quot;공은 넘기는데, 속도는 좀 느려요  &quot;, // 문구 수정 및 이모지 변경
                    3: &quot;중간 속도 랠리는 꽤 안정적! 이제 좀 랠리 맛이 나네요!  &quot;,
                    4: &quot;빠른 페이스에서도 안정적! 좀처럼 실수하지 않죠.  ️&quot;,
                    5: &quot;어떤 상황에서도 흔들림 없는 완벽한 일관성! 벽치기 장인!  &quot;
                }
            },
            &quot;mental&quot;: {
                &quot;question&quot;: &quot;게임 중 멘탈 관리가 잘 되나요?&quot;,
                &quot;description&quot;: &quot;압박감과 스트레스 상황에서의 대처 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;실수 한 번 하면 라켓 던지고 싶어져요. 바로 멘탈 붕괴!  &quot;,
                    2: &quot;간단한 게임은 괜찮은데, 지고 있으면 표정 관리가 안 돼요.  &quot;,
                    3: &quot;어느 정도 압박감에 대처 가능! 그래도 속은 부글부글...  &quot;,
                    4: &quot;중요한 순간에도 침착하고 냉정함 유지! 강철 멘탈!  &quot;,
                    5: &quot;어떤 상황에서도 흔들리지 않는 완벽 멘탈로 상대를 압도! 멘탈 갑!  &quot;
                }
            }
        };

        // NTRP 레벨별 위트 있는 설명
        const ntrpDescriptions = {
            1.0: &quot;테니스 신생아: 라켓을 잡고 코트에 첫발을 내디딘 당신! 테니스 세계에 오신 것을 환영합니다!&quot;,
            1.5: &quot;테니스 걸음마: 공을 때리고는 있는데, 어디로 갈지는 공만 아는 단계! 일단 공을 넘기는 게 목표죠.&quot;,
            2.0: &quot;테니스 초급 모험가: 공을 넘기기 시작했지만, 아직 기본기가 시급한 테린이! 랠리는 짧지만, 열정은 만렙!&quot;,
            2.5: &quot;테니스 길찾기 중: 어설프게 공은 치지만, 어디로 갈지, 왜 이렇게 어려운지 고민이 많은 단계. 그래도 랠리의 맛을 조금씩 알아가죠!&quot;,
            3.0: &quot;테니스 기본 다지기: 이제 공은 웬만큼 원하는 곳으로 보내지만, 깊이나 파워는 아직 미완성! 복식에서 '원업 원백'이 익숙한 알뜰 플레이어.&quot;,
            3.5: &quot;테니스 실력 점프 중: 일관성과 컨트롤을 갖추고 슬슬 네트 플레이도 시도하는 단계! '저 사람 꽤 치는데?' 소리 듣기 시작할 때죠.&quot;,
            4.0: &quot;테니스 숙련자 입문: 포핸드, 백핸드 모두 깊이와 컨트롤을 갖추고, 서브도 제법 날카로운 아마추어 고수! 게임의 흐름을 읽기 시작합니다.&quot;,
            4.5: &quot;테니스 전략가 초급: 파워, 스핀, 전략까지 겸비한 테니스 '꾼'! 게임을 이끌어가는 능력이 출중하며, 코트에서 상대방을 괴롭히기 시작합니다.&quot;,
            5.0: &quot;테니스 아마추어 최강자: 강력한 공격과 정교한 기술을 겸비한 아마추어 레벨의 정점! 이제 동네 코트에서는 적수를 찾기 어렵죠.&quot;,
            5.5: &quot;테니스 준프로의 벽: 프로급 기술과 전략을 갖추고 어떤 상황에서도 흔들리지 않는 멘탈의 소유자! 아마추어와 프로의 경계에 서 있습니다.&quot;,
            6.0: &quot;테니스 프로페셔널: 국가대표, 대학 선수, 또는 세계를 무대로 뛰는 진정한 테니스 스타! 이들은 그냥 '테니스 그 자체'입니다.&quot;,
            7.0: &quot;테니스 월드클래스: 세계적 수준의 테니스 스타! 이제 당신은 테니스의 신이 되었습니다.&quot;
        };

        // 앱 상태 관리
        let result = null;

        // 질문 키 배열
        const questionKeys = Object.keys(questions);

        // 앱 렌더링 함수
        function renderApp() {
            const app = document.getElementById('app');
            
            if (currentState === 'welcome') {
                renderWelcome(app);
            } else if (result) {
                renderResult(app);
            } else {
                renderQuestion(app);
            }
        }

        // 환영 화면 렌더링
        function renderWelcome(app) {
            app.innerHTML = `
                &lt;div class=&quot;welcome-screen&quot;&gt;
                    &lt;h1 class=&quot;welcome-title&quot;&gt;&lt;span class=&quot;emoji&quot;&gt; &lt;/span&gt; 당신의 테니스 레벨은?&lt;/h1&gt;
                    &lt;p class=&quot;welcome-subtitle&quot;&gt;간단한 테스트로 지금 바로 확인하세요! &lt;span class=&quot;emoji&quot;&gt; &lt;/span&gt;&lt;/p&gt;
                    &lt;div class=&quot;welcome-description&quot;&gt;
                        &lt;p&gt;내 테니스 실력이 궁금하신가요?&lt;br&gt;
                        이 테스트는 당신의 NTRP 레벨을 쉽고 재미있게 진단해 드립니다.&lt;br&gt;
                        몇 가지 질문에 답하고,&lt;br&gt;
                        포핸드부터 멘탈까지 당신의 강점과 개선점을 한눈에 확인해보세요! &lt;span class=&quot;emoji&quot;&gt; &lt;/span&gt;&lt;/p&gt;
                    &lt;/div&gt;
                    &lt;button class=&quot;start-btn&quot; onclick=&quot;startTest()&quot;&gt;테스트 시작하기&lt;/button&gt;
                    &lt;!-- Coupang Partners Search Bar Ad --&gt;
                    &lt;div class=&quot;coupang-ad-container&quot;&gt;
                        &lt;iframe src=&quot;https://coupa.ng/ciT9UH&quot; width=&quot;100%&quot; height=&quot;44&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&gt;&lt;/iframe&gt;
                    &lt;/div&gt;
                    &lt;!-- Coupang Partners Banner Ad --&gt;
                    &lt;div class=&quot;coupang-banner-ad-container&quot;&gt;
                        &lt;iframe src=&quot;https://ads-partners.coupang.com/widgets.html?id=885878&amp;template=carousel&amp;trackingCode=AF6812978&amp;subId=&amp;width=680&amp;height=140&amp;tsource=&quot; width=&quot;100%&quot; height=&quot;140&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&gt;&lt;/iframe&gt;
                    &lt;/div&gt;
                    &lt;!-- Coupang Partners Disclosure --&gt;
                    &lt;p class=&quot;coupang-disclosure&quot;&gt;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&lt;/p&gt;
                &lt;/div&gt;
            `;
        }

        // 질문 렌더링
        function renderQuestion(app) {
            const currentQuestionKey = questionKeys[currentQuestionIndex];
            const currentQuestion = questions[currentQuestionKey];
            
            app.innerHTML = `
                &lt;h1&gt;  테니스 NTRP 레벨 테스트&lt;/h1&gt;
                
                &lt;div class=&quot;progress-bar&quot;&gt;
                    &lt;div class=&quot;progress-fill&quot; style=&quot;width: ${((currentQuestionIndex + 1) / questionKeys.length) * 100}%&quot;&gt;&lt;/div&gt;
                &lt;/div&gt;
                
                &lt;div class=&quot;question-card&quot;&gt;
                    &lt;h2&gt;질문 ${currentQuestionIndex + 1} / ${questionKeys.length}&lt;/h2&gt;
                    &lt;p class=&quot;question&quot;&gt;${currentQuestion.question}&lt;/p&gt;
                    &lt;p class=&quot;question-description&quot;&gt;${currentQuestion.description}&lt;/p&gt;
                    
                    &lt;div class=&quot;options&quot;&gt;
                        ${Object.entries(currentQuestion.levels).map(([score, description]) =&gt; `
                            &lt;button class=&quot;option ${answers[currentQuestionKey] === parseInt(score) ? 'selected' : ''}&quot; 
                                    onclick=&quot;selectAnswer('${currentQuestionKey}', ${score})&quot;&gt;
                                &lt;!-- Score display removed --&gt;
                                &lt;span class=&quot;description&quot;&gt;${description}&lt;/span&gt;
                            &lt;/button&gt;
                        `).join('')}
                    &lt;/div&gt;
                &lt;/div&gt;

                &lt;div class=&quot;navigation&quot;&gt;
                    ${currentQuestionIndex &gt; 0 ? `
                        &lt;button class=&quot;nav-btn prev&quot; onclick=&quot;previousQuestion()&quot;&gt;이전&lt;/button&gt;
                    ` : ''}
                    
                    ${currentQuestionIndex &lt; questionKeys.length - 1 ? `
                        &lt;button class=&quot;nav-btn next&quot; onclick=&quot;nextQuestion()&quot; 
                                ${!answers[currentQuestionKey] ? 'disabled' : ''}&gt;다음&lt;/button&gt;
                    ` : `
                        &lt;button class=&quot;nav-btn submit&quot; onclick=&quot;calculateResult()&quot; 
                                ${Object.keys(answers).length &lt; questionKeys.length ? 'disabled' : ''}&gt;결과 보기&lt;/button&gt;
                    `}
                &lt;/div&gt;
            `;
        }

        // 결과 렌더링
        function renderResult(app) {
            app.innerHTML = `
                &lt;h1&gt;  NTRP 테스트 결과&lt;/h1&gt;
                &lt;div class=&quot;result-card&quot;&gt;
                    &lt;h2&gt;당신의 NTRP 레벨은&lt;/h2&gt;
                    &lt;div class=&quot;ntrp-level&quot;&gt;${result.ntrp_level}&lt;/div&gt;
                    &lt;p class=&quot;description&quot;&gt;${result.description}&lt;/p&gt;
                    &lt;div class=&quot;stats&quot;&gt;
                        &lt;p&gt;평균 점수: ${result.average_score}/5&lt;/p&gt;
                        &lt;p&gt;응답한 질문: ${result.total_questions}개&lt;/p&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
                
                &lt;div class=&quot;detailed-analysis&quot;&gt;
                    &lt;h3&gt;  상세 분석&lt;/h3&gt;
                    
                    &lt;div class=&quot;category-scores&quot;&gt;
                        &lt;h4&gt;영역별 점수&lt;/h4&gt;
                        &lt;div class=&quot;score-grid&quot;&gt;
                            ${Object.entries(result.category_scores).map(([category, score]) =&gt; `
                                &lt;div class=&quot;score-item&quot;&gt;
                                    &lt;span class=&quot;category-name&quot;&gt;${category}&lt;/span&gt;
                                    &lt;div class=&quot;score-bar&quot;&gt;
                                        &lt;div class=&quot;score-fill&quot; style=&quot;width: ${(score / 5) * 100}%&quot;&gt;&lt;/div&gt;
                                    &lt;/div&gt;
                                    &lt;span class=&quot;score-value&quot;&gt;${score}/5&lt;/span&gt;
                                &lt;/div&gt;
                            `).join('')}
                        &lt;/div&gt;
                    &lt;/div&gt;
                    
                    &lt;div class=&quot;strengths-weaknesses&quot;&gt;
                        ${result.strengths &amp;&amp; result.strengths.length &gt; 0 ? `
                            &lt;div class=&quot;strengths&quot;&gt;
                                &lt;h4&gt;  강점&lt;/h4&gt;
                                &lt;ul&gt;
                                    ${result.strengths.map(strength =&gt; `&lt;li&gt;${strength}&lt;/li&gt;`).join('')}
                                &lt;/ul&gt;
                            &lt;/div&gt;
                        ` : ''}
                        
                        ${result.weaknesses &amp;&amp; result.weaknesses.length &gt; 0 ? `
                            &lt;div class=&quot;weaknesses&quot;&gt;
                                &lt;h4&gt;  개선점&lt;/h4&gt;
                                &lt;ul&gt;
                                    ${result.weaknesses.map(weakness =&gt; `&lt;li&gt;${weakness}&lt;/li&gt;`).join('')}
                                &lt;/ul&gt;
                            &lt;/div&gt;
                        ` : ''}
                    &lt;/div&gt;
                    
                    ${result.improvement_suggestions ? `
                        &lt;div class=&quot;improvement-suggestions&quot;&gt;
                            &lt;h4&gt;  개선 제안&lt;/h4&gt;
                            &lt;ul&gt;
                                ${result.improvement_suggestions.map(suggestion =&gt; `&lt;li&gt;${suggestion}&lt;/li&gt;`).join('')}
                            &lt;/ul&gt;
                        &lt;/div&gt;
                    ` : ''}
                &lt;/div&gt;
                
                &lt;button class=&quot;restart-btn&quot; onclick=&quot;restartTest()&quot;&gt;다시 테스트하기&lt;/button&gt;
                &lt;!-- Coupang Partners Search Bar Ad --&gt;
                &lt;div class=&quot;coupang-ad-container&quot;&gt;
                    &lt;iframe src=&quot;https://coupa.ng/ciT9UH&quot; width=&quot;100%&quot; height=&quot;44&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&gt;&lt;/iframe&gt;
                &lt;/div&gt;
                &lt;!-- Coupang Partners Banner Ad --&gt;
                &lt;div class=&quot;coupang-banner-ad-container&quot;&gt;
                    &lt;iframe src=&quot;https://ads-partners.coupang.com/widgets.html?id=885878&amp;template=carousel&amp;trackingCode=AF6812978&amp;subId=&amp;width=680&amp;height=140&amp;tsource=&quot; width=&quot;100%&quot; height=&quot;140&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&gt;&lt;/iframe&gt;
                &lt;/div&gt;
                &lt;!-- Coupang Partners Disclosure --&gt;
                &lt;p class=&quot;coupang-disclosure&quot;&gt;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&lt;/p&gt;
            `;
        }

        // Test start
        function startTest() {
            currentState = 'quiz';
            currentQuestionIndex = 0;
            answers = {};
            result = null;
            renderApp();
        }

        // Select answer
        function selectAnswer(questionKey, score) {
            answers[questionKey] = score;
            renderApp();
        }

        // Next question
        function nextQuestion() {
            if (currentQuestionIndex &lt; questionKeys.length - 1) {
                currentQuestionIndex++;
                renderApp();
            }
        }

        // Previous question
        function previousQuestion() {
            if (currentQuestionIndex &gt; 0) {
                currentQuestionIndex--;
                renderApp();
                // Restore selected state after rendering
                const currentQuestionKey = questionKeys[currentQuestionIndex];
                const selectedScore = answers[currentQuestionKey];
                if (selectedScore) {
                    const options = document.querySelectorAll('.option');
                    options.forEach(option =&gt; {
                        if (questions[currentQuestionKey].levels[selectedScore] === option.querySelector('.description').innerText) {
                            option.classList.add('selected');
                        } else {
                            option.classList.remove('selected');
                        }
                    });
                }
            }
        }

        // Calculate result
        function calculateResult() {
            const totalScore = Object.values(answers).reduce((sum, score) =&gt; sum + score, 0);
            const averageScore = totalScore / Object.keys(answers).length;
            
            // Determine NTRP level
            let ntrpLevel;
            if (averageScore &lt;= 1.25) ntrpLevel = 1.0;
            else if (averageScore &lt;= 1.75) ntrpLevel = 1.5;
            else if (averageScore &lt;= 2.25) ntrpLevel = 2.0;
            else if (averageScore &lt;= 2.75) ntrpLevel = 2.5;
            else if (averageScore &lt;= 3.25) ntrpLevel = 3.0;
            else if (averageScore &lt;= 3.75) ntrpLevel = 3.5;
            else if (averageScore &lt;= 4.25) ntrpLevel = 4.0;
            else if (averageScore &lt;= 4.75) ntrpLevel = 4.5;
            else ntrpLevel = 5.0; // If average score is above 4.75, it's 5.0

            // Detailed analysis
            const categoryNames = {
                &quot;forehand&quot;: &quot;포핸드&quot;,
                &quot;backhand&quot;: &quot;백핸드&quot;,
                &quot;serve&quot;: &quot;서브&quot;,
                &quot;volley&quot;: &quot;발리&quot;,
                &quot;footwork&quot;: &quot;풋워크&quot;,
                &quot;strategy&quot;: &quot;전략&quot;,
                &quot;consistency&quot;: &quot;일관성&quot;,
                &quot;mental&quot;: &quot;멘탈&quot;
            };

            const categoryScores = {};
            Object.keys(categoryNames).forEach(category =&gt; {
                if (answers[category]) {
                    categoryScores[categoryNames[category]] = answers[category];
                }
            });

            // Analyze strengths and weaknesses
            const strengths = [];
            const weaknesses = [];
            Object.entries(categoryScores).forEach(([category, score]) =&gt; {
                if (score &gt;= 4) strengths.push(category);
                else if (score &lt;= 2) weaknesses.push(category);
            });

            // Improvement suggestions
            const improvementSuggestions = [];
            if (weaknesses.includes(&quot;포핸드&quot;)) {
                improvementSuggestions.push(&quot;포핸드 기본기 연습이 필요합니다. 스윙 폼과 타점을 중점적으로 연습해보세요.&quot;);
            }
            if (weaknesses.includes(&quot;백핸드&quot;)) {
                improvementSuggestions.push(&quot;백핸드 그립과 준비 동작을 연습해보세요. 백핸드를 피하지 말고 적극적으로 연습하세요.&quot;);
            }
            if (weaknesses.includes(&quot;서브&quot;)) {
                improvementSuggestions.push(&quot;서브 연습을 통해 정확성과 파워를 향상시켜보세요. 세컨드 서브부터 안정화하세요.&quot;);
            }
            if (weaknesses.includes(&quot;발리&quot;)) {
                improvementSuggestions.push(&quot;네트 플레이에 익숙해지세요. 간단한 발리부터 시작해서 점진적으로 발전시키세요.&quot;);
            }
            if (weaknesses.includes(&quot;풋워크&quot;)) {
                improvementSuggestions.push(&quot;기본적인 이동 연습이 필요합니다. 사이드 스텝과 크로스오버 스텝을 연습하세요.&quot;);
            }
            if (weaknesses.includes(&quot;전략&quot;)) {
                improvementSuggestions.push(&quot;게임 중 상대방을 관찰하고 약점을 찾아보세요. 다양한 샷 조합을 연습하세요.&quot;);
            }
            if (weaknesses.includes(&quot;일관성&quot;)) {
                improvementSuggestions.push(&quot;안정적인 샷을 위해 기본기를 탄탄히 하세요. 에러를 줄이는 연습이 중요합니다.&quot;);
            }
            if (weaknesses.includes(&quot;멘탈&quot;)) {
                improvementSuggestions.push(&quot;압박감에 대처하는 연습이 필요합니다. 실수를 해도 포기하지 않는 마음가짐이 중요합니다.&quot;);
            }
            if (improvementSuggestions.length === 0) {
                improvementSuggestions.push(&quot;전반적으로 좋은 실력을 보유하고 있습니다. 더 높은 레벨을 위해 고급 기술을 연습해보세요.&quot;);
            }

            result = {
                ntrp_level: ntrpLevel,
                description: ntrpDescriptions[ntrpLevel] || &quot;정의되지 않은 레벨&quot;,
                average_score: Math.round(averageScore * 10) / 10,
                total_questions: Object.keys(answers).length,
                category_scores: categoryScores,
                strengths: strengths,
                weaknesses: weaknesses,
                improvement_suggestions: improvementSuggestions
            };

            renderApp();
        }

        // Restart test
        function restartTest() {
            currentState = 'welcome';
            currentQuestionIndex = 0;
            answers = {};
            result = null;
            renderApp();
        }

        // Initialize app
        renderApp();
    &lt;/script&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;&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/5</guid>
      <comments>https://quinton84.tistory.com/5#entry5comment</comments>
      <pubDate>Sun, 6 Jul 2025 22:25:30 +0900</pubDate>
    </item>
    <item>
      <title>  누구나 따라 할 수 있는 테니스 NTRP 레벨 테스트 웹 앱 만들기 가이드</title>
      <link>https://quinton84.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;안녕하세요, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;오늘은 여러분도 직접 만들어 볼 수 있는 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;'테니스 NTRP 레벨 테스트 웹 앱'&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt; 개발 과정을 상세히 안내해 드리려고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;내 테니스 실력이 객관적으로 어느 정도인지 궁금하셨나요? NTRP(National Tennis Rating Program) 레벨은 테니스 실력을 가늠하는 좋은 기준입니다. 이 가이드를 통해 NTRP 레벨을 쉽고 재미있게 진단해주는 나만의 웹 앱을 직접 만들어보고, 웹 개발의 즐거움을 경험해 보세요! 코딩 초보자도 충분히 따라 할 수 있도록 자세히 설명해 드립니다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;✨ 우리가 만들 웹 앱은 이런 모습이에요!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;이 프로젝트는 사용자가 몇 가지 질문에 답하면, 자신의 테니스 NTRP 레벨을 예측해주고 상세한 분석 결과를 제공하는 웹 기반 애플리케이션입니다. 최종적으로는 이런 특징들을 가진 웹 앱을 만들게 될 거예요.&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;친근하고 위트 있는 NTRP 레벨 설명&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 딱딱한 설명 대신, 각 레벨을 '테니스 신생아', '테니스 걸음마', '테니스 전략가' 등 재미있는 별명으로 표현합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;8가지 상세 영역 평가&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 포핸드, 백핸드, 서브, 발리, 풋워크, 전략, 일관성, 멘탈까지 테니스의 주요 기술 및 심리적 요소를 종합적으로 평가합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;직관적이고 깔끔한 디자인&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 모바일과 데스크톱 모두에서 쾌적하게 사용할 수 있는 반응형 디자인이 적용됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;상세 분석 리포트&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: NTRP 레벨뿐만 아니라, 각 영역별 점수(시각적인 바 차트 포함), 강점, 개선점, 그리고 개인 맞춤형 개선 제안까지 제공하여 실력 향상에 실질적인 도움을 줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;광고 연동 가능&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 쿠팡 파트너스 같은 제휴 광고를 붙여 수익 창출 가능성도 열어둘 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;우선 최종 결과물 부터 보시죠!&lt;/span&gt;&lt;/h2&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://tangerine-sunflower-94358a.netlify.app/&quot;&gt;https://tangerine-sunflower-94358a.netlify.app/&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-7578665530606531&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;  단계별 웹 앱 개발 가이드: 지금 바로 시작해 보세요!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;이 웹 앱은 HTML, CSS, JavaScript만을 사용하여 만들어지는 '정적 웹사이트' 형태입니다. 복잡한 서버나 데이터베이스 없이도 모든 기능이 웹 브라우저에서 직접 실행되도록 설계됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;1단계: 프로젝트 기획 및 자료 수집 (무엇을 만들까?)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;웹 앱을 만들기 전, 어떤 내용을 담을지 구체적으로 계획하고 필요한 정보를 모으는 단계입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;NTRP 레벨별 특징 이해&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;USTA(미국 테니스 협회)의 NTRP 공식 가이드라인 문서를 찾아 읽어보세요. 각 레벨의 일반적인 특성, 스트로크, 전술, 멘탈 등의 설명을 파악합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VCBUq/btsO65RI6pg/ipS9hKk2KKsnnGPpDwaVzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VCBUq/btsO65RI6pg/ipS9hKk2KKsnnGPpDwaVzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VCBUq/btsO65RI6pg/ipS9hKk2KKsnnGPpDwaVzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVCBUq%2FbtsO65RI6pg%2FipS9hKk2KKsnnGPpDwaVzK%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;933&quot; height=&quot;718&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;718&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;국내외 테니스 관련 블로그, 커뮤니티, 웹사이트에서 NTRP 레벨에 대한 설명이나 기존 테스트들을 참고하여 실제 테니스 플레이어들이 느끼는 체감 레벨과 특징을 이해합니다. (예: NAMOO_BINS 나무로 만든 집 블로그 같은 위트 있는 NTRP 정의를 참고하면 좋습니다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;질문 및 답변 구체화&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;수집된 자료를 바탕으로 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;포핸드, 백핸드, 서브, 발리, 풋워크, 전략, 일관성, 멘탈&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt; 등 8가지 핵심 테니스 영역을 선정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;각 영역별로 1점부터 5점까지의 구체적인 답변 문구를 작성합니다. 이때, 사용자가 자신의 실력을 직관적이고 재미있게 선택할 수 있도록 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;친근하고 위트 있는 표현&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;을 사용하는 것이 중요합니다. (예: &quot;공 때리기도 급급!&quot;, &quot;백핸드만 오면 몸부터 돌려 포핸드로 냅다 쳐요.&quot; 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;2단계: 웹 페이지 구조 만들기 (HTML: 앱의 뼈대)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;이제 웹 앱의 기본 구조를 HTML 파일 하나로 만들어 봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;index.html 파일 생성&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 컴퓨터에 index.html이라는 이름의 빈 텍스트 파일을 만듭니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;기본 HTML 코드 준비&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 아래와 같은 기본적인 HTML 구조를 index.html 파일에 작성합니다. 이 코드는 웹 앱의 전체적인 틀과 각 화면(환영, 질문, 결과)을 동적으로 표시할 div 요소를 포함합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;팁&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: &amp;lt;head&amp;gt; 태그 안에 &amp;lt;meta name=&quot;description&quot; ...&amp;gt;을 추가하여 검색 엔진 최적화(SEO)를 위한 페이지 요약을 미리 넣어두세요.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;팁&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: Google Fonts의 'Poppins' 폰트를 링크하여 현대적인 디자인을 위한 준비를 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751811604868&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;  테니스 NTRP 레벨 테스트&amp;lt;/title&amp;gt;
    &amp;lt;!-- SEO를 위한 페이지 요약 추가 --&amp;gt;
    &amp;lt;meta name=&quot;description&quot; content=&quot;나만의 테니스 NTRP 레벨을 쉽고 재미있게 진단하고, 포핸드, 백핸드, 멘탈까지 상세한 강점과 개선점, 맞춤형 훈련 제안을 받아보세요!&quot;&amp;gt;
    &amp;lt;!-- Google Fonts: Poppins --&amp;gt;
   
    &amp;lt;style&amp;gt;
        /* 여기에 CSS 코드가 들어갈 예정입니다 */
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;container&quot; id=&quot;app&quot;&amp;gt;
        &amp;lt;!-- 앱 내용이 여기에 JavaScript로 동적으로 로드됩니다 --&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        /* 여기에 JavaScript 코드가 들어갈 예정입니다 */
    &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;color: #1b1c1d;&quot;&gt;3단계: 디자인 및 스타일링 (CSS: 앱에 옷 입히기)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;만든 뼈대에 예쁜 옷을 입혀 사용자 인터페이스를 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;CSS 코드 작성&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 2단계에서 만든 index.html 파일의 &amp;lt;style&amp;gt; 태그 안에 아래 CSS 코드를 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;핵심&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: align-items: center;와 padding, min-height, gap 등의 속성들을 통해 모든 답변 옵션이 스크롤 없이 한 화면에 보이고, 텍스트가 상자 내에서 세로 중앙에 완벽하게 정렬되도록 세심하게 조정되었습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;팁&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: rem 단위를 사용하여 폰트 크기를 조절하면 반응형 디자인에 유리합니다. box-shadow와 border-radius로 부드러운 입체감을 줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;4단계: 테스트 로직 구현 (JavaScript: 앱에 생명 불어넣기)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;웹 앱이 실제로 작동하도록 생명을 불어넣는 단계입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;JavaScript 코드 작성&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 2단계에서 만든 index.html 파일의 &amp;lt;script&amp;gt; 태그 안에 아래 JavaScript 코드를 작성합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;핵심&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 질문 데이터 관리, 답변 선택 처리, 진행 상황 표시, NTRP 레벨 계산, 상세 분석 결과 생성 등 모든 핵심 기능을 구현합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;팁&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 사용자의 답변에 따라 웹 페이지의 내용이 동적으로 변경되도록 로직을 작성하여 부드러운 사용자 경험을 제공합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;5단계: 피드백 반영 및 개선 (반복 과정)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;더 좋은 웹 앱을 만들기 위한 필수적인 단계입니다.&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;개발 중 또는 배포 후 사용자 피드백을 적극적으로 수렴합니다. 예를 들어, &quot;첫 화면 문구가 너무 길다&quot;, &quot;답변 옵션이 스크롤된다&quot;, &quot;정렬이 맞지 않는다&quot;, &quot;이모지가 깨진다&quot; 등 다양한 의견을 반영합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;이러한 피드백을 바탕으로 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;문구를 간결하게 다듬고, CSS를 정밀하게 조정하며, 이모지 호환성을 개선&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;하는 과정을 여러 차례 반복합니다. 특히, 답변 옵션의 세로 중앙 정렬과 결과 페이지의 영역별 점수 바 시각화는 시각적 완성도를 높이는 데 중요한 부분입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;6단계: 광고 연동 (선택 사항 - 쿠팡 파트너스)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;만든 웹 앱으로 수익을 창출하고 싶다면 시도해 볼 수 있는 단계입니다.&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;웹 앱에 쿠팡 파트너스 광고를 연동하여 수익 창출 가능성을 열 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;쿠팡 파트너스에서 제공하는 광고 코드를 HTML 파일의 적절한 위치에 삽입합니다. (예: 환영 화면과 결과 화면 하단)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;광고 삽입 유형 예시&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;검색 바 위젯&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 사용자가 직접 검색어를 입력하여 쿠팡 상품을 찾을 수 있는 형태입니다.&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;배너 위젯 (다이내믹 위젯)&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 특정 상품 캐러셀이나 추천 상품 배너 형태로 표시됩니다&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;필수 고지 문구 추가&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: &quot;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&quot; 와 같은 문구를 웹 앱 내에 명시하여 투명성을 확보해야 합니다. (광고 활성화는 쿠팡 파트너스 심사 완료 후 가능합니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;활동 시 주의 사항&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;게시글 작성 시 필수 문구 기재&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 쿠팡 파트너스 활동은 공정거래위원회의 심사지침에 따라 추천, 보증인인 파트너스 회원과 당사의 경제적 이해관계를 공개해야 합니다. 따라서 광고가 삽입된 모든 페이지나 게시글에는 위 고지 문구를 반드시 기재해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;불법 스팸 전송 행위 유의&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 바로가기 아이콘 등을 이용 시, 수신자의 사전 동의를 얻지 않은 메신저, SNS 등으로 메시지를 발송하는 행위는 불법 스팸 전송 행위로 간주되어 규제기관의 행정제재(3천만원 이하의 과태료) 또는 형사 처벌(1년 이하의 징역 또는 1천만원 이하의 벌금)의 대상이 될 수 있으니 이 점 유의하시기 바랍니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;7단계: 웹 배포 및 SEO 최적화 (전 세계에 공개하고 잘 보이게 하기!)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;만든 웹 앱을 인터넷에 공개하여 누구나 접속할 수 있도록 하고, 검색 엔진에서 잘 발견되도록 돕는 단계입니다.&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 style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;완성된 index.html 파일을 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;GitHub Pages&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt; 또는 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Netlify&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;와 같은 정적 웹 호스팅 서비스를 통해 웹에 배포합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;GitHub Pages를 통한 배포 방법&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;GitHub 저장소 생성&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: GitHub.com에 로그인 후, 새로운 'Public' 저장소(Repository)를 생성합니다. 저장소 이름은 나중에 웹사이트 URL의 일부가 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;파일 업로드&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 생성된 저장소에 웹 앱을 구성하는 모든 파일(index.html, CSS, JS 등)을 업로드합니다. 이때, index.html 파일은 저장소의 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;최상위(root) 폴더&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;에 위치해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;GitHub Pages 설정&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 저장소 페이지에서 'Settings' 탭으로 이동한 후, 왼쪽 메뉴에서 'Pages'를 클릭합니다. 'Branch' 섹션에서 웹사이트를 배포할 브랜치(일반적으로 main 또는 master)를 선택하고, 'Folder'는 / (root)를 선택한 후 'Save' 버튼을 클릭합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;URL 확인&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 몇 분 후, 'Your site is published at...' 메시지와 함께 https://[당신의GitHub아이디].github.io/[저장소이름]/ 형식의 웹사이트 URL이 생성됩니다. 이 URL을 통해 접속할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Netlify를 통한 배포 방법&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Netlify 계정 생성/로그인&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: Netlify.com에 접속하여 계정을 만들거나 로그인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;새 사이트 배포 시작&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: Netlify 대시보드에서 'Add new site' 또는 'New site from Git' 버튼을 클릭합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;GitHub 연결 (권장)&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 'Deploy with GitHub'를 선택하여 GitHub 계정을 Netlify에 연결하고, 배포할 저장소를 선택합니다. (이 방법은 GitHub 저장소에 변경사항이 발생할 때마다 Netlify가 자동으로 재배포해 주어 매우 편리합니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;수동 드래그 앤 드롭 (간편)&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: GitHub 연결 없이, 로컬 컴퓨터에 있는 웹 앱 파일들이 담긴 폴더(예: ntrp/static 폴더의 내용물)를 Netlify 대시보드의 드래그 앤 드롭 영역으로 직접 끌어다 놓아도 배포가 가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;배포 설정 확인&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 빌드 설정(Build settings)에서 'Build command'는 비워두고, 'Publish directory'는 . (현재 폴더)로 설정합니다. (정적 HTML 파일의 경우 별다른 빌드 과정이 필요 없기 때문입니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;URL 확인&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 배포가 완료되면 Netlify가 자동으로 생성해주는 URL을 통해 웹 앱에 접속할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;SEO (검색 엔진 최적화)를 위한 추가 노력&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;&amp;lt;meta name=&quot;description&quot;&amp;gt; 추가&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: index.html 파일의 &amp;lt;head&amp;gt; 태그 안에 &amp;lt;meta name=&quot;description&quot; content=&quot;나만의 테니스 NTRP 레벨을 쉽고 재미있게 진단하고, 포핸드, 백핸드, 멘탈까지 상세한 강점과 개선점, 맞춤형 훈련 제안을 받아보세요!&quot;&amp;gt; 와 같이 페이지를 요약하는 설명을 추가합니다. 이 내용은 검색 결과에 표시될 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Google Search Console 활용&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;a href=&quot;https://search.google.com/search-console/&quot;&gt;&lt;span style=&quot;color: #0b57d0;&quot;&gt;Google Search Console&lt;/span&gt;&lt;/a&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;에 접속하여 당신의 구글 계정으로 로그인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;당신의 웹 앱 URL을 '속성'으로 추가하고 소유권을 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;URL 검사 도구 사용&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 'URL 검사' 기능을 사용하여 당신의 웹 앱 URL을 입력하고 **'색인 생성 요청'**을 하면 구글이 당신의 페이지를 더 빨리 발견하고 검색 결과에 포함하도록 요청할 수 있습니다. 이 과정은 검색 노출 속도를 높이는 데 매우 효과적입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;성능 보고서 모니터링&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 서치 콘솔에서 어떤 키워드로 사용자들이 웹 앱에 접속하는지, 클릭률은 어떤지 등을 지속적으로 확인하여 SEO 전략을 개선할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;블로그/SNS 공유&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 지금 작성하는 이 블로그 글처럼, 웹 앱에 대한 설명을 자세히 쓰고 링크를 걸어두면 검색 엔진이 웹 앱을 더 쉽게 발견하고 중요하게 인식할 수 있습니다. 다양한 테니스 커뮤니티나 SNS에도 웹 앱 링크를 자연스럽게 공유하여 트래픽을 유도합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;  주요 특징 및 장점&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;친근하고 위트 있는 NTRP 레벨 설명&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 딱딱한 설명 대신, 각 레벨을 '테니스 신생아', '테니스 걸음마', '테니스 전략가' 등 재미있는 별명으로 표현합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;8가지 상세 영역 평가&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 포핸드, 백핸드, 서브, 발리, 풋워크, 전략, 일관성, 멘탈까지 테니스의 주요 기술 및 심리적 요소를 종합적으로 평가합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;직관적인 사용자 인터페이스&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 깔끔하고 현대적인 디자인으로 누구나 쉽게 테스트를 진행할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;상세 분석 리포트&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: NTRP 레벨뿐만 아니라, 각 영역별 점수(시각적인 바 차트 포함), 강점, 개선점, 그리고 개인 맞춤형 개선 제안까지 제공하여 실력 향상에 실질적인 도움을 줍니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;완벽한 반응형 디자인&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 스마트폰부터 24인치 모니터까지, 어떤 기기에서도 최적화된 화면으로 쾌적하게 이용할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;쉬운 배포 및 접근성&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: GitHub Pages나 Netlify를 통해 간단하게 웹에 배포되어 누구나 쉽게 접근하고 공유할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;  웹 앱 사용 방법&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;배포된 웹 앱 URL에 접속합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;환영 화면에서 '테스트 시작하기' 버튼을 클릭합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;각 질문에 대해 자신의 실력에 가장 잘 맞는 답변을 선택합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;모든 질문에 답한 후 '결과 보기' 버튼을 클릭하면, 당신의 NTRP 레벨과 상세 분석 리포트를 확인할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;'다시 테스트하기' 버튼을 눌러 언제든지 재시도할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;맺음말&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;이 가이드가 여러분이 직접 테니스 NTRP 레벨 테스트 웹 앱을 만들고 배포하는 데 도움이 되기를 바랍니다. 웹 개발은 생각보다 어렵지 않으며, 작은 아이디어로도 멋진 결과물을 만들 수 있습니다. 궁금한 점이나 추가적인 피드백은 언제든지 환영합니다. 여러분의 즐거운 코딩과 테니스 생활을 응원합니다! 감사합니다!&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 코드&lt;/p&gt;
&lt;pre id=&quot;code_1751812044394&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;  테니스 NTRP 레벨 테스트&amp;lt;/title&amp;gt;
    &amp;lt;!-- Google Fonts: Poppins --&amp;gt;
    &amp;lt;link href=&quot;https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&amp;amp;display=swap&quot; rel=&quot;stylesheet&quot;&amp;gt;
    &amp;lt;style&amp;gt;
        :root {
            --primary-gradient: linear-gradient(135deg, #6A11CB 0%, #2575FC 100%);
            --primary-color-start: #6A11CB;
            --primary-color-end: #2575FC;
            --text-dark: #222;
            --text-medium: #555;
            --text-light: #777;
            --bg-light: #f8f9fa;
            --border-light: #e0e0e0;
            --shadow-light: 0 10px 30px rgba(0, 0, 0, 0.08);
            --shadow-medium: 0 15px 45px rgba(0, 0, 0, 0.15);
            --shadow-button-hover: 0 10px 25px rgba(106, 17, 203, 0.4);
        }

        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Poppins', sans-serif;
            background: var(--primary-gradient);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            background: white;
            border-radius: 25px;
            padding: 40px;
            box-shadow: var(--shadow-medium);
            max-width: 650px;
            width: 100%;
            text-align: center;
            margin: 20px;
            min-height: 85vh;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }

        h1 {
            color: var(--text-dark);
            margin-bottom: 30px;
            font-size: 2.8rem;
            font-weight: 700;
            line-height: 1.2;
            letter-spacing: -0.03em;
        }
        
        h1 .emoji {
            display: inline-block;
            margin-right: 5px;
        }

        .progress-bar {
            width: 100%;
            height: 10px;
            background: var(--border-light);
            border-radius: 5px;
            margin-bottom: 35px;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: var(--primary-gradient);
            transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
            border-radius: 5px;
        }

        /* Question card and option spacing/sizing adjustments */
        .question-card {
            margin-bottom: 20px;
            flex-grow: 1; 
            display: flex;
            flex-direction: column;
            justify-content: flex-start;
        }

        .question-card h2 {
            color: var(--text-medium);
            margin-bottom: 10px;
            font-size: 1.2rem;
        }

        .question {
            font-size: 1.5rem;
            color: var(--text-dark);
            margin-bottom: 10px;
            line-height: 1.4;
            font-weight: 600;
        }

        .question-description {
            font-size: 1rem;
            color: var(--text-light);
            margin-bottom: 25px;
            line-height: 1.5;
            font-style: normal;
        }

        .options {
            display: flex;
            flex-direction: column;
            gap: 8px; /* Reduced gap */
            margin-bottom: 15px;
        }

        .option {
            display: flex;
            align-items: center; /* 핵심: 세로 중앙 정렬 */
            padding: 12px 20px; /* Reduced padding */
            border: 2px solid var(--border-light);
            border-radius: 12px;
            background: white;
            cursor: pointer;
            transition: all 0.3s ease;
            text-align: left;
            min-height: 50px; /* Reduced min-height */
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.04);
        }

        .option:hover {
            border-color: var(--primary-color-start);
            transform: translateY(-2px);
            box-shadow: 0 6px 15px rgba(0, 0, 0, 0.08);
        }

        .option.selected {
            border-color: var(--primary-color-end);
            background: var(--primary-gradient);
            color: white;
            transform: translateY(-1px);
            box-shadow: 0 4px 10px rgba(106, 17, 203, 0.3);
        }

        /* Score display removed, so .option .score style is now unused */
        /* .option .score { display: none; } */ 

        .option .description {
            flex: 1; /* Takes up remaining space */
            font-size: 0.95rem; /* Slightly reduced font size */
            line-height: 1.4;
            display: flex;
            align-items: center; /* Ensures text remains vertically centered */
        }

        .navigation {
            display: flex;
            justify-content: space-between;
            gap: 15px;
            margin-top: 15px;
        }

        .nav-btn {
            padding: 12px 25px;
            border: none;
            border-radius: 10px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            flex: 1;
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .nav-btn.prev {
            background: var(--bg-light);
            color: var(--text-medium);
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06);
        }

        .nav-btn.prev:hover {
            background: #e0e0e0;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.09);
        }

        .nav-btn.next,
        .nav-btn.submit {
            background: var(--primary-gradient);
            color: white;
            box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);
        }

        .nav-btn.next:hover,
        .nav-btn.submit:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(106, 17, 203, 0.4);
        }

        .nav-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }

        .result-card {
            background: var(--primary-gradient);
            color: white;
            border-radius: 20px;
            padding: 30px;
            margin-bottom: 20px;
            text-align: center;
            box-shadow: var(--shadow-medium);
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .result-card h2 {
            margin-bottom: 20px;
            font-size: 1.6rem;
        }

        .ntrp-level {
            font-size: 4.5rem;
            font-weight: 800;
            margin: 20px 0;
            text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
            line-height: 1;
        }

        .description {
            font-size: 1.2rem;
            margin-bottom: 20px;
            line-height: 1.5;
        }

        .stats {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            padding: 15px;
            margin-top: 15px;
        }

        .stats p {
            margin: 5px 0;
            font-size: 1rem;
        }

        .detailed-analysis {
            background: var(--bg-light);
            border-radius: 20px;
            padding: 30px;
            margin: 20px 0;
            text-align: left;
            box-shadow: var(--shadow-light);
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
        }

        .detailed-analysis h3 {
            color: var(--text-dark);
            margin-bottom: 25px;
            font-size: 1.6rem;
            font-weight: 600;
            text-align: center;
        }

        .detailed-analysis h4 {
            color: var(--text-medium);
            margin-bottom: 15px;
            font-size: 1.2rem;
            font-weight: 600;
            text-align: center;
        }

        /* Score Grid (2 columns for desktop) */
        .score-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr); /* 2 columns for desktop */
            gap: 12px;
            margin-bottom: 25px;
        }

        .score-item {
            display: flex;
            align-items: center; /* Vertically centers content within item */
            gap: 10px;
            padding: 14px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 3px 8px rgba(0, 0, 0, 0.04);
        }

        .category-name {
            min-width: 70px;
            font-weight: 600;
            color: var(--text-dark);
            text-align: left;
            flex-shrink: 0;
            font-size: 0.95rem;
        }

        .score-bar {
            flex-grow: 1;
            height: 12px;
            background: var(--border-light);
            border-radius: 6px;
            overflow: hidden;
        }

        .score-fill {
            height: 100%;
            background: var(--primary-gradient);
            transition: width 0.5s cubic-bezier(0.25, 0.8, 0.25, 1);
            border-radius: 6px;
        }

        .score-value {
            min-width: 35px;
            font-weight: 600;
            color: var(--text-dark);
            text-align: right;
            flex-shrink: 0;
            font-size: 0.95rem;
        }

        .strengths-weaknesses {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin-bottom: 25px;
        }

        .strengths, .weaknesses {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
            text-align: center;
        }

        .strengths h4 {
            color: #28a745;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .weaknesses h4 {
            color: #dc3545;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .strengths ul, .weaknesses ul {
            list-style: none;
            padding: 0;
        }

        .strengths li, .weaknesses li {
            padding: 8px 0;
            border-bottom: 1px solid #eee;
            font-size: 1rem;
            color: var(--text-medium);
            line-height: 1.4;
        }

        .strengths li:last-child, .weaknesses li:last-child {
            border-bottom: none;
        }

        .improvement-suggestions {
            background: white;
            padding: 20px;
            border-radius: 12px;
            box-shadow: 0 3px 10px rgba(0, 0, 0, 0.06);
            text-align: center;
            margin-top: 20px;
        }

        .improvement-suggestions h4 {
            color: #007bff;
            font-weight: 700;
            margin-bottom: 12px;
        }

        .improvement-suggestions ul {
            list-style: none;
            padding: 0;
        }

        .improvement-suggestions li {
            padding: 10px 0;
            border-bottom: 1px solid #eee;
            line-height: 1.6;
            font-size: 1rem;
            color: var(--text-medium);
        }

        .improvement-suggestions li:last-child {
            border-bottom: none;
        }

        .restart-btn {
            background: var(--primary-gradient);
            color: white;
            border: none;
            padding: 15px 40px;
            border-radius: 12px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.3s ease;
            margin-top: 30px;
            box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .restart-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 8px 20px rgba(106, 17, 203, 0.4);
        }

        /* Welcome Screen Styles */
        .welcome-screen {
            text-align: center;
            max-width: 800px;
            margin: 0 auto;
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
        }

        .welcome-title {
            font-size: 3rem;
            font-weight: 800;
            color: var(--primary-color-start);
            margin-bottom: 15px;
            line-height: 1.2;
            letter-spacing: -0.04em;
            text-shadow: 0 3px 8px rgba(0, 0, 0, 0.05);
        }

        .welcome-subtitle {
            font-size: 1.3rem;
            color: var(--text-medium);
            margin-bottom: 20px;
            line-height: 1.6;
            font-weight: 400;
        }

        .welcome-description {
            font-size: 1rem;
            color: var(--text-light);
            line-height: 1.6;
            margin-bottom: 30px;
            text-align: center;
            max-width: 500px;
        }

        .start-btn {
            background: var(--primary-gradient);
            color: white;
            border: none;
            padding: 16px 45px;
            border-radius: 15px;
            font-size: 1.2rem;
            font-weight: 700;
            cursor: pointer;
            transition: all 0.3s ease;
            box-shadow: 0 8px 25px rgba(106, 17, 203, 0.35);
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
            letter-spacing: 0.02em;
        }

        .start-btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 10px 30px rgba(106, 17, 203, 0.45);
        }

        /* Coupang Ad Style */
        .coupang-ad-container {
            margin-top: 25px;
            margin-bottom: 5px;
            text-align: center;
            width: 100%;
        }
        .coupang-ad-container iframe {
            max-width: 100%;
            height: 44px; /* Default height for search bar */
            border-radius: 8px;
            overflow: hidden;
        }
        /* Style for banner ad iframe */
        .coupang-banner-ad-container {
            margin-top: 20px; /* Spacing from previous content */
            margin-bottom: 10px; /* Spacing to next content */
            text-align: center;
            width: 100%;
        }
        .coupang-banner-ad-container iframe {
            max-width: 100%; /* Ensure responsiveness */
            height: 140px; /* Fixed height for banner */
            border-radius: 8px;
            overflow: hidden;
            border: none; /* Remove default iframe border */
        }


        /* Coupang Disclosure Style */
        .coupang-disclosure {
            font-size: 0.75rem;
            color: var(--text-light);
            margin-top: 10px;
            text-align: center;
            line-height: 1.3;
        }


        /* Responsive Design */
        @media (max-width: 768px) {
            .container {
                padding: 20px;
                margin: 10px;
                min-height: 95vh;
            }
            
            h1 {
                font-size: 2rem;
                margin-bottom: 20px;
            }
            
            .question-card h2 {
                font-size: 1rem;
            }
            
            .question {
                font-size: 1.3rem;
                margin-bottom: 8px;
            }
            
            .question-description {
                font-size: 0.85rem;
                margin-bottom: 20px;
            }
            
            .options {
                gap: 8px;
                margin-bottom: 15px;
            }
            
            .option {
                padding: 12px 15px;
                min-height: 50px;
                border-radius: 10px;
            }
            
            .option .description { /* score가 display: none이므로 직접 접근 */
                font-size: 0.85rem;
            }
            
            .nav-btn {
                padding: 10px 20px;
                font-size: 0.9rem;
            }
            
            .result-card {
                padding: 25px;
            }
            
            .result-card h2 {
                font-size: 1.3rem;
            }
            
            .ntrp-level {
                font-size: 3rem;
            }
            
            .description {
                font-size: 1rem;
            }
            
            .stats p {
                font-size: 0.85rem;
            }
            
            .detailed-analysis {
                padding: 20px;
            }
            
            .detailed-analysis h3 {
                font-size: 1.4rem;
                margin-bottom: 15px;
            }
            
            .detailed-analysis h4 {
                font-size: 1rem;
                margin-bottom: 10px;
            }
            
            /* 모바일에서 score-grid는 다시 1열로 */
            .score-grid {
                grid-template-columns: 1fr;
                gap: 8px;
            }
            
            /* 모바일에서 score-item 내부 정렬 다시 설정 */
            .score-item {
                flex-direction: row;
                align-items: center;
                justify-content: space-between;
                gap: 8px;
                padding: 10px 15px;
            }
            
            .category-name {
                min-width: auto;
                text-align: left;
                margin-bottom: 0;
                font-size: 0.9rem;
            }
            
            .score-bar {
                flex-grow: 1;
                height: 10px;
            }
            
            .score-value {
                min-width: auto;
                text-align: right;
                margin-top: 0;
                font-size: 0.9rem;
            }

            .strengths-weaknesses {
                grid-template-columns: 1fr;
                gap: 15px;
            }
            
            .strengths, .weaknesses, .improvement-suggestions {
                padding: 15px;
            }
            
            .strengths h4, .weaknesses h4, .improvement-suggestions h4 {
                font-size: 1rem;
            }
            
            .strengths ul, .weaknesses ul, .improvement-suggestions ul {
                font-size: 0.85rem;
                line-height: 1.4;
            }
            
            .restart-btn {
                padding: 12px 25px;
                font-size: 1rem;
                margin-top: 20px;
            }

            /* Welcome Screen Responsive */
            .welcome-title {
                font-size: 1.8rem;
                margin-bottom: 10px;
            }
            
            .welcome-subtitle {
                font-size: 1rem;
                margin-bottom: 15px;
            }
            
            .welcome-description {
                font-size: 0.85rem;
                margin-bottom: 25px;
            }
            
            .start-btn {
                padding: 12px 30px;
                font-size: 1rem;
            }
            .coupang-disclosure {
                font-size: 0.7rem;
            }
        }
    &amp;lt;/style&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;container&quot; id=&quot;app&quot;&amp;gt;
        &amp;lt;!-- 앱 내용이 여기에 동적으로 로드됩니다 --&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;script&amp;gt;
        // 앱 상태 관리
        let currentState = 'welcome'; // 'welcome', 'quiz', 'result'
        let currentQuestionIndex = 0;
        let answers = {};

        // NTRP 질문 데이터
        const questions = {
            &quot;forehand&quot;: {
                &quot;question&quot;: &quot;포핸드를 원하는 곳으로 보낼 수 있나요?&quot;,
                &quot;description&quot;: &quot;포핸드의 방향 제어와 깊이 조절 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;공 때리기도 급급! 라켓에 맞으면 다행이죠.  &quot;,
                    2: &quot;간단한 방향(대충 저~쪽)은 어쩌다 가능해요. 가끔 예상치 못한 곳으로!  &quot;, 
                    3: &quot;중간 속도 공은 원하는 곳으로 척척! 나름 컨트롤 가능!  &quot;,
                    4: &quot;깊이와 방향을 자유자재로 조절! 상대 코트 구석구석을 노려요.  &quot;, 
                    5: &quot;어떤 상황에서도 정확한 포핸드로 게임 지배! 역시 필승!  &quot;
                }
            },
            &quot;backhand&quot;: {
                &quot;question&quot;: &quot;백핸드를 원하는 곳으로 보낼 수 있나요?&quot;,
                &quot;description&quot;: &quot;백핸드의 일관성과 방향 제어 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;백핸드만 오면 몸부터 돌려 포핸드로 냅다 쳐요. 나의 숙제!  &quot;,
                    2: &quot;백핸드를 치긴 하는데 네트행이거나 엉뚱한 방향으로 가요. 삐끗삐끗!  &quot;,
                    3: &quot;중간 속도 공은 백핸드로도 꽤 안정적! 이제 좀 폼이 나는 듯?  &quot;,
                    4: &quot;백핸드로도 방향/깊이 자유자재! 상대방이 약점인 줄 알았대요.  &quot;,
                    5: &quot;포핸드만큼 강력하고 정확한 백핸드로 상대를 압도! 백핸드마스터!  &quot;
                }
            },
            &quot;serve&quot;: {
                &quot;question&quot;: &quot;서브의 정확성과 파워는 어느 정도인가요?&quot;,
                &quot;description&quot;: &quot;첫 서브와 세컨드 서브의 성공률과 효과를 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;일단 서브 넣기만 해도 감지덕지! 더블 폴트는 기본이죠.  &quot;,
                    2: &quot;세컨드 서브는 어떻게든 코트 안에 넣어요. 첫 서브는 모험!  &quot;,
                    3: &quot;첫 서브도 가끔 번개처럼 꽂히고, 세컨드 서브도 안정적! ⚡&quot;,
                    4: &quot;강력한 파워와 날카로운 정확성! 서브 에이스 제조기!  &quot;,
                    5: &quot;압박감 속에서도 에이스 꽂는 믿음직한 서브! 서브는 곧 득점!  &quot;
                }
            },
            &quot;volley&quot;: {
                &quot;question&quot;: &quot;발리와 네트 플레이를 할 수 있나요?&quot;,
                &quot;description&quot;: &quot;네트에서의 편안함과 발리 득점 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;네트 근처 가는 건 좀... 공이 무서워요. �&quot;,
                    2: &quot;간단하게 네트 앞에서 공 받아 넘기는 정도는 해요. 툭!  &quot;,
                    3: &quot;포핸드 발리는 꽤 자신 있어요! 백핸드는 아직 연습 중...  &quot;,
                    4: &quot;발리로 포인트를 직접 따낼 수 있어요! 네트는 나의 구역!  &quot;,
                    5: &quot;네트 앞에서 상대를 압박하고 발리로 게임을 끝냅니다! 발리의 마법사!  &quot;
                }
            },
            &quot;footwork&quot;: {
                &quot;question&quot;: &quot;풋워크와 코트 커버리지는 어느 정도인가요?&quot;,
                &quot;description&quot;: &quot;코트 이동과 포지셔닝 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;공이 오면 멍... 공 따라가는 것도 버거워요.  &quot;,
                    2: &quot;기본적인 좌우 이동은 하지만, 빠르진 않아요. 영차영차!  &quot;,
                    3: &quot;코트 넓이를 어느 정도 커버해요. 이제 좀 테니스 선수 같아요!  &quot;,
                    4: &quot;날렵한 풋워크로 코트 구석구석을 누벼요! 휙휙!  &quot;,
                    5: &quot;프로 선수처럼 코트 전체를 지배하는 풋워크와 포지셔닝! 코트의 지배자!  &quot;
                }
            },
            &quot;strategy&quot;: {
                &quot;question&quot;: &quot;게임 전략을 세우고 실행할 수 있나요?&quot;,
                &quot;description&quot;: &quot;상대방 분석과 전략적 사고 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;그냥 공만 치면 되는 거 아닌가요? 전략은 뭔가요?  &quot;,
                    2: &quot;상대방 약점을 대충 노려볼까...? 정도는 생각해요.  &quot;,
                    3: &quot;상대 약점 파악하고 거기에 맞춰 공격 시도! 제법 전략적!  &quot;,
                    4: &quot;게임을 예측하고 상황에 따라 전략을 유연하게 바꿔요! 이기는 게임은 전략 싸움!  &quot;,
                    5: &quot;어떤 상대방도 완벽 분석, 맞춤 전략으로 승리 쟁취! 테니스 브레인!  &quot;
                }
            },
            &quot;consistency&quot;: {
                &quot;question&quot;: &quot;일관성 있게 랠리를 이어갈 수 있나요?&quot;,
                &quot;description&quot;: &quot;안정적인 샷과 에러 관리 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;3번 이상 랠리 이어가면 기적! 내 공은 어디로...  &amp;zwj; &quot;,
                    2: &quot;공은 넘기는데, 속도는 좀 느려요  &quot;, // 문구 수정 및 이모지 변경
                    3: &quot;중간 속도 랠리는 꽤 안정적! 이제 좀 랠리 맛이 나네요!  &quot;,
                    4: &quot;빠른 페이스에서도 안정적! 좀처럼 실수하지 않죠.  ️&quot;,
                    5: &quot;어떤 상황에서도 흔들림 없는 완벽한 일관성! 벽치기 장인!  &quot;
                }
            },
            &quot;mental&quot;: {
                &quot;question&quot;: &quot;게임 중 멘탈 관리가 잘 되나요?&quot;,
                &quot;description&quot;: &quot;압박감과 스트레스 상황에서의 대처 능력을 평가합니다.&quot;,
                &quot;levels&quot;: {
                    1: &quot;실수 한 번 하면 라켓 던지고 싶어져요. 바로 멘탈 붕괴!  &quot;,
                    2: &quot;간단한 게임은 괜찮은데, 지고 있으면 표정 관리가 안 돼요.  &quot;,
                    3: &quot;어느 정도 압박감에 대처 가능! 그래도 속은 부글부글...  &quot;,
                    4: &quot;중요한 순간에도 침착하고 냉정함 유지! 강철 멘탈!  &quot;,
                    5: &quot;어떤 상황에서도 흔들리지 않는 완벽 멘탈로 상대를 압도! 멘탈 갑!  &quot;
                }
            }
        };

        // NTRP 레벨별 위트 있는 설명
        const ntrpDescriptions = {
            1.0: &quot;테니스 신생아: 라켓을 잡고 코트에 첫발을 내디딘 당신! 테니스 세계에 오신 것을 환영합니다!&quot;,
            1.5: &quot;테니스 걸음마: 공을 때리고는 있는데, 어디로 갈지는 공만 아는 단계! 일단 공을 넘기는 게 목표죠.&quot;,
            2.0: &quot;테니스 초급 모험가: 공을 넘기기 시작했지만, 아직 기본기가 시급한 테린이! 랠리는 짧지만, 열정은 만렙!&quot;,
            2.5: &quot;테니스 길찾기 중: 어설프게 공은 치지만, 어디로 갈지, 왜 이렇게 어려운지 고민이 많은 단계. 그래도 랠리의 맛을 조금씩 알아가죠!&quot;,
            3.0: &quot;테니스 기본 다지기: 이제 공은 웬만큼 원하는 곳으로 보내지만, 깊이나 파워는 아직 미완성! 복식에서 '원업 원백'이 익숙한 알뜰 플레이어.&quot;,
            3.5: &quot;테니스 실력 점프 중: 일관성과 컨트롤을 갖추고 슬슬 네트 플레이도 시도하는 단계! '저 사람 꽤 치는데?' 소리 듣기 시작할 때죠.&quot;,
            4.0: &quot;테니스 숙련자 입문: 포핸드, 백핸드 모두 깊이와 컨트롤을 갖추고, 서브도 제법 날카로운 아마추어 고수! 게임의 흐름을 읽기 시작합니다.&quot;,
            4.5: &quot;테니스 전략가 초급: 파워, 스핀, 전략까지 겸비한 테니스 '꾼'! 게임을 이끌어가는 능력이 출중하며, 코트에서 상대방을 괴롭히기 시작합니다.&quot;,
            5.0: &quot;테니스 아마추어 최강자: 강력한 공격과 정교한 기술을 겸비한 아마추어 레벨의 정점! 이제 동네 코트에서는 적수를 찾기 어렵죠.&quot;,
            5.5: &quot;테니스 준프로의 벽: 프로급 기술과 전략을 갖추고 어떤 상황에서도 흔들리지 않는 멘탈의 소유자! 아마추어와 프로의 경계에 서 있습니다.&quot;,
            6.0: &quot;테니스 프로페셔널: 국가대표, 대학 선수, 또는 세계를 무대로 뛰는 진정한 테니스 스타! 이들은 그냥 '테니스 그 자체'입니다.&quot;,
            7.0: &quot;테니스 월드클래스: 세계적 수준의 테니스 스타! 이제 당신은 테니스의 신이 되었습니다.&quot;
        };

        // 앱 상태 관리
        let result = null;

        // 질문 키 배열
        const questionKeys = Object.keys(questions);

        // 앱 렌더링 함수
        function renderApp() {
            const app = document.getElementById('app');
            
            if (currentState === 'welcome') {
                renderWelcome(app);
            } else if (result) {
                renderResult(app);
            } else {
                renderQuestion(app);
            }
        }

        // 환영 화면 렌더링
        function renderWelcome(app) {
            app.innerHTML = `
                &amp;lt;div class=&quot;welcome-screen&quot;&amp;gt;
                    &amp;lt;h1 class=&quot;welcome-title&quot;&amp;gt;&amp;lt;span class=&quot;emoji&quot;&amp;gt; &amp;lt;/span&amp;gt; 당신의 테니스 레벨은?&amp;lt;/h1&amp;gt;
                    &amp;lt;p class=&quot;welcome-subtitle&quot;&amp;gt;간단한 테스트로 지금 바로 확인하세요! &amp;lt;span class=&quot;emoji&quot;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;div class=&quot;welcome-description&quot;&amp;gt;
                        &amp;lt;p&amp;gt;내 테니스 실력이 궁금하신가요?&amp;lt;br&amp;gt;
                        이 테스트는 당신의 NTRP 레벨을 쉽고 재미있게 진단해 드립니다.&amp;lt;br&amp;gt;
                        몇 가지 질문에 답하고,&amp;lt;br&amp;gt;
                        포핸드부터 멘탈까지 당신의 강점과 개선점을 한눈에 확인해보세요! &amp;lt;span class=&quot;emoji&quot;&amp;gt; &amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;button class=&quot;start-btn&quot; onclick=&quot;startTest()&quot;&amp;gt;테스트 시작하기&amp;lt;/button&amp;gt;
                    &amp;lt;!-- Coupang Partners Search Bar Ad --&amp;gt;
                    &amp;lt;div class=&quot;coupang-ad-container&quot;&amp;gt;
                        &amp;lt;iframe src=&quot;https://coupa.ng/ciT9UH&quot; width=&quot;100%&quot; height=&quot;44&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;!-- Coupang Partners Banner Ad --&amp;gt;
                    &amp;lt;div class=&quot;coupang-banner-ad-container&quot;&amp;gt;
                        &amp;lt;iframe src=&quot;https://ads-partners.coupang.com/widgets.html?id=885878&amp;amp;template=carousel&amp;amp;trackingCode=AF6812978&amp;amp;subId=&amp;amp;width=680&amp;amp;height=140&amp;amp;tsource=&quot; width=&quot;100%&quot; height=&quot;140&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;!-- Coupang Partners Disclosure --&amp;gt;
                    &amp;lt;p class=&quot;coupang-disclosure&quot;&amp;gt;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            `;
        }

        // 질문 렌더링
        function renderQuestion(app) {
            const currentQuestionKey = questionKeys[currentQuestionIndex];
            const currentQuestion = questions[currentQuestionKey];
            
            app.innerHTML = `
                &amp;lt;h1&amp;gt;  테니스 NTRP 레벨 테스트&amp;lt;/h1&amp;gt;
                
                &amp;lt;div class=&quot;progress-bar&quot;&amp;gt;
                    &amp;lt;div class=&quot;progress-fill&quot; style=&quot;width: ${((currentQuestionIndex + 1) / questionKeys.length) * 100}%&quot;&amp;gt;&amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                
                &amp;lt;div class=&quot;question-card&quot;&amp;gt;
                    &amp;lt;h2&amp;gt;질문 ${currentQuestionIndex + 1} / ${questionKeys.length}&amp;lt;/h2&amp;gt;
                    &amp;lt;p class=&quot;question&quot;&amp;gt;${currentQuestion.question}&amp;lt;/p&amp;gt;
                    &amp;lt;p class=&quot;question-description&quot;&amp;gt;${currentQuestion.description}&amp;lt;/p&amp;gt;
                    
                    &amp;lt;div class=&quot;options&quot;&amp;gt;
                        ${Object.entries(currentQuestion.levels).map(([score, description]) =&amp;gt; `
                            &amp;lt;button class=&quot;option ${answers[currentQuestionKey] === parseInt(score) ? 'selected' : ''}&quot; 
                                    onclick=&quot;selectAnswer('${currentQuestionKey}', ${score})&quot;&amp;gt;
                                &amp;lt;!-- Score display removed --&amp;gt;
                                &amp;lt;span class=&quot;description&quot;&amp;gt;${description}&amp;lt;/span&amp;gt;
                            &amp;lt;/button&amp;gt;
                        `).join('')}
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;

                &amp;lt;div class=&quot;navigation&quot;&amp;gt;
                    ${currentQuestionIndex &amp;gt; 0 ? `
                        &amp;lt;button class=&quot;nav-btn prev&quot; onclick=&quot;previousQuestion()&quot;&amp;gt;이전&amp;lt;/button&amp;gt;
                    ` : ''}
                    
                    ${currentQuestionIndex &amp;lt; questionKeys.length - 1 ? `
                        &amp;lt;button class=&quot;nav-btn next&quot; onclick=&quot;nextQuestion()&quot; 
                                ${!answers[currentQuestionKey] ? 'disabled' : ''}&amp;gt;다음&amp;lt;/button&amp;gt;
                    ` : `
                        &amp;lt;button class=&quot;nav-btn submit&quot; onclick=&quot;calculateResult()&quot; 
                                ${Object.keys(answers).length &amp;lt; questionKeys.length ? 'disabled' : ''}&amp;gt;결과 보기&amp;lt;/button&amp;gt;
                    `}
                &amp;lt;/div&amp;gt;
            `;
        }

        // 결과 렌더링
        function renderResult(app) {
            app.innerHTML = `
                &amp;lt;h1&amp;gt;  NTRP 테스트 결과&amp;lt;/h1&amp;gt;
                &amp;lt;div class=&quot;result-card&quot;&amp;gt;
                    &amp;lt;h2&amp;gt;당신의 NTRP 레벨은&amp;lt;/h2&amp;gt;
                    &amp;lt;div class=&quot;ntrp-level&quot;&amp;gt;${result.ntrp_level}&amp;lt;/div&amp;gt;
                    &amp;lt;p class=&quot;description&quot;&amp;gt;${result.description}&amp;lt;/p&amp;gt;
                    &amp;lt;div class=&quot;stats&quot;&amp;gt;
                        &amp;lt;p&amp;gt;평균 점수: ${result.average_score}/5&amp;lt;/p&amp;gt;
                        &amp;lt;p&amp;gt;응답한 질문: ${result.total_questions}개&amp;lt;/p&amp;gt;
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                
                &amp;lt;div class=&quot;detailed-analysis&quot;&amp;gt;
                    &amp;lt;h3&amp;gt;  상세 분석&amp;lt;/h3&amp;gt;
                    
                    &amp;lt;div class=&quot;category-scores&quot;&amp;gt;
                        &amp;lt;h4&amp;gt;영역별 점수&amp;lt;/h4&amp;gt;
                        &amp;lt;div class=&quot;score-grid&quot;&amp;gt;
                            ${Object.entries(result.category_scores).map(([category, score]) =&amp;gt; `
                                &amp;lt;div class=&quot;score-item&quot;&amp;gt;
                                    &amp;lt;span class=&quot;category-name&quot;&amp;gt;${category}&amp;lt;/span&amp;gt;
                                    &amp;lt;div class=&quot;score-bar&quot;&amp;gt;
                                        &amp;lt;div class=&quot;score-fill&quot; style=&quot;width: ${(score / 5) * 100}%&quot;&amp;gt;&amp;lt;/div&amp;gt;
                                    &amp;lt;/div&amp;gt;
                                    &amp;lt;span class=&quot;score-value&quot;&amp;gt;${score}/5&amp;lt;/span&amp;gt;
                                &amp;lt;/div&amp;gt;
                            `).join('')}
                        &amp;lt;/div&amp;gt;
                    &amp;lt;/div&amp;gt;
                    
                    &amp;lt;div class=&quot;strengths-weaknesses&quot;&amp;gt;
                        ${result.strengths &amp;amp;&amp;amp; result.strengths.length &amp;gt; 0 ? `
                            &amp;lt;div class=&quot;strengths&quot;&amp;gt;
                                &amp;lt;h4&amp;gt;  강점&amp;lt;/h4&amp;gt;
                                &amp;lt;ul&amp;gt;
                                    ${result.strengths.map(strength =&amp;gt; `&amp;lt;li&amp;gt;${strength}&amp;lt;/li&amp;gt;`).join('')}
                                &amp;lt;/ul&amp;gt;
                            &amp;lt;/div&amp;gt;
                        ` : ''}
                        
                        ${result.weaknesses &amp;amp;&amp;amp; result.weaknesses.length &amp;gt; 0 ? `
                            &amp;lt;div class=&quot;weaknesses&quot;&amp;gt;
                                &amp;lt;h4&amp;gt;  개선점&amp;lt;/h4&amp;gt;
                                &amp;lt;ul&amp;gt;
                                    ${result.weaknesses.map(weakness =&amp;gt; `&amp;lt;li&amp;gt;${weakness}&amp;lt;/li&amp;gt;`).join('')}
                                &amp;lt;/ul&amp;gt;
                            &amp;lt;/div&amp;gt;
                        ` : ''}
                    &amp;lt;/div&amp;gt;
                    
                    ${result.improvement_suggestions ? `
                        &amp;lt;div class=&quot;improvement-suggestions&quot;&amp;gt;
                            &amp;lt;h4&amp;gt;  개선 제안&amp;lt;/h4&amp;gt;
                            &amp;lt;ul&amp;gt;
                                ${result.improvement_suggestions.map(suggestion =&amp;gt; `&amp;lt;li&amp;gt;${suggestion}&amp;lt;/li&amp;gt;`).join('')}
                            &amp;lt;/ul&amp;gt;
                        &amp;lt;/div&amp;gt;
                    ` : ''}
                &amp;lt;/div&amp;gt;
                
                &amp;lt;button class=&quot;restart-btn&quot; onclick=&quot;restartTest()&quot;&amp;gt;다시 테스트하기&amp;lt;/button&amp;gt;
                &amp;lt;!-- Coupang Partners Search Bar Ad --&amp;gt;
                &amp;lt;div class=&quot;coupang-ad-container&quot;&amp;gt;
                    &amp;lt;iframe src=&quot;https://coupa.ng/ciT9UH&quot; width=&quot;100%&quot; height=&quot;44&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;!-- Coupang Partners Banner Ad --&amp;gt;
                &amp;lt;div class=&quot;coupang-banner-ad-container&quot;&amp;gt;
                    &amp;lt;iframe src=&quot;https://ads-partners.coupang.com/widgets.html?id=885878&amp;amp;template=carousel&amp;amp;trackingCode=AF6812978&amp;amp;subId=&amp;amp;width=680&amp;amp;height=140&amp;amp;tsource=&quot; width=&quot;100%&quot; height=&quot;140&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot; referrerpolicy=&quot;unsafe-url&quot; browsingtopics&amp;gt;&amp;lt;/iframe&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;!-- Coupang Partners Disclosure --&amp;gt;
                &amp;lt;p class=&quot;coupang-disclosure&quot;&amp;gt;이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.&amp;lt;/p&amp;gt;
            `;
        }

        // Test start
        function startTest() {
            currentState = 'quiz';
            currentQuestionIndex = 0;
            answers = {};
            result = null;
            renderApp();
        }

        // Select answer
        function selectAnswer(questionKey, score) {
            answers[questionKey] = score;
            renderApp();
        }

        // Next question
        function nextQuestion() {
            if (currentQuestionIndex &amp;lt; questionKeys.length - 1) {
                currentQuestionIndex++;
                renderApp();
            }
        }

        // Previous question
        function previousQuestion() {
            if (currentQuestionIndex &amp;gt; 0) {
                currentQuestionIndex--;
                renderApp();
                // Restore selected state after rendering
                const currentQuestionKey = questionKeys[currentQuestionIndex];
                const selectedScore = answers[currentQuestionKey];
                if (selectedScore) {
                    const options = document.querySelectorAll('.option');
                    options.forEach(option =&amp;gt; {
                        if (questions[currentQuestionKey].levels[selectedScore] === option.querySelector('.description').innerText) {
                            option.classList.add('selected');
                        } else {
                            option.classList.remove('selected');
                        }
                    });
                }
            }
        }

        // Calculate result
        function calculateResult() {
            const totalScore = Object.values(answers).reduce((sum, score) =&amp;gt; sum + score, 0);
            const averageScore = totalScore / Object.keys(answers).length;
            
            // Determine NTRP level
            let ntrpLevel;
            if (averageScore &amp;lt;= 1.25) ntrpLevel = 1.0;
            else if (averageScore &amp;lt;= 1.75) ntrpLevel = 1.5;
            else if (averageScore &amp;lt;= 2.25) ntrpLevel = 2.0;
            else if (averageScore &amp;lt;= 2.75) ntrpLevel = 2.5;
            else if (averageScore &amp;lt;= 3.25) ntrpLevel = 3.0;
            else if (averageScore &amp;lt;= 3.75) ntrpLevel = 3.5;
            else if (averageScore &amp;lt;= 4.25) ntrpLevel = 4.0;
            else if (averageScore &amp;lt;= 4.75) ntrpLevel = 4.5;
            else ntrpLevel = 5.0; // If average score is above 4.75, it's 5.0

            // Detailed analysis
            const categoryNames = {
                &quot;forehand&quot;: &quot;포핸드&quot;,
                &quot;backhand&quot;: &quot;백핸드&quot;,
                &quot;serve&quot;: &quot;서브&quot;,
                &quot;volley&quot;: &quot;발리&quot;,
                &quot;footwork&quot;: &quot;풋워크&quot;,
                &quot;strategy&quot;: &quot;전략&quot;,
                &quot;consistency&quot;: &quot;일관성&quot;,
                &quot;mental&quot;: &quot;멘탈&quot;
            };

            const categoryScores = {};
            Object.keys(categoryNames).forEach(category =&amp;gt; {
                if (answers[category]) {
                    categoryScores[categoryNames[category]] = answers[category];
                }
            });

            // Analyze strengths and weaknesses
            const strengths = [];
            const weaknesses = [];
            Object.entries(categoryScores).forEach(([category, score]) =&amp;gt; {
                if (score &amp;gt;= 4) strengths.push(category);
                else if (score &amp;lt;= 2) weaknesses.push(category);
            });

            // Improvement suggestions
            const improvementSuggestions = [];
            if (weaknesses.includes(&quot;포핸드&quot;)) {
                improvementSuggestions.push(&quot;포핸드 기본기 연습이 필요합니다. 스윙 폼과 타점을 중점적으로 연습해보세요.&quot;);
            }
            if (weaknesses.includes(&quot;백핸드&quot;)) {
                improvementSuggestions.push(&quot;백핸드 그립과 준비 동작을 연습해보세요. 백핸드를 피하지 말고 적극적으로 연습하세요.&quot;);
            }
            if (weaknesses.includes(&quot;서브&quot;)) {
                improvementSuggestions.push(&quot;서브 연습을 통해 정확성과 파워를 향상시켜보세요. 세컨드 서브부터 안정화하세요.&quot;);
            }
            if (weaknesses.includes(&quot;발리&quot;)) {
                improvementSuggestions.push(&quot;네트 플레이에 익숙해지세요. 간단한 발리부터 시작해서 점진적으로 발전시키세요.&quot;);
            }
            if (weaknesses.includes(&quot;풋워크&quot;)) {
                improvementSuggestions.push(&quot;기본적인 이동 연습이 필요합니다. 사이드 스텝과 크로스오버 스텝을 연습하세요.&quot;);
            }
            if (weaknesses.includes(&quot;전략&quot;)) {
                improvementSuggestions.push(&quot;게임 중 상대방을 관찰하고 약점을 찾아보세요. 다양한 샷 조합을 연습하세요.&quot;);
            }
            if (weaknesses.includes(&quot;일관성&quot;)) {
                improvementSuggestions.push(&quot;안정적인 샷을 위해 기본기를 탄탄히 하세요. 에러를 줄이는 연습이 중요합니다.&quot;);
            }
            if (weaknesses.includes(&quot;멘탈&quot;)) {
                improvementSuggestions.push(&quot;압박감에 대처하는 연습이 필요합니다. 실수를 해도 포기하지 않는 마음가짐이 중요합니다.&quot;);
            }
            if (improvementSuggestions.length === 0) {
                improvementSuggestions.push(&quot;전반적으로 좋은 실력을 보유하고 있습니다. 더 높은 레벨을 위해 고급 기술을 연습해보세요.&quot;);
            }

            result = {
                ntrp_level: ntrpLevel,
                description: ntrpDescriptions[ntrpLevel] || &quot;정의되지 않은 레벨&quot;,
                average_score: Math.round(averageScore * 10) / 10,
                total_questions: Object.keys(answers).length,
                category_scores: categoryScores,
                strengths: strengths,
                weaknesses: weaknesses,
                improvement_suggestions: improvementSuggestions
            };

            renderApp();
        }

        // Restart test
        function restartTest() {
            currentState = 'welcome';
            currentQuestionIndex = 0;
            answers = {};
            result = null;
            renderApp();
        }

        // Initialize app
        renderApp();
    &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>dev</category>
      <category>HTML</category>
      <category>Javascript</category>
      <category>ntrp</category>
      <category>웹개발</category>
      <category>웹앱만들기</category>
      <category>코딩초보</category>
      <category>테니스</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/4</guid>
      <comments>https://quinton84.tistory.com/4#entry4comment</comments>
      <pubDate>Sun, 6 Jul 2025 22:24:42 +0900</pubDate>
    </item>
    <item>
      <title>Gemini API를 활용한 슬랙 챗봇 제작 가이드</title>
      <link>https://quinton84.tistory.com/3</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gemini API를 활용한 슬랙 챗봇 제작 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안녕하세요! 오늘은 제가 직접 제작했던 Gemini API 기반 슬랙 챗봇 제작 경험을 공유하려고 합니다. 이전에 작업할 때 참고했던 브런치 글과 제 작업물을 바탕으로 자세히 설명해 드릴게요.&lt;/p&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: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://brunch.co.kr/@cocosociety/14&quot;&gt;브런치: ChatGPT API로 슬랙 챗봇 만들기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 제작 방법 개요&lt;/h3&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;&lt;b&gt;학습 데이터 제작:&lt;/b&gt; 구글 시트를 활용하여 챗봇의 학습 데이터와 요청 사항을 관리합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;슬랙 봇 세팅:&lt;/b&gt; 슬랙 API를 통해 챗봇을 생성하고 필요한 권한을 부여합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Gemini API 연동 및 배포:&lt;/b&gt; Gemini API 키를 발급받아 슬랙 봇과 연동하고, Google Apps Script를 통해 챗봇을 배포합니다.&lt;/li&gt;
&lt;/ol&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;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-7578665530606531&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 학습 데이터 제작&lt;/h3&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;구글 &lt;/b&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&gt;&lt;b&gt;시트 탭 이름:&lt;/b&gt; 학습데이터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A1 셀:&lt;/b&gt; 학습 데이터 입력&lt;/li&gt;
&lt;li&gt;&lt;b&gt;A2 셀:&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;b&gt;요청 사항 (프롬프트 지시어)&lt;/b&gt;&lt;span&gt; 입력&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중요:&lt;/b&gt;&lt;span&gt; 각 셀에는 명확한 내용을 입력해야 합니다.&lt;/span&gt;&lt;span&gt; 특히 A2 셀의 &lt;/span&gt;&lt;b&gt;요청 사항&lt;/b&gt;&lt;span&gt;은 Gemini 모델이 A1 셀의 &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;span&gt; 예를 들어,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;b&gt;&quot;A1 셀에 있는 내용을 기반으로 답변을 생성해 주세요. 외부 정보 검색은 절대 하지 마세요.&quot;&lt;/b&gt;&lt;span&gt; 와 같이 구체적으로 명시해야 Gemini가 학습 데이터의 내용이 있음에도 불구하고 외부 데이터를 기반으로 답변을 만드는 것을 방지할 수 있습니다.&lt;/span&gt;&amp;nbsp;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;[당신의 역할]
당신은 '재미나이회사'의 **팀 소속의 AI 어시스턴트입니다. 당신의 이름은 '** AI'이며, 동료 직원들에게 회사 생활과 관련된 다양한 정보를 친절하고 정확하게 안내하는 것이 당신의 주된 임무입니다. 당신은 매우 유능하며, 동료들을 돕는 것을 중요하게 생각합니다.

[주요 임무]
정보 제공: 동료 '재미나이회사' 직원들이 회사 정책, 복지, 내부 절차, 조직 문화 등 회사 전반에 대해 질문하면, 당신에게 제공된 '참고 자료'에 근거하여 명확하고 이해하기 쉽게 답변해야 합니다.
문맥 이해: 직원들이 대화 중에 '우리 회사', '우리 팀', '우리' 또는 이와 유사한 표현을 사용할 경우, 이는 항상 '재미나이회사'를 지칭하는 것으로 이해하고 대화해야 합니다.

[답변 생성 시 추가 가이드라인]
정보 출처의 절대성 (매우 중요한 규칙)
당신의 모든 답변은 (필수) 반드시 당신에게 제공된 '참고 자료'의 내용에만 근거해야 합니다. 이 규칙은 절대적이며, 당신의 일반 지식이나 외부 정보는 절대로 사용되어서는 안 됩니다. ('참고 자료'는 이 시스템 지침 이후에 제공되는 명확한 구분자 뒤에 위치합니다.)

소통 스타일 (지침)
동료 직원을 대하는 것처럼, 전반적으로 친절하고 부드러운 어투를 사용해주세요. 답변이 기계적이거나 지나치게 정형화되지 않도록, 실제 사람이 대화하는 것처럼 더욱 자연스러운 흐름을 유지해주세요. 사용자의 상황에 공감하는 따뜻한 느낌을 전달하되, 답변의 명확성과 간결함이 우선시되어야 합니다. 지나치게 사무적이거나 딱딱한 말투는 피해주시고, 긍정적이고 협조적인 태도를 보여주세요. 핵심은 전문성을 유지하면서도 사용자가 편안하게 정보를 얻고 소통할 수 있도록 돕는 것입니다.

명료성 (지침)
답변은 명확하고 간결해야 합니다. 직원들이 쉽게 이해할 수 있도록 필요한 경우 부연 설명을 할 수 있지만, 이 부연 설명 역시 '참고 자료'에 근거해야 하며, 당신의 추측이나 외부 지식을 추가해서는 안 됩니다.

언어 (지침)
모든 답변은 자연스러운 한국어로 제공해야 합니다.

가독성 높은 답변 형식 (매우 중요한 지침)
답변을 명확하고 읽기 쉽게 구성하는 것을 최우선으로 합니다.
1. 슬랙 최적화된 답변 구조 (매우 중요)
첫 답변은 핵심 정보만 2-3줄로 간단히 제공
긴 설명이나 상세 정보는 &quot;더 자세한 내용이 필요하시면 말씀해주세요!&quot; 형태로 추가 질문 유도
한 번에 모든 정보를 제공하지 말고, 사용자가 필요한 만큼만 단계적으로 제공
2. 문장 나누기 규칙 (슬랙 가독성 - 필수 준수)
모든 문장 끝에 줄바꿈: &quot;~습니다.&quot;, &quot;~됩니다.&quot;, &quot;~세요.&quot;, &quot;~요.&quot; 등으로 끝나는 모든 문장 뒤에는 반드시 한 번의 줄바꿈을 해야 합니다
한 줄에 하나의 완전한 문장만 작성
빈 줄은 만들지 않고 연속적인 문장으로 구성
긴 설명은 여러 문장으로 나누어 각각 줄바꿈 처리
3. 항목화된 정보 제공 (세부 지침)
여러 정보를 나열하여 전달할 때는 아래의 지침에 따라 내용을 명확히 구분해주세요. 이렇게 하면 사용자가 정보를 쉽게 구조적으로 파악할 수 있습니다.
내용이 순서나 절차를 명확히 나타내는 경우 (예: 단계별 안내, 방법 설명 등)에는 (필수) 반드시 번호 매기기(예: 1., 2., 3.)를 사용하여 그 순서를 명확히 해주세요.
순서가 중요하지 않은 여러 항목들(예: 제도 특징, 조건 목록, 주의사항 목록, 참고사항 등)을 나열할 경우에는 글머리 기호(- 또는 *)를 사용하여 각 항목을 효과적으로 구분해주세요.
4. 텍스트 강조 사용 금지 (가장 엄격하게 지켜야 할 규칙)
답변을 생성할 때, 본문, 제목, 항목명, 목록의 첫머리 등 답변의 그 어떤 부분에서도 텍스트를 굵게 만드는 마크다운 형식(예시: 단어 또는 단어 등)을 절대로 사용해서는 안 됩니다. 예를 들어, &quot;이것이 바로 핵심입니다!&quot; 와 같이 특정 문장을 굵게 표시하거나, &quot;1. 카드 정지 요청:&quot; 과 같이 번호 매기기 목록의 항목명이나 소제목을 굵게 표시하는 것도 절대 허용되지 않습니다. 내용의 중요성을 나타내고 싶다면, &quot;가장 중요한 점은 다음과 같습니다:&quot;, &quot;특히 유의해야 할 사항은 다음과 같습니다.&quot; 와 같이 문장 자체의 표현을 사용하거나, 해당 내용을 별도의 문장이나 항목으로 명확히 분리하여 설명해야 합니다. 모든 의미상의 강조나 내용 구분은 오직 글머리 기호(- 또는 *), 번호 매기기(1., 2., 3.), 명확한 문장 구조, 그리고 적절한 단락 구분을 통해서만 표현해야 합니다. 이 규칙은 답변 스타일과 관련하여 다른 어떤 지침보다 우선하며, (필수) 반드시, 예외 없이, 가장 엄격하게 지켜주십시오.
5. 문장 및 단락 구분 (세부 지침)
설명이 길어질 경우, 간결한 문장으로 나누어 작성하고 내용의 흐름이나 주제가 바뀔 때는 단락을 명확히 구분하여 가독성을 높여주세요.
6. 링크 형식 (세부 지침)
답변에 참고할 수 있는 링크를 제공해야 할 경우, 다음 형식을 (필수) 반드시 따라주세요. 이 방식은 사용자가 링크 정보와 URL을 명확하게 인지하도록 돕습니다.
본문에서는 링크의 존재와 내용을 간략히 언급합니다. (예: &quot;자세한 내용은 관련 가이드에서 확인하실 수 있습니다.&quot;)
그 다음 줄에 글머리 기호(-)를 사용하여 구체적인 링크 정보와 URL을 명시합니다. 형식: &quot;- 링크 설명 텍스트: URL주소&quot;
마크다운 하이퍼링크 형식(예: 링크 설명 텍스트)은 사용하지 않으며, 반드시 위에 명시된 &quot;링크 설명 텍스트: URL주소&quot; (URL주소는 텍스트 그대로 표시) 형식을 사용해야 합니다.
7. 시각적 구분자 활용 (슬랙 최적화)
다음 이모지들을 상황에 맞게 매우 제한적으로 활용하여 정보의 성격을 시각적으로 구분해주세요. 감정 표현 이모지는 절대 사용하지 마세요.
행동 관련 (정보 구분용):
✅ (해야할 것/올바른 방법/허용되는 것)
❌ (하지말아야 할 것/금지사항/잘못된 방법)
  (절차/과정/단계)
⏰ (시간/기한/마감일)
  (일정/예약/날짜)
정보 관련 (정보 구분용):
  (정보/목록/내용)
  (팁/유용한 정보/추천사항)
⚠️ (주의사항/경고/중요한 안내)
  (연락처/문의/담당자)
  (링크/참고자료/문서)
상태/결과 관련 (정보 구분용):
✨ (혜택/장점/특징)
  (신청/작성/제출)
  (비용/금액/급여)
  (부서/팀/조직)
  (대상/인원/참여자)
절대 사용 금지 이모지:
        등 모든 웃는 얼굴 이모지
      등 모든 감정 표현 이모지
      ❤️ 등 모든 감정/인사 관련 이모지
답변 마무리에도 감정 이모지 절대 사용 금지

소통 스타일 (상세 지침)
동료 직원을 대하는 것처럼, 전반적으로 친절하고 부드러운 어투를 사용해주세요. 답변이 기계적이거나 지나치게 정형화되지 않도록, 실제 사람이 대화하는 것처럼 더욱 자연스러운 흐름을 유지해주세요. 사용자의 상황에 공감하는 따뜻한 느낌을 전달하되, 답변의 명확성과 간결함이 우선시되어야 합니다. 지나치게 사무적이거나 딱딱한 말투는 피해주시고, 긍정적이고 협조적인 태도를 보여주세요. 핵심은 전문성을 유지하면서도 사용자가 편안하게 정보를 얻고 소통할 수 있도록 돕는 것입니다.

인사 규칙 (매우 중요):
첫 번째 질문에만 &quot;안녕하세요!&quot; 인사 사용
&quot;안녕하세요!&quot; 뒤에는 한 번의 줄바꿈
같은 대화 세션 내에서 추가 질문 시에는 인사 없이 바로 답변 시작

문장 마침 규칙 (매우 중요):
모든 문장 끝(&quot;~습니다.&quot;, &quot;~됩니다.&quot;, &quot;~세요.&quot;, &quot;~요.&quot; 등) 뒤에는 반드시 한 번의 줄바꿈
예외 없이 모든 종료 어미 뒤에 줄바꿈 적용
한 줄에 하나의 완전한 문장만 작성
빈 줄은 만들지 않고 연속적인 줄바꿈으로 구성

답변을 생성할 때, 정보의 출처(예: &quot;참고 자료에 따르면&quot;, &quot;문서에 의하면&quot;)나 학습 자료의 내부 구조(예: &quot;X.Y.Z 항목을 보면&quot;)를 직접적으로 언급하지 말아주세요. 대신, 질문받은 내용을 이미 '피플 AI' 당신이 알고 있는 지식처럼 자연스럽게 설명하는 방식으로 전달해야 합니다. 마치 동료가 이미 정보를 알고 있어서 친절하게 알려주는 듯한 느낌을 주는 것이 중요합니다.

(피해야 할 예시) &quot;참고 자료에는 병가에 대한 구체적인 일수가 명시되어 있지 않습니다. 다만, 4.2.2. 경조사 지원 안내 항목에는...&quot;
(권장하는 예시) &quot;병가에 대해 궁금하시군요! 제가 확인해 본 바로는, 우리 회사 병가에 대한 구체적인 일수가 제가 잘 알지 못합니다&amp;hellip;&quot;

만약 '참고 자료'에서 요청된 정보를 찾을 수 없거나 내용이 부족할 경우에는, &quot;해당 정보는 참고 자료에 없습니다.&quot;와 같이 직접적이고 기계적인 표현 대신, &quot;음, 문의주신 부분에 대해서는 제가 지금 바로 명확한 답변을 드리기는 조금 어렵네요.&quot; 또는 &quot;제가 알고 있는 선에서는 해당 내용에 대한 구체적인 정보가 확인되지 않아서요.&quot;와 같이 좀 더 부드럽고 공감하는 어투로 답변해주세요. 이러한 경우, 사용자에게 도움이 될 수 있는 다음 단계나 대안(예: &quot;더 정확한 안내를 위해 피플팀 다른 담당자분께 한번 문의해보시는 건 어떨까요?&quot;)을 친절하게 제시하는 것이 좋습니다.

첫 답변은 핵심만 간단히 제공하고, &quot;더 궁금한 점이 있으시면 말씀해주세요.&quot; 같은 추가 질문 유도 문구 포함 (단, 감정 이모지는 절대 사용하지 않음)

답변의 전체적인 흐름이 딱딱한 정보 전달이 아니라, 실제 사람과 편안하게 대화하는 것처럼 느껴지도록 구성해주세요. 사용자의 질문 의도를 파악하고, 그에 맞춰 감정을 담아 따뜻하게 소통하려는 진심이 전달되도록 노력해주세요.
--- 참고 자료 ---&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&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; 학습 데이터가 많다면 구글 시트보다는 **구글 문서(Google Docs)**를 활용하는 것이 더 효율적일 수 있습니다. 구글시트는 셀의 글자수 제한이 있답니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 슬랙 봇 세팅&lt;/h3&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;&lt;b&gt;슬랙 API 접속:&lt;/b&gt; &lt;a href=&quot;https://api.slack.com/&quot;&gt;https://api.slack.com/&lt;/a&gt; 에 접속합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앱 생성:&lt;/b&gt; your App &amp;rarr; create an app &amp;rarr; from scratch &amp;rarr; app name (원하는 이름으로 변경 가능) &amp;rarr; pick a workspace to develop your app in &amp;rarr; create app&lt;/li&gt;
&lt;li&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&gt;Features &amp;rarr; Oauth &amp;amp; Permissions &amp;rarr; Scopes &amp;rarr; bot token scopes &amp;rarr; add an oauth scope&lt;/li&gt;
&lt;li&gt;chat:write 권한을 부여합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;워크스페이스에 설치:&lt;/b&gt; Features &amp;rarr; Oauth &amp;amp; Permissions &amp;rarr; oauth token &amp;rarr; install to workspace 선택 &amp;rarr; 허용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;봇 유저 OAuth 토큰 복사:&lt;/b&gt; 발급된 bot user oauth token 값을 복사해 둡니다. 이 값은 나중에 코드에 사용됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Gemini API 키 발급&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini API를 사용하기 위한 키를 발급받는 과정입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Google AI Studio 접속:&lt;/b&gt; &lt;a href=&quot;https://ai.google.dev/&quot;&gt;https://ai.google.dev/&lt;/a&gt; 에 접속합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 키 발급:&lt;/b&gt; Google AI Studio에서 get api key를 클릭하여 API 키를 발급받습니다. 이 키 역시 코드에 사용됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Google Apps Script 코드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 슬랙 봇과 Gemini API를 연동하는 Google Apps Script 코드입니다. 이 코드는 구글 시트와 연동되어 작동합니다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;JavaScript&lt;/span&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// GeminiChatbot (searchText)

// 기본 세팅
const apiKey = 'YOUR_GEMINI_API_KEY'; // AI Studio에서 발급한 키를 여기에 붙여넣으세요.
const geminiVer = &quot;gemini-2.0-flash&quot;;
const url = `https://generativelanguage.googleapis.com/v1beta/models/${geminiVer}:generateContent?key=${apiKey}`;
var finalTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(&quot;학습데이터&quot;);
var finalTabData = finalTab.getDataRange().getValues();
var userQuestion = searchText;
var prePrompt = finalTabData[0][0]; // A1 셀의 학습 데이터
var sheetData = finalTabData[0][0]; // A1 셀의 학습 데이터 (prePrompt와 동일하게 설정됨)


// 프롬프트 구성
const prompt = `
${prePrompt}
${sheetData}
--- 질문 ---
${userQuestion}
`;

// Gemini API 요청
const payload = {
  contents: [{ parts: [{ text: prompt }] }]
};

const options = {
  method: 'post',
  contentType: 'application/json',
  payload: JSON.stringify(payload)
};

try {
  const response = UrlFetchApp.fetch(url, options);
  const raw = response.getContentText();
  Logger.log(&quot;✅ 전체 응답:\n&quot; + raw); // 전체 응답 확인용 로그

  const result = JSON.parse(raw);
  const reply = result?.candidates?.[0]?.content?.parts?.[0]?.text;

  if (reply) {
    Logger.log(&quot;✨ Gemini 응답:\n&quot; + reply);
    return reply;
  } else {
    Logger.log(&quot;❗️응답은 성공했지만 candidates가 비어 있음&quot;);
    return &quot;답변을 찾을 수 없었어요.&quot;;
  }

} catch (error) {
  Logger.log(`❌ 에러 발생: ${error}`);
  Logger.log(error.response ? error.response.getContentText() : &quot;응답 없음&quot;);
  finalTab.getRange(1,2).setValue(error);
  return &quot;오류가 발생했어요.&quot;;
}

function geminiSendResult(uID, searchText, answer) {

  var payload = {
    channel: uID,
    blocks: [
      {
        &quot;type&quot;: &quot;section&quot;,
        &quot;text&quot;: {
          &quot;type&quot;: &quot;mrkdwn&quot;,
          &quot;text&quot;: &quot;Q. &quot; + searchText + &quot; `&quot;
        }
      },
      {
        &quot;type&quot;: &quot;section&quot;,
        &quot;text&quot;: {
          &quot;type&quot;: &quot;mrkdwn&quot;,
          &quot;text&quot;: &quot; &quot; + answer + &quot; `&quot;
        }
      }
    ]
  };

  var options = {
    &quot;method&quot;: &quot;post&quot;,
    &quot;headers&quot;: {
      &quot;Authorization&quot;: &quot;Bearer &quot; + &quot;YOUR_SLACK_BOT_TOKEN&quot;, // 슬랙 봇 유저 OAuth 토큰을 여기에 붙여넣으세요.
      &quot;Content-Type&quot;: &quot;application/json&quot;
    },
    &quot;payload&quot;: JSON.stringify(payload)
  };

  try{
    let response = UrlFetchApp.fetch(&quot;https://slack.com/api/chat.postMessage&quot;, options);
    var data = JSON.parse(response.getContentText());
    console.log(data);
  }catch(err){
    console.error(&quot;Slack 메시지 전송 에러:&quot;, err);
  }finally{

  }

}

function doPost(e){
  const params = JSON.parse(e.postData.getDataAsString());
  if('challenge' in params){
    return ContentService.createTextOutput(params.challenge);
  }


  if(params.event.channel_type == 'im' &amp;amp;&amp;amp; params.api_app_id == 'YOUR_SLACK_APP_ID'){ // 'YOUR_SLACK_APP_ID' 부분은 실제 앱 ID로 변경 필요
    var uID = params.event.user;
    var searchText = params.event.text;
    var answer = geminiChatbot(searchText);
    geminiSendResult(uID, searchText, answer);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 챗봇 배포 (Apps Script)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성한 Apps Script 코드를 웹 앱으로 배포하는 과정입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;새 배포:&lt;/b&gt; Google Apps Script 편집기에서 배포 &amp;rarr; 새 배포를 클릭합니다.&lt;/li&gt;
&lt;li&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&gt;유형 선택: 웹 앱&lt;/li&gt;
&lt;li&gt;실행 계정: 나&lt;/li&gt;
&lt;li&gt;액세스 권한이 있는 사용자: 모든 사용자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;배포:&lt;/b&gt; 배포 버튼을 클릭합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;웹 앱 URL 복사:&lt;/b&gt; 배포가 완료되면 생성된 웹 앱 URL을 복사합니다. 이 URL은 슬랙 이벤트 구독에 사용됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;예시 URL:&lt;/b&gt; &lt;a href=&quot;https://script.google.com/macros/s/YOUR_WEB_APP_URL/exec&quot;&gt;https://script.google.com/macros/s/YOUR_WEB_APP_URL/exec&lt;/a&gt; (실제 URL은 배포 시 생성됩니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 슬랙 API 이벤트 구독 설정&lt;/h3&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;&lt;b&gt;슬랙 API 접속:&lt;/b&gt; &lt;a href=&quot;https://api.slack.com/&quot;&gt;https://api.slack.com/&lt;/a&gt; 에 접속하여 해당 앱을 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 구독 활성화:&lt;/b&gt; Features &amp;rarr; Event Subscriptions &amp;rarr; Enable Events를 On으로 설정합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청 URL 입력:&lt;/b&gt; Request URL 필드에 위에서 복사한 &lt;b&gt;웹 앱 URL&lt;/b&gt;을 붙여넣습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;봇 이벤트 구독:&lt;/b&gt; Subscribe to bot events에서 다음 이벤트를 추가합니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;message.im (다이렉트 메시지)&lt;/li&gt;
&lt;li&gt;message.channels (채널 메시지)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 사항 저장:&lt;/b&gt; Save Changes 버튼을 클릭합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;앱 홈 설정:&lt;/b&gt; App Home &amp;rarr; Show Tabs &amp;rarr; Message Tab을 On으로 설정합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 슬랙에서 챗봇에게 메시지를 보내면 Gemini API를 통해 응답을 받을 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 가이드가 Gemini API와 슬랙 챗봇을 연동하는 데 도움이 되기를 바랍니다. 궁금한 점이 있다면 언제든지 질문해주세요!&lt;/p&gt;</description>
      <category>dev</category>
      <category>gemini api</category>
      <category>GoogleAppsScript</category>
      <category>노코드</category>
      <category>슬랙봇</category>
      <category>업무자동화</category>
      <category>챗봇제작</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/3</guid>
      <comments>https://quinton84.tistory.com/3#entry3comment</comments>
      <pubDate>Sun, 6 Jul 2025 18:36:56 +0900</pubDate>
    </item>
    <item>
      <title>파이썬 Flask와 Google Gemini API 연동 개발 가이드: 초보자도 쉽게 따라하기</title>
      <link>https://quinton84.tistory.com/2</link>
      <description>&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;오늘은 파이썬(Python)의 경량 웹 프레임워크인 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Flask&lt;/span&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;여기에 Google의 강력한 AI 모델인 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Gemini API&lt;/span&gt;&lt;/b&gt;&lt;span&gt;를 연동하여 AI 기반의 질문-답변 기능을 구현하는 방법을 단계별로 알려드릴게요. 코딩 초보자분들도 쉽게 따라오실 수 있도록 상세하게 설명해 드리니, 차근차근 함께 만들어 봐요!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-7578665530606531&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;1. 개요 (Overview)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 가이드를 통해 여러분은 다음을 배우고 직접 구현하게 됩니다.&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;b&gt;&lt;span&gt;목표&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 파이썬 Flask 웹 애플리케이션을 구축하고, Google Gemini AI 모델을 연동하여 사용자의 질문에 AI가 답변하는 기능을 구현합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;결과물&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 웹 브라우저에서 질문을 입력하면 Gemini 모델이 실시간으로 답변을 제공하는 간단한 웹 애플리케이션.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;2. 준비물 (Prerequisites)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;시작하기 전에 다음 도구들이 컴퓨터에 설치되어 있어야 합니다.&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;b&gt;&lt;span&gt;Visual Studio Code (VS Code)&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 코드 편집 및 터미널 사용을 위한 통합 개발 환경 (IDE).&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;&lt;span&gt;VS Code 다운로드&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;VS Code 내 &lt;/span&gt;&lt;b&gt;&lt;span&gt;Python 확장&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 설치 (Microsoft 제공).&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Python 3.x&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 파이썬 언어 및 패키지 관리를 위한 런타임.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.python.org/downloads/&quot;&gt;&lt;span&gt;Python 다운로드&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Google Gemini API 키&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: Google AI Studio 또는 Google Cloud Platform에서 발급받은 API 키.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://aistudio.google.com/&quot;&gt;&lt;span&gt;Google AI Studio&lt;/span&gt;&lt;/a&gt;&lt;span&gt;에서 API 키 발급 (무료 사용량 제공).&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;3. 프로젝트 설정 (Project Setup)&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3.1. 프로젝트 폴더 생성 및 VS Code 열기&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;컴퓨터의 원하는 위치에 새 폴더를 생성합니다 (예: &lt;/span&gt;&lt;span&gt;gemini-flask-app&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;VS Code를 열고 &lt;/span&gt;&lt;span&gt;파일(File)&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;폴더 열기(Open Folder...)&lt;/span&gt;&lt;span&gt;를 클릭하여 방금 만든 폴더를 선택합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3.2. 가상 환경 설정 및 활성화 (권장)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프로젝트별로 독립적인 파이썬 환경을 구축하여 라이브러리 충돌을 방지하고 프로젝트를 깔끔하게 관리할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;VS Code 터미널을 엽니다: &lt;/span&gt;&lt;span&gt;터미널(Terminal)&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;새 터미널(New Terminal)&lt;/span&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;span&gt;Ctrl&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt; (백틱).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;터미널에 다음 명령어를 입력하여 가상 환경을 생성합니다:&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;python -m venv venv &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;가상 환경을 활성화합니다:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Windows (PowerShell)&lt;/span&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;.\venv\Scripts\activate &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;macOS/Linux (Bash/Zsh)&lt;/span&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;source venv/bin/activate &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;터미널 프롬프트 앞에 &lt;/span&gt;&lt;span&gt;(venv)&lt;/span&gt;&lt;span&gt;가 표시되면 가상 환경이 성공적으로 활성화된 것입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3.3. 필수 라이브러리 설치&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;활성화된 가상 환경에서 다음 라이브러리들을 설치합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;cmake&quot;&gt;&lt;code&gt;pip install Flask google-generativeai python-dotenv

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Flask&lt;/span&gt;&lt;span&gt;: 웹 애플리케이션을 만들기 위한 핵심 프레임워크입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;google-generativeai&lt;/span&gt;&lt;span&gt;: Google Gemini API를 파이썬에서 쉽게 사용할 수 있도록 돕는 공식 라이브러리입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;python-dotenv&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;span&gt; 파일에 저장된 환경 변수(API 키 등)를 파이썬 코드에서 안전하게 불러오기 위한 라이브러리입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;3.4. &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;span&gt; 파일 생성 및 API 키 저장 (보안 필수!)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span&gt;API 키는 절대 코드에 직접 노출하거나 GitHub와 같은 공개 저장소에 올리면 안 됩니다.&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 유출될 경우 악의적으로 사용될 수 있습니다. &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;span&gt; 파일을 사용하여 환경 변수로 관리하는 것이 가장 안전한 방법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;VS Code 탐색기에서 프로젝트 폴더를 마우스 오른쪽 버튼으로 클릭하고 &lt;/span&gt;&lt;span&gt;새 파일(New File)&lt;/span&gt;&lt;span&gt;을 선택합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;파일 이름을 &lt;/span&gt;&lt;span&gt;.env&lt;/span&gt;&lt;span&gt;로 입력하고 Enter 키를 누릅니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;.env&lt;/span&gt;&lt;span&gt; 파일에 발급받은 Gemini API 키를 다음과 같이 저장합니다 (따옴표를 포함해야 합니다).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;GOOGLE_API_KEY=&quot; &lt;b&gt;&lt;span&gt;실제 발급받은 당신의 Gemini API 키&lt;/span&gt;&lt;/b&gt; &quot; &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;4. 코드 구현 (Code Implementation)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이제 웹 애플리케이션의 핵심 로직을 구현할 차례입니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4.1. &lt;/span&gt;&lt;span&gt;app.py&lt;/span&gt;&lt;span&gt; 파일 작성 (Flask 애플리케이션 로직)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;프로젝트 폴더에 &lt;/span&gt;&lt;span&gt;app.py&lt;/span&gt;&lt;span&gt; 파일을 생성하고 아래 코드를 복사하여 붙여넣습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;from flask import Flask, render_template, request # Flask 웹 애플리케이션 관련 모듈 임포트
import google.generativeai as genai # Google Gemini API 라이브러리 임포트
import os # 환경 변수를 사용하기 위해 os 모듈 임포트
from dotenv import load_dotenv # .env 파일에서 환경 변수를 로드하기 위한 라이브러리 임포트

# .env 파일에서 환경 변수를 로드합니다.
load_dotenv() 

# 환경 변수에서 Gemini API 키를 불러옵니다.
# GOOGLE_API_KEY는 .env 파일에 설정한 변수 이름과 동일해야 합니다.
GOOGLE_API_KEY = os.getenv(&quot;GOOGLE_API_KEY&quot;) 

# API 키가 제대로 로드되었는지 확인하는 로직 (필수)
if not GOOGLE_API_KEY:
    raise ValueError(&quot;Google API Key가 설정되지 않았습니다. .env 파일을 확인하거나 환경 변수를 설정해주세요.&quot;)

# Gemini API를 구성합니다.
genai.configure(api_key=GOOGLE_API_KEY)

# Flask 애플리케이션 인스턴스를 생성합니다.
app = Flask(__name__)

@app.route('/')
def index():
    &quot;&quot;&quot;
    웹페이지의 루트 경로('/')에 대한 GET 요청을 처리합니다.
    초기 웹페이지 로드 시 질문과 결과는 비어 있는 상태로 템플릿을 렌더링합니다.
    &quot;&quot;&quot;
    return render_template('index.html', question=None, result=None)

@app.route('/', methods=['POST'])
def submit():
    &quot;&quot;&quot;
    웹페이지의 루트 경로('/')에 대한 POST 요청을 처리합니다.
    사용자가 폼을 통해 질문을 제출했을 때 실행됩니다.
    &quot;&quot;&quot;
    # 웹 폼에서 'prompt'라는 이름으로 전송된 사용자 질문을 가져옵니다.
    user_prompt = request.form['prompt']
    
    # Gemini 모델을 사용하여 사용자 질문에 대한 응답을 생성합니다.
    gemini_response = access_gemini(user_prompt)
    
    # 사용자 질문과 Gemini 응답을 HTML 템플릿에 전달하여 렌더링합니다.
    # 이를 통해 웹페이지에 질문과 답변이 표시됩니다.
    return render_template('index.html', question=user_prompt, result=gemini_response)

def access_gemini(prompt_value):
    &quot;&quot;&quot;
    Google Gemini 모델에 프롬프트를 전송하고 응답 텍스트를 반환하는 함수입니다.
    API 호출 중 오류가 발생하면 오류 메시지를 반환합니다.
    &quot;&quot;&quot;
    try:
        # 사용할 Gemini 모델을 지정합니다.
        # 'models/gemini-2.0-flash'는 현재 시점에서 텍스트 생성에 권장되는 모델입니다.
        # Google Generative AI 라이브러리에서는 'models/' 접두사를 붙여 명시하는 것이 안정적입니다.
        model = genai.GenerativeModel('models/gemini-2.0-flash') 
        
        # 모델에 프롬프트를 전송하고 응답을 생성합니다.
        # generate_content 메서드는 텍스트 외에 이미지 등 다양한 콘텐츠 유형을 처리할 수 있습니다.
        response = model.generate_content(prompt_value)
        
        # 생성된 응답 텍스트를 반환합니다.
        return response.text
    except Exception as e:
        # API 호출 중 발생한 예외(네트워크 오류, API 키 오류, 할당량 초과 등)를 잡아서
        # 사용자에게 친화적인 오류 메시지를 반환합니다.
        return f&quot;Gemini API 호출 중 오류가 발생했습니다: {e}&quot;

if __name__ == '__main__':
    # Flask 애플리케이션을 실행합니다.
    # debug=True는 개발 모드로, 코드 변경 시 자동으로 서버를 재시작해주는 편리한 기능입니다.
    # 실제 운영 환경에 배포할 때는 보안과 성능을 위해 debug=False로 설정해야 합니다.
    app.run(debug=True)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;4.2. &lt;/span&gt;&lt;span&gt;templates/index.html&lt;/span&gt;&lt;span&gt; 파일 작성 (프런트엔드 UI)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;app.py&lt;/span&gt;&lt;span&gt; 파일과 같은 레벨에 &lt;/span&gt;&lt;span&gt;templates&lt;/span&gt;&lt;span&gt; 폴더를 생성하고, 그 안에 &lt;/span&gt;&lt;span&gt;index.html&lt;/span&gt;&lt;span&gt; 파일을 다음과 같이 작성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1751736409586&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 http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Gemini Flask Example&amp;lt;/title&amp;gt;
    &amp;lt;!-- Bootstrap CSS를 CDN으로 포함하여 기본적인 스타일을 제공합니다. --&amp;gt;
    &amp;lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot; crossorigin=&quot;anonymous&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body class=&quot;container&quot;&amp;gt;
    &amp;lt;h1 class=&quot;display-6 py-2&quot;&amp;gt;Gemini Flask Example&amp;lt;/h1&amp;gt;
    
    &amp;lt;!-- 사용자 입력을 받는 폼 --&amp;gt;
    &amp;lt;form method=&quot;POST&quot; action=&quot;/&quot;&amp;gt;
        &amp;lt;div&amp;gt;
            &amp;lt;label for=&quot;prompt&quot;&amp;gt;프롬프트:&amp;lt;/label&amp;gt;
            &amp;lt;textarea id=&quot;prompt&quot; name=&quot;prompt&quot; class=&quot;form-control&quot; rows=&quot;5&quot; placeholder=&quot;여기에 질문을 입력하세요.&quot;&amp;gt;&amp;lt;/textarea&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;center class=&quot;py-3&quot;&amp;gt;
            &amp;lt;input type=&quot;submit&quot; value=&quot;질문하기&quot; class=&quot;btn btn-primary&quot;&amp;gt;
        &amp;lt;/center&amp;gt;
    &amp;lt;/form&amp;gt;

    &amp;lt;!-- 질문과 답변 결과를 표시하는 영역 --&amp;gt;
    {# Jinja2 템플릿 문법: question 변수가 None이 아닐 때만 표시 #}
    {% if question is not none %} 
        &amp;lt;div class=&quot;mt-4&quot;&amp;gt;
            &amp;lt;h5 class=&quot;text-primary&quot;&amp;gt;당신의 질문:&amp;lt;/h5&amp;gt;
            &amp;lt;p class=&quot;border border-2 p-3 h6 bg-light&quot;&amp;gt;{{ question }}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    {% endif %}

    {# Jinja2 템플릿 문법: result 변수가 None이 아닐 때만 표시 #}
    {% if result is not none %} 
        &amp;lt;div class=&quot;mt-2&quot;&amp;gt;
            &amp;lt;h5 class=&quot;text-success&quot;&amp;gt;Gemini의 답변:&amp;lt;/h5&amp;gt;
            &amp;lt;p class=&quot;border border-2 p-3 h6 bg-light&quot;&amp;gt;{{ result }}&amp;lt;/p&amp;gt;
        &amp;lt;/div&amp;gt;
    {% endif %}

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;mt-2&quot;&gt;
&lt;p class=&quot;border border-2 p-3 h6 bg-light&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&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;span&gt;5. 애플리케이션 실행 및 테스트 (Execution &amp;amp; Testing)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모든 파일 준비가 완료되었다면, 다음 단계에 따라 Flask 애플리케이션을 실행하고 웹 브라우저에서 테스트합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;VS Code 터미널 열기&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;터미널(Terminal)&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;새 터미널(New Terminal)&lt;/span&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;span&gt;Ctrl&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;가상 환경 활성화 확인&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 터미널 프롬프트 앞에 &lt;/span&gt;&lt;span&gt;(venv)&lt;/span&gt;&lt;span&gt;가 표시되어 있는지 확인합니다. (만약 표시되지 않으면 &lt;/span&gt;&lt;span&gt;.\venv\Scripts\activate&lt;/span&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;span&gt;source venv/bin/activate&lt;/span&gt;&lt;span&gt; 명령어로 활성화합니다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;FLASK_APP&lt;/span&gt;&lt;span&gt; 환경 변수 설정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&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;/span&gt;&lt;b&gt;&lt;span&gt;Enter 키를 눌러 명령을 확정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;$env:FLASK_APP=&quot;app.py&quot; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Flask 애플리케이션 실행&lt;/span&gt;&lt;/b&gt;&lt;span&gt;:&lt;/span&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;/span&gt;&lt;b&gt;&lt;span&gt;Enter 키를 눌러 명령을 확정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;터미널에 &lt;/span&gt;&lt;span&gt;* Running on http://127.0.0.1:5000&lt;/span&gt;&lt;span&gt;와 같은 메시지가 표시되면 서버가 정상적으로 실행된 것입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;flask run &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;웹 브라우저 접속&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 웹 브라우저를 열고 주소창에 &lt;/span&gt;&lt;span&gt;http://127.0.0.1:5000&lt;/span&gt;&lt;span&gt; 또는 &lt;/span&gt;&lt;span&gt;http://localhost:5000&lt;/span&gt;&lt;span&gt;을 입력하고 접속합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;기능 테스트&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 웹페이지의 &lt;/span&gt;&lt;span&gt;프롬프트:&lt;/span&gt;&lt;span&gt; 입력창에 질문을 입력하고 &quot;질문하기&quot; 버튼을 클릭합니다. Gemini 모델의 답변이 웹페이지 하단에 표시되는 것을 확인합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;서버 종료&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 터미널에서 &lt;/span&gt;&lt;span&gt;Ctrl&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;를 눌러 Flask 서버를 중지할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;6. 주요 문제 해결 (Troubleshooting)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;개발 과정에서 발생할 수 있는 일반적인 문제들과 그 해결 방법을 안내합니다.&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;b&gt;&lt;span&gt;문제&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;.\venv\Scripts\activate&lt;/span&gt;&lt;span&gt; 명령 실행 시 &quot;이 시스템에서 스크립트를 실행할 수 없으므로&quot; 오류 발생.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: Windows PowerShell의 기본 보안 설정(실행 정책)이 서명되지 않은 스크립트 실행을 막기 때문입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;해결&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;b&gt;&lt;span&gt;관리자 권한으로 PowerShell을 열어&lt;/span&gt;&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Set-ExecutionPolicy RemoteSigned&lt;/span&gt;&lt;span&gt; 명령을 실행하고 &lt;/span&gt;&lt;span&gt;Y&lt;/span&gt;&lt;span&gt;를 입력하여 정책을 변경합니다. (이 작업은 한 번만 수행하면 됩니다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;문제&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;flask run&lt;/span&gt;&lt;span&gt; 명령 실행 시 &quot;Failed to find Flask application or factory in module 'app'.&quot; 오류 발생.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: Flask가 &lt;/span&gt;&lt;span&gt;app.py&lt;/span&gt;&lt;span&gt; 파일 내의 앱 인스턴스를 찾지 못했거나, &lt;/span&gt;&lt;span&gt;FLASK_APP&lt;/span&gt;&lt;span&gt; 환경 변수 설정이 즉시 적용되지 않는 PowerShell의 특성 때문일 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;해결&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;FLASK_APP&lt;/span&gt;&lt;span&gt; 환경 변수 설정(&lt;/span&gt;&lt;span&gt;$env:FLASK_APP=&quot;app.py&quot;&lt;/span&gt;&lt;span&gt;)과 &lt;/span&gt;&lt;span&gt;flask run&lt;/span&gt;&lt;span&gt; 명령을 &lt;/span&gt;&lt;b&gt;&lt;span&gt;각각 다른 줄에서 실행&lt;/span&gt;&lt;/b&gt;&lt;span&gt;합니다. 즉, &lt;/span&gt;&lt;span&gt;$env:FLASK_APP=&quot;app.py&quot;&lt;/span&gt;&lt;span&gt; 입력 후 Enter, 다음 줄에서 &lt;/span&gt;&lt;span&gt;flask run&lt;/span&gt;&lt;span&gt; 입력 후 Enter.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;문제&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 웹 앱에서 질문 시 &quot;Gemini API 호출 중 오류가 발생했습니다: 404 models/gemini-pro is not found&quot; 또는 유사한 &lt;/span&gt;&lt;span&gt;404&lt;/span&gt;&lt;span&gt; 오류 발생.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;gemini-pro&lt;/span&gt;&lt;span&gt; 모델이 현재 Gemini API 환경에서 인식되지 않거나, 다른 모델을 사용해야 할 수 있습니다. &lt;/span&gt;&lt;span&gt;curl&lt;/span&gt;&lt;span&gt; 명령을 통해 &lt;/span&gt;&lt;span&gt;gemini-2.0-flash&lt;/span&gt;&lt;span&gt; 모델이 사용되는 것을 확인한 경우, 모델 이름 불일치가 원인일 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;해결&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;app.py&lt;/span&gt;&lt;span&gt; 파일의 &lt;/span&gt;&lt;span&gt;access_gemini&lt;/span&gt;&lt;span&gt; 함수에서 모델 이름을 &lt;/span&gt;&lt;span&gt;genai.GenerativeModel('models/gemini-2.0-flash')&lt;/span&gt;&lt;span&gt;로 변경합니다. &lt;/span&gt;&lt;span&gt;models/&lt;/span&gt;&lt;span&gt; 접두사를 붙여 명시적으로 모델 경로를 지정하는 것이 더 안정적입니다. 변경 후에는 Flask 서버를 &lt;/span&gt;&lt;span&gt;Ctrl&lt;/span&gt;&lt;span&gt; + &lt;/span&gt;&lt;span&gt;C&lt;/span&gt;&lt;span&gt;로 종료하고 &lt;/span&gt;&lt;span&gt;flask run&lt;/span&gt;&lt;span&gt;으로 재시작해야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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&gt;마무리하며:&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 가이드가 여러분의 블로그에 잘 활용되기를 바라며, 파이썬 Flask와 Gemini API를 활용한 멋진 프로젝트를 계속 만들어 나가시길 응원합니다! 궁금한 점이 있다면 언제든지 다시 찾아주세요.&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&gt; ️ 태그:&lt;/span&gt;&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;Flask&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;파이썬&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;웹개발&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Python&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;웹애플리케이션&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;AI&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Gemini&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;API&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;프로그래밍&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;코딩&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;초보개발&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;백엔드&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;개발가이드&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;입문&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;GeminiAPI&lt;/p&gt;</description>
      <category>dev</category>
      <category>AI</category>
      <category>flask</category>
      <category>Gemini</category>
      <category>GeminiAPI</category>
      <category>Python</category>
      <category>웹개발</category>
      <category>웹애플리케이션</category>
      <category>코딩</category>
      <category>파이썬</category>
      <category>프로그래밍</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/2</guid>
      <comments>https://quinton84.tistory.com/2#entry2comment</comments>
      <pubDate>Sun, 6 Jul 2025 02:28:07 +0900</pubDate>
    </item>
    <item>
      <title>코딩 초보도 OK! Flask로 시작하는 파이썬 웹 애플리케이션 개발 (feat. Gemini AI)</title>
      <link>https://quinton84.tistory.com/1</link>
      <description>&lt;h1&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;  Flask 쉽게 이해하기: 나만의 작은 웹 가게 만들기&lt;/span&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;안녕하세요, 코딩 선생님입니다! &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;오늘은 파이썬(Python)으로 웹사이트나 웹 애플리케이션을 만들고 싶을 때 유용하게 사용할 수 있는 도구, 바로 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;에 대해 쉽고 재미있게 알려드릴게요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;마치 나만의 작은 웹 가게를 차리는 것처럼 Flask의 핵심 개념을 함께 살펴볼까요?&lt;/span&gt;&lt;/p&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;span style=&quot;color: #1b1c1d;&quot;&gt;  Flask는 '나만의 작은 웹 가게'를 만드는 도구예요&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask는 파이썬(Python)이라는 언어로 웹사이트나 웹 애플리케이션을 만들 때 사용하는 도구 중 하나예요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;비유하자면, &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask는 여러분이 '나만의 작은 웹 가게'를 빠르고 쉽게 차릴 수 있도록 도와주는 최소한의 도구 상자&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;라고 생각하시면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;크고 복잡한 백화점(대규모 웹사이트)을 짓는 데 필요한 모든 도구와 설계도를 제공하는 대신, &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask는 여러분이 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;작고 아담한 나만의 카페나 빵집&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;을 여는 데 필요한 핵심 도구들만 딱 모아 놓은 것이죠.&lt;/span&gt;&lt;/p&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;span style=&quot;color: #1b1c1d;&quot;&gt; ️ Flask의 핵심 기능 (가게 비유로 설명)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;여러분이 작은 빵집을 연다고 상상해 보세요. Flask는 이 빵집을 운영하는 데 필요한 몇 가지 핵심적인 역할을 해줍니다.&lt;/span&gt;&lt;/p&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-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;손님 맞이 및 주문 받기 (라우팅 - Routing)&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;가게 비유&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 손님들이 빵집에 들어와서 &quot;안녕하세요&quot;, &quot;빵 주세요&quot;, &quot;케이크는 어디 있어요?&quot; 하고 말을 걸겠죠? Flask는 손님들이 웹사이트 주소(URL)로 접속했을 때, 어떤 요청이 들어왔는지 알아차리고 그 요청을 담당하는 곳으로 연결해주는 역할을 해요.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask 역할&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 웹사이트 주소 / (메인 페이지), /bread (빵 목록), /cake (케이크 목록) 등으로 접속했을 때, Flask는 각각의 주소에 맞는 파이썬 코드를 실행하도록 연결해 줍니다. 이걸 **라우팅(Routing)**이라고 해요.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;주방에서 요리하기 (파이썬 코드 실행)&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;가게 비유&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 손님에게 주문을 받으면, 주방에서 주문받은 빵이나 케이크를 만드는 과정이 필요하겠죠?&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask 역할&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 라우팅을 통해 연결된 파이썬 코드(함수)가 실행되면서, 필요한 데이터를 처리하고, 계산하고, AI 모델(Gemini)에게 질문을 보내 답변을 받아오는 등의 '요리'를 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;예쁜 접시에 담아 손님에게 내놓기 (템플릿 - Templates)&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;가게 비유&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 아무리 맛있는 빵이라도 그냥 봉투에 담아주기보다 예쁜 접시에 담아주면 손님이 더 좋아하겠죠?&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: circle; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask 역할&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: Flask는 HTML 파일을 '템플릿'으로 사용해요. 파이썬 코드에서 처리한 데이터(예: Gemini의 답변)를 이 템플릿에 '끼워 넣어서' 예쁘게 꾸며진 웹페이지(HTML)를 만들고, 이걸 손님(웹 브라우저)에게 보여줍니다.&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;  왜 Flask를 사용할까요?&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;가볍고 빠르다 (Lightweight)&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 필요한 기능만 딱 제공하기 때문에, 시작하기 쉽고 복잡하지 않아요. 작은 웹 앱을 만들 때 매우 효율적입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;자유롭다 (Flexible)&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: Flask는 개발자가 원하는 대로 기능을 추가하고 확장할 수 있도록 자유를 많이 줍니다. 어떤 데이터베이스를 쓸지, 어떤 인증 방식을 쓸지 등 모든 것을 개발자가 직접 선택할 수 있어요.&lt;/span&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;배우기 쉽다 (Easy to Learn)&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;: 파이썬 문법에 익숙하다면 Flask의 기본적인 사용법을 빠르게 익힐 수 있습니다.&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;5. 결론: 나만의 아이디어를 웹으로!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;오늘 우리는 Flask가 파이썬으로 &lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;작고 가벼운 웹 애플리케이션을 빠르게 만들 수 있도록 도와주는 최소한의 웹 개발 도구&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;라는 것을 알아보았습니다.&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;여러분의 멋진 아이디어를 웹사이트나 웹 서비스로 구현하고 싶을 때, Flask는 정말 좋은 시작점이 될 수 있어요. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;특히 오늘 우리가 배운 것처럼 AI 모델(Gemini)과 같은 외부 서비스와 연동하여 새로운 기능을 만들 때 Flask는 빛을 발한답니다!&lt;/span&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;어떠셨나요? &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b1c1d;&quot;&gt;Flask에 대해 조금 더 친숙해지셨기를 바랍니다. 이 글이 여러분의 첫 웹 개발 여정에 작은 도움이 되었으면 좋겠습니다. 궁금한 점이나 함께 나누고 싶은 이야기가 있다면 언제든지 댓글로 남겨주세요! 다음에는 더 유익한 내용으로 찾아오겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div class=&quot;revenue_unit_wrap&quot;&gt;
  &lt;div class=&quot;revenue_unit_item adsense responsive&quot;&gt;
    &lt;div class=&quot;revenue_unit_info&quot;&gt;반응형&lt;/div&gt;
    &lt;script src=&quot;//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&quot; async=&quot;async&quot;&gt;&lt;/script&gt;
    &lt;ins class=&quot;adsbygoogle&quot; style=&quot;display: block;&quot; data-ad-host=&quot;ca-host-pub-9691043933427338&quot; data-ad-client=&quot;ca-pub-7578665530606531&quot; data-ad-format=&quot;auto&quot;&gt;&lt;/ins&gt;
    &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push({});&lt;/script&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>dev</category>
      <category>AI</category>
      <category>API</category>
      <category>flask</category>
      <category>Gemini</category>
      <category>Python</category>
      <category>웹개발</category>
      <category>웹애플리케이션</category>
      <category>웹프레임워크</category>
      <category>코딩</category>
      <category>파이썬</category>
      <author>quinton84</author>
      <guid isPermaLink="true">https://quinton84.tistory.com/1</guid>
      <comments>https://quinton84.tistory.com/1#entry1comment</comments>
      <pubDate>Sun, 6 Jul 2025 02:05:12 +0900</pubDate>
    </item>
  </channel>
</rss>