<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>개발/프로그래밍 &#8211; 솜삽 블로그</title>
	<atom:link href="https://somsap.somsap.com/category/%EA%B0%9C%EB%B0%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D/feed/" rel="self" type="application/rss+xml" />
	<link>https://somsap.somsap.com</link>
	<description>개발, 업무, 피아노 등등</description>
	<lastBuildDate>Sun, 03 May 2026 13:47:17 +0000</lastBuildDate>
	<language>ko-KR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.7.5</generator>

<image>
	<url>https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/02/cropped-%EC%9D%B4%EB%AF%B8%EC%A7%80-1924-2.png?fit=32%2C32&#038;ssl=1</url>
	<title>개발/프로그래밍 &#8211; 솜삽 블로그</title>
	<link>https://somsap.somsap.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">241690237</site>	<item>
		<title>AICC 자동 인터뷰어(면접관) 시스템 개발기 (feat. FastAPI, Ollama)</title>
		<link>https://somsap.somsap.com/2026/05/03/aicc-%ec%9e%90%eb%8f%99-%eb%a9%b4%ec%a0%91%ea%b4%80-%ec%8b%9c%ec%8a%a4%ed%85%9c-%ea%b0%9c%eb%b0%9c%ea%b8%b0-feat-fastapi-ollama/</link>
					<comments>https://somsap.somsap.com/2026/05/03/aicc-%ec%9e%90%eb%8f%99-%eb%a9%b4%ec%a0%91%ea%b4%80-%ec%8b%9c%ec%8a%a4%ed%85%9c-%ea%b0%9c%eb%b0%9c%ea%b8%b0-feat-fastapi-ollama/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Sun, 03 May 2026 13:46:37 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<category><![CDATA[AICC]]></category>
		<category><![CDATA[AI면접관]]></category>
		<category><![CDATA[FastAPI]]></category>
		<category><![CDATA[IT정보]]></category>
		<category><![CDATA[LLM]]></category>
		<category><![CDATA[인공지능]]></category>
		<category><![CDATA[자동인터뷰어]]></category>
		<category><![CDATA[파이썬]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=5965</guid>

					<description><![CDATA[<p>안녕하세요! 오늘은 최근 IT 업계에서 가장 뜨거운 주제 중 하나인 AICC(인공지능 컨택센터)와 관련된 아주 흥미로운 시스템을 하나 소개해드리려고 합니다. 바로 &#8216;AICC 자동 인터뷰어&#8217;인데요. 혹시 AI와 대화해 보신 적 있으신가요? 보통은 텍스트로 타이핑을 치며 대화하는 챗봇 형태를 떠올리실 텐데요. 오늘 살펴볼 이 시스템은 단순히 글자로 대화하는 것을 넘어, 사람처럼 내 목소리를 듣고 자신의 목소리로 다음 질문을 [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2026/05/03/aicc-%ec%9e%90%eb%8f%99-%eb%a9%b4%ec%a0%91%ea%b4%80-%ec%8b%9c%ec%8a%a4%ed%85%9c-%ea%b0%9c%eb%b0%9c%ea%b8%b0-feat-fastapi-ollama/">AICC 자동 인터뷰어(면접관) 시스템 개발기 (feat. FastAPI, Ollama)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<p>안녕하세요! 오늘은 최근 IT 업계에서 가장 뜨거운 주제 중 하나인 AICC(인공지능 컨택센터)와 관련된 아주 흥미로운 시스템을 하나 소개해드리려고 합니다. 바로 &#8216;AICC 자동 인터뷰어&#8217;인데요.</p>



<p>혹시 AI와 대화해 보신 적 있으신가요? 보통은 텍스트로 타이핑을 치며 대화하는 챗봇 형태를 떠올리실 텐데요. 오늘 살펴볼 이 시스템은 단순히 글자로 대화하는 것을 넘어, 사람처럼 내 목소리를 듣고 자신의 목소리로 다음 질문을 던지는 똑똑한 &#8216;자동 인터뷰어&#8217;입니다. 개발 가이드 문서를 바탕으로, 이 복잡해 보이는 기술이 도대체 어떻게 돌아가는지 일반인 분들도 쉽게 이해하실 수 있도록 하나하나 친절하게 풀어드릴게요!</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">1. AI 면접관, 도대체 어떤 기술로 만들어졌을까?</h2>



<p>이 똑똑한 AI 인터뷰어가 사람처럼 듣고 말하기 위해서는 여러 가지 최신 기술들이 마치 톱니바퀴처럼 맞물려 돌아가야 합니다. 핵심만 쏙쏙 뽑아볼까요?</p>



<ul class="wp-block-list">
<li><strong>서버와 뼈대 (FastAPI):</strong> 전체 시스템이 빠르고 안정적으로 돌아가도록 도와주는 중심 역할입니다.</li>



<li><strong>AI의 뇌 (Ollama + EXAONE 3.5):</strong> 외부 인터넷을 거치지 않고 내 컴퓨터 안에서 직접 돌아가는 강력한 한국어 특화 인공지능 모델을 사용합니다. 보안성도 좋고 대답도 아주 똑똑하게 해내죠.</li>



<li><strong>AI의 귀 (STT &#8211; faster-whisper):</strong> 사람이 말하는 음성을 실시간으로 듣고 텍스트로 변환해 주는 기술입니다.</li>



<li><strong>AI의 입 (TTS &#8211; edge-tts):</strong> 텍스트를 다시 자연스러운 사람의 목소리(한국어 남성 목소리 등)로 읽어주는 기술입니다.</li>



<li><strong>마법의 노트 (RAG &amp; ChromaDB):</strong> 인터뷰이가 과거 모임 등에 대해 질문하면, 저장된 데이터베이스를 슬쩍 참고해서 자연스럽게 대답할 수 있게 해주는 기억 장치입니다.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">2. 물 흐르듯 자연스러운 인터뷰 진행 과정</h2>



<p></p>



<p>그렇다면 실제 인터뷰는 어떤 흐름으로 진행될까요? 마치 실제 면접장에 온 것 같은 치밀한 과정을 거칩니다.</p>



<ol start="1" class="wp-block-list">
<li><strong>인사 및 첫 질문:</strong> 세션이 시작되면 AI가 미리 준비된 첫 번째 질문을 음성(TTS)으로 자연스럽게 읽어줍니다.</li>



<li><strong>침묵 감지기 (VAD) 작동:</strong> 인터뷰이가 대답을 시작하면, AI는 조용히 듣고 있습니다. 말이 자연스럽게 끝맺어지면 2초, 완전히 조용해지면 5초 정도 기다렸다가 대답이 끝났다고 판단합니다. 눈치코치 백 단이죠!</li>



<li><strong>질문일까? 대답일까?:</strong> 인터뷰이의 말이 끝나면 인공지능이 그 내용을 분석합니다. 만약 인터뷰이가 역으로 질문을 했다면 관련 데이터를 바탕으로 대답을 해주고, 평범한 대답이었다면 내용에 맞춰 &#8216;꼬리 질문&#8217;을 던지거나 다음 주제로 넘어갑니다.</li>



<li><strong>마무리 및 결과 저장:</strong> 정해진 질문 개수가 끝나면 부드러운 마무리 멘트와 함께 인터뷰가 종료됩니다. 이후 모든 대화 내용과 녹화된 영상, 음성이 하나로 예쁘게 합쳐져 관리자에게 리포트로 제공됩니다.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">3. 한눈에 보는 시스템 아키텍처 (개발자 감성 한 스푼 🥄)</h2>



<p>말로만 들으면 조금 복잡하죠? 전체 시스템이 어떻게 소통하는지 아래 다이어그램으로 한눈에 살펴보세요. 복잡한 코드를 몰라도 데이터가 어떻게 흘러가는지 쉽게 이해하실 수 있을 거예요.</p>



<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#0a4a4a', 'primaryTextColor': '#aaffee', 'primaryBorderColor': '#00e5cc', 'lineColor': '#00e5cc', 'secondaryColor': '#062e2e', 'tertiaryColor': '#041e1e', 'clusterBkg': '#062e2e', 'clusterBorder': '#00e5cc', 'titleColor': '#aaffee', 'edgeLabelBackground': '#062e2e', 'fontFamily': 'monospace'}}}%%
flowchart LR
    subgraph Client["🖥️ 사용자 인터페이스"]
        A["인터뷰이 화면&lt;br/&gt;(순수 HTML/JS)"]
    end

    subgraph Server["⚙️ FastAPI 서버"]
        B["세션 관리 및 오디오 수신"]
        C["STT 엔진&lt;br/&gt;(음성을 텍스트로)"]
        D["TTS 엔진&lt;br/&gt;(텍스트를 음성으로)"]
    end

    subgraph AI_RAG["🤖 인공지능 &amp; 데이터베이스"]
        F["Ollama EXAONE 3.5&lt;br/&gt;(두뇌 역할)"]
        G["임베딩 모델&lt;br/&gt;(ko-sroberta)"]
        H[("ChromaDB&lt;br/&gt;(기존 정보 DB)")]
    end

    A -- "① 음성 전송" --&gt; B
    B -- "② 음성 데이터" --&gt; C
    C -- "③ 텍스트 변환 결과" --&gt; F
    F -. "④ 모임 관련 질문 시" .-&gt; G
    G -. "⑤ 벡터 검색 요청" .-&gt; H
    H -. "⑥ 관련 분위기/정보 반환" .-&gt; F
    F -- "⑦ 꼬리 질문/답변 생성" --&gt; D
    D -- "⑧ 음성 파일 생성" --&gt; B
    B -- "⑨ 질문 재생" --&gt; A

    style A fill:#0e6e4d,stroke:#34d399,color:#d1fae5
    style B fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style C fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style D fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style F fill:#2d1b4e,stroke:#c084fc,color:#f0d6ff
    style G fill:#2d1b4e,stroke:#c084fc,color:#f0d6ff
    style H fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style Client fill:#062e1e,stroke:#34d399
    style Server fill:#041e2e,stroke:#38bdf8
    style AI_RAG fill:#110a1f,stroke:#c084fc</pre></div>



<h2 class="wp-block-heading">4. 헛소리를 원천 차단하는 똑똑한 필터링 기술</h2>



<p>AI가 가끔 거짓말을 하거나 이상한 말을 지어내는 현상을 &#8216;할루시네이션(환각)&#8217;이라고 부릅니다. 이 자동 인터뷰어는 이런 문제를 막기 위해 아주 재미있고 철저한 안전장치들을 마련해두었어요.</p>



<ul class="wp-block-list">
<li><strong>침묵과 소음 걸러내기:</strong> 마이크에 아무 말도 안 했는데 &#8220;감사합니다&#8221;, &#8220;네&#8221;, &#8220;아&#8221; 처럼 무의미한 소음이 텍스트로 잡히면 AI가 이를 무시하고 다시 녹음을 요청합니다.</li>



<li><strong>TV 소음 필터링:</strong> 주변에서 틀어놓은 TV 소리 때문에 &#8220;MBC&#8221;, &#8220;KBS&#8221;, &#8220;SBS&#8221; 같은 단어가 섞여 들어오면 가짜 답변으로 인식하고 차단합니다.</li>



<li><strong>메아리(Echo) 방지:</strong> 스피커에서 나오는 AI 자신의 목소리가 다시 마이크로 들어가는 것을 방지하기 위해, 직전 질문과 50% 이상 겹치는 단어가 들리면 자동으로 필터링합니다.</li>



<li><strong>없는 사실 지어내지 않기:</strong> &#8220;인터뷰이가 말하지 않은 내용&#8221;을 억지로 엮어서 &#8220;말씀하셨으니~&#8221;라며 꼬리 질문을 지어내지 않도록 모델을 꼼꼼하게 통제합니다.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p>어떠신가요? 단순한 챗봇을 넘어 사람의 말을 듣고, 이해하고, 적절하게 반응하는 AICC 자동 인터뷰어 기술! 미래의 면접이나 상담은 정말 이렇게 똑똑하고 친절한 AI와 함께하게 될 날이 머지않은 것 같습니다.</p>



<p>다음에도 흥미롭고 유익한 IT 이야기로 찾아오겠습니다. 궁금한 점이 있으시다면 언제든 편하게 댓글 남겨주세요!</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2026/05/03/aicc-%ec%9e%90%eb%8f%99-%eb%a9%b4%ec%a0%91%ea%b4%80-%ec%8b%9c%ec%8a%a4%ed%85%9c-%ea%b0%9c%eb%b0%9c%ea%b8%b0-feat-fastapi-ollama/">AICC 자동 인터뷰어(면접관) 시스템 개발기 (feat. FastAPI, Ollama)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2026/05/03/aicc-%ec%9e%90%eb%8f%99-%eb%a9%b4%ec%a0%91%ea%b4%80-%ec%8b%9c%ec%8a%a4%ed%85%9c-%ea%b0%9c%eb%b0%9c%ea%b8%b0-feat-fastapi-ollama/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5965</post-id>	</item>
		<item>
		<title>🎙️ 로컬 AI로 만드는 인터뷰 분석기 개발기 &#8211; 외부 API 없이 완전 오프라인 구현</title>
		<link>https://somsap.somsap.com/2026/03/30/%f0%9f%8e%99%ef%b8%8f-%eb%a1%9c%ec%bb%ac-ai%eb%a1%9c-%eb%a7%8c%eb%93%9c%eb%8a%94-%ec%9d%b8%ed%84%b0%eb%b7%b0-%eb%b6%84%ec%84%9d%ea%b8%b0-%ea%b0%9c%eb%b0%9c%ea%b8%b0-%ec%99%b8%eb%b6%80-api-%ec%97%86/</link>
					<comments>https://somsap.somsap.com/2026/03/30/%f0%9f%8e%99%ef%b8%8f-%eb%a1%9c%ec%bb%ac-ai%eb%a1%9c-%eb%a7%8c%eb%93%9c%eb%8a%94-%ec%9d%b8%ed%84%b0%eb%b7%b0-%eb%b6%84%ec%84%9d%ea%b8%b0-%ea%b0%9c%eb%b0%9c%ea%b8%b0-%ec%99%b8%eb%b6%80-api-%ec%97%86/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Mon, 30 Mar 2026 12:02:29 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<category><![CDATA[ChromaDB]]></category>
		<category><![CDATA[CUDA]]></category>
		<category><![CDATA[EXAONE]]></category>
		<category><![CDATA[FastAPI]]></category>
		<category><![CDATA[faster-whisper]]></category>
		<category><![CDATA[HuggingFace]]></category>
		<category><![CDATA[mlx-whisper]]></category>
		<category><![CDATA[Ollama]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[RAG]]></category>
		<category><![CDATA[Streamlit]]></category>
		<category><![CDATA[STT]]></category>
		<category><![CDATA[Whisper]]></category>
		<category><![CDATA[개인정보보호]]></category>
		<category><![CDATA[로컬AI]]></category>
		<category><![CDATA[벡터DB]]></category>
		<category><![CDATA[오프라인AI]]></category>
		<category><![CDATA[음성인식]]></category>
		<category><![CDATA[인터뷰분석기]]></category>
		<category><![CDATA[자연어처리]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=3432</guid>

					<description><![CDATA[<p>외부 API 비용과 데이터 유출 걱정은 이제 그만! STT(Whisper), 벡터 DB, LLM(Ollama)을 결합하여 100% 로컬 환경에서 무료로 동작하는 'AI 인터뷰 분석기' 개발 과정을 아키텍처부터 하드웨어 최적화까지 상세히 공유합니다.</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2026/03/30/%f0%9f%8e%99%ef%b8%8f-%eb%a1%9c%ec%bb%ac-ai%eb%a1%9c-%eb%a7%8c%eb%93%9c%eb%8a%94-%ec%9d%b8%ed%84%b0%eb%b7%b0-%eb%b6%84%ec%84%9d%ea%b8%b0-%ea%b0%9c%eb%b0%9c%ea%b8%b0-%ec%99%b8%eb%b6%80-api-%ec%97%86/">🎙️ 로컬 AI로 만드는 인터뷰 분석기 개발기 &#8211; 외부 API 없이 완전 오프라인 구현</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading" id="프로젝트-개요">프로젝트 개요</h2>



<p>최근 AI 기술이 발전하면서 음성 인식과 자연어 처리를 활용한 다양한 서비스들이 등장하고 있습니다.&nbsp;하지만 대부분의 서비스들이 외부 API에 의존하고 있어 비용 부담과 데이터 보안 우려가 있었습니다.</p>



<p>이번에 개발한&nbsp;&#8216;인터뷰 분석기&#8217;는&nbsp;<strong>완전히 로컬 환경에서 동작</strong>하는 AI 서비스로,&nbsp;인터뷰 녹음 파일을 업로드하면 음성을 텍스트로 변환하고,&nbsp;변환된 내용을 바탕으로 자유롭게 질의응답할 수 있는 시스템입니다.</p>



<h3 class="wp-block-heading" id="주요-특징">주요 특징</h3>



<ul class="wp-block-list">
<li>🔒 <strong>완전 오프라인</strong>: 모든 데이터가 로컬에서만 처리</li>



<li>🎯 <strong>올인원 솔루션</strong>: STT, 임베딩, LLM까지 통합</li>



<li>💰 <strong>제로 비용</strong>: 외부 API 호출 없음</li>



<li>🚀 <strong>하드웨어 최적화</strong>: Windows/Mac 환경별 자동 최적화</li>
</ul>



<h2 class="wp-block-heading" id="기술-스택-선정">기술 스택 선정</h2>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="1024" height="572" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-20.png?resize=1024%2C572&#038;ssl=1" alt="image 20" class="wp-image-3440" title="🎙️ 로컬 AI로 만드는 인터뷰 분석기 개발기 - 외부 API 없이 완전 오프라인 구현 1" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-20.png?w=1024&amp;ssl=1 1024w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-20.png?resize=300%2C168&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-20.png?resize=768%2C429&amp;ssl=1 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<h3 class="wp-block-heading" id="핵심-기술-구성">핵심 기술 구성</h3>



<pre class="wp-block-code"><code><code>Frontend: Streamlit (빠른 프로토타이핑)
Backend: FastAPI (비동기 처리)
STT: faster-whisper / mlx-whisper (로컬 실행)
Vector DB: ChromaDB (임베딩 저장)
LLM: Ollama (로컬 추론)
Embedding: HuggingFace Transformers</code></code></pre>



<h3 class="wp-block-heading" id="왜-이-기술들을-선택했나">왜 이 기술들을 선택했나?</h3>



<p><strong>1. STT 엔진 선택</strong></p>



<ul class="wp-block-list">
<li>OpenAI Whisper API: 정확하지만 비용 발생</li>



<li><strong>faster-whisper</strong>: GPU 가속, 로컬 실행, 무료</li>



<li><strong>mlx-whisper</strong>: Mac M1/M2/M3 최적화</li>
</ul>



<p><strong>2. LLM 선택</strong></p>



<ul class="wp-block-list">
<li>ChatGPT API: 성능 좋지만 토큰당 과금</li>



<li><strong>Ollama</strong>: 로컬 실행, 다양한 모델 지원, 무료</li>
</ul>



<p><strong>3. 벡터 DB 선택</strong></p>



<ul class="wp-block-list">
<li>Pinecone: 클라우드 기반, 유료</li>



<li><strong>ChromaDB</strong>: 로컬 저장, 가벼움, 무료</li>
</ul>



<h2 class="wp-block-heading" id="개발-과정">개발 과정</h2>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" decoding="async" width="1024" height="572" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-22.png?resize=1024%2C572&#038;ssl=1" alt="image 22" class="wp-image-3442" title="🎙️ 로컬 AI로 만드는 인터뷰 분석기 개발기 - 외부 API 없이 완전 오프라인 구현 2" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-22.png?w=1024&amp;ssl=1 1024w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-22.png?resize=300%2C168&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-22.png?resize=768%2C429&amp;ssl=1 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<h3 class="wp-block-heading" id="1단계-기본-아키텍처-설계">1단계: 기본 아키텍처 설계</h3>



<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#0a4a4a', 'primaryTextColor': '#aaffee', 'primaryBorderColor': '#00e5cc', 'lineColor': '#00e5cc', 'secondaryColor': '#062e2e', 'tertiaryColor': '#041e1e', 'clusterBkg': '#062e2e', 'clusterBorder': '#00e5cc', 'titleColor': '#aaffee', 'edgeLabelBackground': '#062e2e', 'fontFamily': 'monospace'}}}%%
flowchart TD
    subgraph Client["🖥️ 클라이언트"]
        A["Streamlit UI&lt;br/&gt;(녹음/업로드/채팅 인터페이스)"]
    end

    subgraph Server["⚙️ 서버"]
        B["FastAPI&lt;br/&gt;(요청 수신 및 비동기 처리)"]
        C["STT&lt;br/&gt;(faster-whisper/CUDA&lt;br/&gt;음성 → 텍스트 변환)"]
        G["HuggingFace Embeddings&lt;br/&gt;(텍스트 → 벡터 변환)"]
        E[("ChromaDB&lt;br/&gt;(벡터 저장/검색)")]
        D["RAG Chain&lt;br/&gt;(문맥 검색 + LLM 답변 생성)"]
    end

    subgraph AI["🤖 AI 모델"]
        H["Ollama EXAONE 3.5&lt;br/&gt;(한국어 특화 LLM 추론)"]
    end

    A -- "① 오디오 업로드" --&gt; B
    B -- "② STT 변환" --&gt; C
    C -- "③ 텍스트" --&gt; G
    G -- "④ 벡터화 후 저장" --&gt; E

    A -- "⑤ 질문" --&gt; B
    B -- "⑥ RAG 실행" --&gt; D
    D -- "⑦ 벡터 검색" --&gt; E
    E -- "⑧ 관련 문맥 반환" --&gt; D
    D -- "⑨ 문맥 + 질문 전달" --&gt; H
    H -- "⑩ 답변 생성" --&gt; D
    D -- "⑪ 답변 반환" --&gt; B
    B -- "⑫ 답변 반환" --&gt; A

    style A fill:#0e6e4d,stroke:#34d399,color:#d1fae5
    style B fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style C fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style D fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style E fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style G fill:#0e4d6e,stroke:#38bdf8,color:#e0f9ff
    style H fill:#2d1b4e,stroke:#c084fc,color:#f0d6ff
    style Client fill:#062e1e,stroke:#34d399
    style Server fill:#041e2e,stroke:#38bdf8
    style AI fill:#110a1f,stroke:#c084fc</pre></div>



<p>처음에는 단순한 구조로 시작했지만,&nbsp;성능과 사용성을 고려하여 점진적으로 개선했습니다.</p>



<h3 class="wp-block-heading" id="2단계-하드웨어-최적화-로직-구현">2단계: 하드웨어 최적화 로직 구현</h3>



<p>가장 까다로웠던 부분은 다양한 하드웨어 환경에서 최적의 성능을 내는 것이었습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code>def get_optimal_hardware_config():
    """시스템 환경을 자동 감지하여 최적 설정 반환"""
    os_name = platform.system()
    machine_arch = platform.machine()
    cpu_cores = max(1, (os.cpu_count() or 4) - 1)

    if os_name == "Darwin" and machine_arch == "arm64":
        # Mac M1/M2/M3: Metal GPU 활용
        return {
            "engine": "mlx",
            "model_size": "medium"
        }
    elif os_name == "Windows":
        # Windows: NVIDIA CUDA 활용
        return {
            "engine": "faster-whisper",
            "model_size": "medium",
            "device": "cuda",
            "compute_type": "float16"
        }
    else:
        # 기타 환경: CPU 최적화
        return {
            "engine": "faster-whisper",
            "model_size": "medium",
            "device": "auto",
            "compute_type": "int8"
        }<span style="font-size: 0.8em"></span></code></code></pre></div>



<h3 class="wp-block-heading" id="3단계-cuda-환경-설정-문제-해결">3단계: CUDA 환경 설정 문제 해결</h3>



<p>Windows 환경에서 가장 큰 난관은 CUDA 라이브러리 인식 문제였습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code># 핵심 해결책: faster_whisper 임포트 전에 CUDA 경로 강제 등록
if platform.system() == "Windows":
    cuda_bin_path = r"D:\Program Files\NVIDIA\CUDA\v11.2\bin"
    if os.path.exists(cuda_bin_path):
        os.add_dll_directory(cuda_bin_path)
        os.environ&#091;"PATH"] = cuda_bin_path + os.pathsep + os.environ.get("PATH", "")

from faster_whisper import WhisperModel  # 이제 정상 작동<span style="font-size: 0.8em"></span></code></code></pre></div>



<h3 class="wp-block-heading" id="4단계-비동기-처리-및-성능-최적화">4단계: 비동기 처리 및 성능 최적화</h3>



<p>STT 변환은 시간이 오래 걸리는 작업이므로 비동기 처리가 필수였습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code>@app.post("/api/upload-audio")
async def upload_audio(file: UploadFile = File(...)):
    # 파일 저장을 비동기로 처리
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(thread_pool, lambda: open(file_path, "wb").write(content))
    
    # STT 변환을 별도 스레드에서 실행
    transcribed_text = await loop.run_in_executor(
        thread_pool, lambda: _run_stt(file_path, file.filename, start_time)
    )
    
    # 벡터 DB 저장도 비동기 처리
    await loop.run_in_executor(thread_pool, lambda: save_to_vector_db(safe_text, file.filename))<span style="font-size: 0.8em"></span></code></code></pre></div>



<h3 class="wp-block-heading" id="5단계-rag-시스템-구현">5단계: RAG 시스템 구현</h3>



<p>단순한 STT를 넘어서 질의응답이 가능한 RAG(Retrieval-Augmented Generation)&nbsp;시스템을 구현했습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code># 텍스트를 청크 단위로 분할하여 벡터 DB에 저장
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,      # 청크 크기
    chunk_overlap=50     # 겹침 정도
)

# 한국어 특화 임베딩 모델 사용
embeddings = HuggingFaceEmbeddings(
    model_name="jhgan/ko-sroberta-multitask"
)

# ChromaDB에 벡터 저장
vector_db = Chroma.from_texts(
    texts=chunks,
    embedding=embeddings,
    metadatas=metadatas,
    persist_directory="chroma_db"
)<span style="font-size: 0.8em"></span></code></code></pre></div>



<h3 class="wp-block-heading" id="6단계-개인정보-보호-기능">6단계: 개인정보 보호 기능</h3>



<p>인터뷰 데이터의 민감성을 고려하여 자동 마스킹 기능을 추가했습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code>def mask_pii(text: str) -&gt; str:
    """개인정보 자동 마스킹"""
    # 주민등록번호 마스킹
    text = re.sub(r'(\d{6})&#091;-]\d{7}', r'\1-*******', text)
    # 전화번호 마스킹
    text = re.sub(r'(010)&#091;-]\d{4}&#091;-]\d{4}', r'\1-****-****', text)
    return text<span style="font-size: 0.8em"></span></code></code></pre></div>



<h2 class="wp-block-heading" id="개발-중-마주한-도전과-해결책">개발 중 마주한 도전과 해결책</h2>



<h3 class="wp-block-heading" id="도전-1-mac-m1-환경에서의-성능-이슈">도전 1: Mac M1 환경에서의 성능 이슈</h3>



<p><strong>문제</strong>:&nbsp;Mac M1에서 faster-whisper 성능이 현저히 떨어짐&nbsp;<strong>해결</strong>:&nbsp;mlx-whisper로 분기 처리하여 Metal GPU 활용</p>



<h3 class="wp-block-heading" id="도전-2-메모리-사용량-최적화">도전 2: 메모리 사용량 최적화</h3>



<p><strong>문제</strong>:&nbsp;큰 오디오 파일 처리 시 메모리 부족&nbsp;<strong>해결</strong>:&nbsp;스트리밍 처리 및 임시 파일 자동 삭제</p>



<h3 class="wp-block-heading" id="도전-3-인터뷰-대상자-구분">도전 3: 인터뷰 대상자 구분</h3>



<p><strong>문제</strong>:&nbsp;여러 명이 참여한 인터뷰에서 화자 구분 어려움&nbsp;<strong>해결</strong>:&nbsp;LLM을 활용한 이름 추출 및 메타데이터 관리</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code>def extract_interviewee_name(text: str) -&gt; str:
    """LLM으로 인터뷰 대상자 이름 추출, 실패 시 순번 반환"""
    global _interviewee_counter
    _interviewee_counter += 1

    try:
        llm = Ollama(model="exaone3.5:7.8b", temperature=0)
        answer = llm.invoke(
            f"""아래 인터뷰 텍스트에서 인터뷰 대상자(피면접자)의 이름만 추출하세요.
이름이 명확히 언급된 경우에만 이름을 반환하고, 불확실하면 반드시 'UNKNOWN'만 반환하세요.
다른 말은 절대 하지 마세요.

텍스트:\n{text&#091;:1000]}"""
        )
        name = answer.strip().replace("'", "").replace('"', "")
        if name and name != "UNKNOWN" and len(name) &lt;= 10:
            return name
    except Exception:
        pass

    return f"interviewee_{_interviewee_counter}"<span style="font-size: 0.8em"></span></code></code></pre></div>



<h2 class="wp-block-heading" id="성능-최적화-결과">사용자 경험 개선</h2>



<h3 class="wp-block-heading" id="실시간-진행률-표시">실시간 진행률 표시</h3>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code># STT 진행률을 실시간으로 표시
for segment in segments:
    progress_percent = (segment.end / total_duration) * 100
    elapsed_time = time.time() - start_time
    remaining_time = (total_duration - segment.end) * (elapsed_time / segment.end)
    
    print(f"진행률: {progress_percent:5.1f}% | "
          f"경과: {elapsed_time:4.1f}초 | "
          f"남은 시간: {remaining_time:4.1f}초")<span style="font-size: 0.8em"></span></code></code></pre></div>



<h3 class="wp-block-heading" id="직관적인-웹-인터페이스">직관적인 웹 인터페이스</h3>



<p>Streamlit을 활용하여 3개 탭으로 구성:</p>



<ol class="wp-block-list">
<li><strong>녹음 탭</strong>: 브라우저에서 직접 녹음</li>



<li><strong>파일 업로드 탭</strong>: mp3, wav, m4a, webm, ogg, flac 파일 드래그 앤 드롭</li>



<li><strong>AI 채팅 탭</strong>: 분석 결과 질의응답</li>
</ol>



<h2 class="wp-block-heading" id="배포-및-운영">배포 및 운영</h2>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" decoding="async" width="1024" height="572" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-23.png?resize=1024%2C572&#038;ssl=1" alt="image 23" class="wp-image-3443" title="🎙️ 로컬 AI로 만드는 인터뷰 분석기 개발기 - 외부 API 없이 완전 오프라인 구현 3" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-23.png?w=1024&amp;ssl=1 1024w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-23.png?resize=300%2C168&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/03/image-23.png?resize=768%2C429&amp;ssl=1 768w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<h3 class="wp-block-heading" id="원클릭-실행-스크립트">원클릭 실행 스크립트</h3>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"><code>@echo off
call .\venv\Scripts\activate
start "FastAPI Server" cmd /k "uvicorn main:app --reload"
start "ngrok" cmd /k "ngrok http 8000"
start "Streamlit Client" cmd /k "streamlit run client/app.py"
start "Cloudflared" cmd /k "cloudflared tunnel --url http://127.0.0.1:8501"<span style="font-size: 0.8em"></span></code></code></pre></div>



<h3 class="wp-block-heading" id="외부-접속-지원">외부 접속 지원</h3>



<ul class="wp-block-list">
<li><strong>ngrok</strong>: 서버 터널링</li>



<li><strong>cloudflared</strong>: 클라이언트 터널링</li>
</ul>



<h2 class="wp-block-heading" id="향후-개선-계획">향후 개선 계획</h2>



<h3 class="wp-block-heading" id="1-화자-분리-speaker-diarization">1. 화자 분리 (Speaker Diarization)</h3>



<p>현재는 이름 추출로만 구분하지만,&nbsp;실제 음성 패턴을 분석하여 화자를 자동 분리하는 기능 추가 예정</p>



<h3 class="wp-block-heading" id="2-다국어-지원">2. 다국어 지원</h3>



<p>현재 한국어 중심이지만,&nbsp;영어,&nbsp;일본어 등 다국어 STT 및 질의응답 지원</p>



<h3 class="wp-block-heading" id="3-실시간-스트리밍">3. 실시간 스트리밍</h3>



<p>파일 업로드가 아닌 실시간 음성 스트리밍 분석 기능</p>



<h3 class="wp-block-heading" id="4-고급-분석-기능">4. 고급 분석 기능</h3>



<ul class="wp-block-list">
<li>감정 분석</li>



<li>키워드 추출</li>



<li>요약 생성</li>



<li>인사이트 도출</li>
</ul>



<h2 class="wp-block-heading" id="마무리">마무리</h2>



<p>이 프로젝트를 통해 <strong>&#8220;외부 API 없이도 충분히 실용적인 AI 서비스를 만들 수 있다&#8221;</strong>는 것을 증명할 수 있었습니다.</p>



<p>특히 인상 깊었던 점들:</p>



<ol class="wp-block-list">
<li><strong>로컬 AI의 가능성</strong>: 생각보다 성능이 뛰어나고 실용적</li>



<li><strong>비용 효율성</strong>: 장시간 인터뷰도 추가 비용 없이 처리 가능</li>



<li><strong>보안성</strong>: 민감한 인터뷰 데이터가 외부로 전송되지 않음</li>



<li><strong>커스터마이징</strong>: 필요에 따라 모델이나 설정을 자유롭게 변경 가능</li>
</ol>



<p>앞으로도 로컬 AI 기술이 더욱 발전하여,&nbsp;개인이나 소규모 팀도 쉽게 AI 서비스를 구축할 수 있는 시대가 올 것이라 기대합니다.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>이 글이 도움이 되셨다면&nbsp;❤️&nbsp;좋아요와 공유 부탁드립니다!</p>
</blockquote>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2026/03/30/%f0%9f%8e%99%ef%b8%8f-%eb%a1%9c%ec%bb%ac-ai%eb%a1%9c-%eb%a7%8c%eb%93%9c%eb%8a%94-%ec%9d%b8%ed%84%b0%eb%b7%b0-%eb%b6%84%ec%84%9d%ea%b8%b0-%ea%b0%9c%eb%b0%9c%ea%b8%b0-%ec%99%b8%eb%b6%80-api-%ec%97%86/">🎙️ 로컬 AI로 만드는 인터뷰 분석기 개발기 &#8211; 외부 API 없이 완전 오프라인 구현</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2026/03/30/%f0%9f%8e%99%ef%b8%8f-%eb%a1%9c%ec%bb%ac-ai%eb%a1%9c-%eb%a7%8c%eb%93%9c%eb%8a%94-%ec%9d%b8%ed%84%b0%eb%b7%b0-%eb%b6%84%ec%84%9d%ea%b8%b0-%ea%b0%9c%eb%b0%9c%ea%b8%b0-%ec%99%b8%eb%b6%80-api-%ec%97%86/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3432</post-id>	</item>
		<item>
		<title>맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기)</title>
		<link>https://somsap.somsap.com/2026/01/23/%eb%a7%a5%eb%b6%81-%ea%b0%a4%eb%9f%ad%ec%8b%9c-%eb%af%b8%eb%9f%ac%eb%a7%81-%eb%ac%b4%eb%a3%8c-scrcpy%eb%a1%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b2%8c-%eb%ac%b4%ec%84%a0/</link>
					<comments>https://somsap.somsap.com/2026/01/23/%eb%a7%a5%eb%b6%81-%ea%b0%a4%eb%9f%ad%ec%8b%9c-%eb%af%b8%eb%9f%ac%eb%a7%81-%eb%ac%b4%eb%a3%8c-scrcpy%eb%a1%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b2%8c-%eb%ac%b4%ec%84%a0/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Fri, 23 Jan 2026 10:12:54 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<category><![CDATA[adb 무선 연결]]></category>
		<category><![CDATA[homebrew 사용법]]></category>
		<category><![CDATA[scrcpy 사용법]]></category>
		<category><![CDATA[개발자 옵션]]></category>
		<category><![CDATA[맥 갤럭시 연결]]></category>
		<category><![CDATA[맥OS 터미널]]></category>
		<category><![CDATA[맥북 안드로이드 미러링]]></category>
		<category><![CDATA[안드로이드 화면 공유]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=2601</guid>

					<description><![CDATA[<p>맥북을 사용하면서 안드로이드 폰(갤럭시) 화면을 맥에 띄워서 제어하고 싶을 때가 종종 있습니다. 유료 앱들도 있지만, 오픈소스인 scrcpy를 사용하면 무료임에도 불구하고 화질이 좋고 반응 속도도 매우 빠릅니다. 최근 커뮤니티 글을 참고하여 맥 터미널 환경에서 설치부터 무선 연결까지 직접 세팅해 보았습니다. 추후 재설정이 필요할 때를 대비하여 과정을 자세히 기록해 둡니다. 1. 터미널 열고 설치하기 (Homebrew &#38; scrcpy) [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2026/01/23/%eb%a7%a5%eb%b6%81-%ea%b0%a4%eb%9f%ad%ec%8b%9c-%eb%af%b8%eb%9f%ac%eb%a7%81-%eb%ac%b4%eb%a3%8c-scrcpy%eb%a1%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b2%8c-%eb%ac%b4%ec%84%a0/">맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity" />



<p>맥북을 사용하면서 안드로이드 폰(갤럭시) 화면을 맥에 띄워서 제어하고 싶을 때가 종종 있습니다. 유료 앱들도 있지만, 오픈소스인 <strong>scrcpy</strong>를 사용하면 무료임에도 불구하고 화질이 좋고 반응 속도도 매우 빠릅니다.</p>



<p>최근 커뮤니티 글을 참고하여 맥 터미널 환경에서 설치부터 무선 연결까지 직접 세팅해 보았습니다. 추후 재설정이 필요할 때를 대비하여 과정을 자세히 기록해 둡니다.</p>



<h2 class="wp-block-heading"><strong>1. 터미널 열고 설치하기 (Homebrew &amp; scrcpy)</strong></h2>



<figure class="wp-block-image aligncenter size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="861" height="470" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-111-edited.png?resize=861%2C470&#038;ssl=1" alt="image 111 edited" class="wp-image-2613" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 4" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-111-edited.png?w=861&amp;ssl=1 861w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-111-edited.png?resize=300%2C164&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-111-edited.png?resize=768%2C419&amp;ssl=1 768w" sizes="auto, (max-width: 861px) 100vw, 861px" /></figure>



<p>맥 사용자라면 <strong>Homebrew</strong>는 거의 필수적으로 사용하실 텐데요. 혹시 아직 설치되어 있지 않다면 터미널을 열고 아래 명령어를 복사하여 입력해 주세요. (이미 설치되어 있다면 이 단계는 건너뛰셔도 됩니다.)</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"</code></pre></div>



<p>그다음 바로 <strong>scrcpy</strong>를 설치합니다. 이 프로그램 하나만 설치하면 안드로이드 연결에 필요한 <code>adb</code> 툴까지 자동으로 의존성을 체크하여 함께 설치해 주므로 매우 편리합니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">brew install scrcpy</code></pre></div>



<p>제 맥북(Apple Silicon)에서 실제로 설치를 진행했을 때의 로그입니다. 맥주잔(🍺) 이모티콘이 뜨면 성공적으로 설치 되고 있는 것입니다.</p>



<p><strong>[실제 터미널 로그]</strong></p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">somsap@ihoseob-ui-MacBookAir ~ % brew install scrcpy
==&gt; Fetching downloads for: scrcpy
✔︎ Bottle Manifest scrcpy (3.3.4) ...
...
==&gt; Installing scrcpy
==&gt; Pouring scrcpy--3.3.4.arm64_sequoia.bottle.tar.gz
🍺  /opt/homebrew/Cellar/scrcpy/3.3.4: 11 files, 414.4KB</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><strong>2. 스마트폰 설정 및 유선 연결 (최초 1회)</strong></h2>



<figure class="wp-block-image aligncenter size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="910" height="497" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-112-edited.png?resize=910%2C497&#038;ssl=1" alt="image 112 edited" class="wp-image-2614" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 5" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-112-edited.png?w=910&amp;ssl=1 910w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-112-edited.png?resize=300%2C164&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-112-edited.png?resize=768%2C419&amp;ssl=1 768w" sizes="auto, (max-width: 910px) 100vw, 910px" /></figure>



<p>처음 한 번은 유선으로 연결하여 권한을 획득해야 합니다.</p>



<ol start="1" class="wp-block-list">
<li>스마트폰에서 <strong>설정 &gt; 휴대전화 정보 &gt; 소프트웨어 정보</strong>로 이동합니다.</li>



<li><strong>빌드 번호</strong>를 7번 연속 터치하여 개발자 모드를 활성화합니다.</li>



<li>다시 <strong>설정 &gt; 개발자 옵션</strong>에 진입하여 <strong>USB 디버깅</strong>을 켜줍니다.</li>



<li>맥북과 스마트폰을 케이블로 연결합니다.</li>
</ol>



<div class="wp-block-group is-content-justification-center is-nowrap is-layout-flex wp-container-core-group-is-layout-1 wp-block-group-is-layout-flex">
<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="504" height="1024" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?resize=504%2C1024&#038;ssl=1" alt="image 107" class="wp-image-2606" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 6" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?resize=504%2C1024&amp;ssl=1 504w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?resize=148%2C300&amp;ssl=1 148w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?resize=768%2C1560&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?resize=756%2C1536&amp;ssl=1 756w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?resize=1008%2C2048&amp;ssl=1 1008w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-107.png?w=1080&amp;ssl=1 1080w" sizes="auto, (max-width: 504px) 100vw, 504px" /></figure>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="504" height="1024" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?resize=504%2C1024&#038;ssl=1" alt="image 109" class="wp-image-2608" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 7" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?resize=504%2C1024&amp;ssl=1 504w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?resize=148%2C300&amp;ssl=1 148w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?resize=768%2C1560&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?resize=756%2C1536&amp;ssl=1 756w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?resize=1008%2C2048&amp;ssl=1 1008w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-109.png?w=1080&amp;ssl=1 1080w" sizes="auto, (max-width: 504px) 100vw, 504px" /></figure>
</div>



<p>맥북 화면에 스마트폰 연결하겠냐는 확인 창이 뜹니다. 확인을 누릅니다. 이제 터미널에 <code>scrcpy</code>라고 입력하면 스마트폰에 &#8220;이 컴퓨터를 허용하시겠습니까?&#8221;라는 팝업이 뜹니다. 이때 <strong>&#8216;항상 허용&#8217;</strong>은 편의상 체크합니다. 그리고 <strong>허용</strong>을 누르면 바로 맥북 화면에 스마트폰 화면이 나타납니다. 유선 연결 설정은 이것으로 완료됩니다.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading"><strong>3. 선 없는 자유, 무선 연결 방법</strong></h2>



<figure class="wp-block-image aligncenter size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="883" height="482" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-113-edited.png?resize=883%2C482&#038;ssl=1" alt="image 113 edited" class="wp-image-2616" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 8" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-113-edited.png?w=883&amp;ssl=1 883w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-113-edited.png?resize=300%2C164&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-113-edited.png?resize=768%2C419&amp;ssl=1 768w" sizes="auto, (max-width: 883px) 100vw, 883px" /></figure>



<p>유선도 좋지만 충전 중이거나 폰을 멀리 두고 사용할 때는 무선 연결이 훨씬 편리합니다. <strong>단, 맥북과 스마트폰이 반드시 동일한 와이파이 네트워크에 연결되어 있어야 합니다.</strong></p>



<h3 class="wp-block-heading"><strong>3-1. 포트 개방</strong></h3>



<p>아직 케이블이 연결된 상태에서 터미널에 아래 명령어를 입력합니다. 이는 5555번 포트를 개방해 주는 과정입니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">adb tcpip 5555</code></pre></div>



<h3 class="wp-block-heading"><strong>3-2. IP 주소로 접속</strong></h3>



<p>이제 케이블을 분리하셔도 됩니다. 스마트폰 설정의 와이파이 &#8211; 현재 네트워크 오른쪽의 톱니바퀴 버튼 터치해서 현재 연결된 IP 주소를 확인합니다. (더보기 터치해야 상세정보 뜸. 제 경우에는 <code>192.168.0.4</code>였습니다.)</p>



<figure class="wp-block-image aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="504" height="1024" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?resize=504%2C1024&#038;ssl=1" alt="image 108" class="wp-image-2607" style="object-fit:cover" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 9" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?resize=504%2C1024&amp;ssl=1 504w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?resize=148%2C300&amp;ssl=1 148w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?resize=768%2C1560&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?resize=756%2C1536&amp;ssl=1 756w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?resize=1008%2C2048&amp;ssl=1 1008w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-108.png?w=1080&amp;ssl=1 1080w" sizes="auto, (max-width: 504px) 100vw, 504px" /></figure>



<p>그다음 터미널에서 연결 명령어를 입력합니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">adb connect 192.168.0.4</code></pre></div>



<p><code>connected to 192.168.0.4:5555</code>라는 메시지가 뜨면 연결에 성공한 것입니다. 이제 실행해 보겠습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">scrcpy -s 192.168.0.4</code></pre></div>



<p><strong>[실제 무선 연결 성공 로그]</strong></p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">somsap@ihoseob-ui-MacBookAir ~ % adb connect 192.168.0.4
* daemon not running; starting now at tcp:5037
* daemon started successfully
connected to 192.168.0.4:5555
somsap@ihoseob-ui-MacBookAir ~ % scrcpy -s 192.168.0.4 
INFO: ADB device found:
INFO:     --&gt; (tcpip)  192.168.0.4:5555                device  SM_G991N
&#091;server] INFO: Device: &#091;samsung] samsung SM-G991N (Android 15)
INFO: Renderer: metal</code></pre></div>



<p>이제 선 없이도 아주 쾌적하게 미러링 기능을 사용하실 수 있습니다. 터미널 창을 닫으면 미러링도 종료되니 이 점 참고해 주시기 바랍니다.</p>



<p>만약, 연결 실패를 한다면 IP 숫자나 명령어 오탈자를 확인 부탁드립니다.</p>



<h2 class="wp-block-heading">실제 터미널 스크린샷 캡쳐</h2>



<h3 class="wp-block-heading">터미널 1</h3>



<figure class="wp-block-image aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="891" height="1024" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?resize=891%2C1024&#038;ssl=1" alt="image 103" class="wp-image-2602" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 10" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?resize=891%2C1024&amp;ssl=1 891w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?resize=261%2C300&amp;ssl=1 261w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?resize=768%2C882&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?resize=1337%2C1536&amp;ssl=1 1337w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?resize=1783%2C2048&amp;ssl=1 1783w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-103.png?w=1882&amp;ssl=1 1882w" sizes="auto, (max-width: 891px) 100vw, 891px" /></figure>



<figure class="wp-block-image aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="891" height="1024" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?resize=891%2C1024&#038;ssl=1" alt="image 104" class="wp-image-2603" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 11" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?resize=891%2C1024&amp;ssl=1 891w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?resize=261%2C300&amp;ssl=1 261w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?resize=768%2C882&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?resize=1337%2C1536&amp;ssl=1 1337w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?resize=1783%2C2048&amp;ssl=1 1783w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-104.png?w=1882&amp;ssl=1 1882w" sizes="auto, (max-width: 891px) 100vw, 891px" /></figure>



<h3 class="wp-block-heading">터미널 2</h3>



<figure class="wp-block-image aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="906" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-105.png?resize=1024%2C906&#038;ssl=1" alt="image 105" class="wp-image-2604" title="맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기) 12" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-105.png?resize=1024%2C906&amp;ssl=1 1024w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-105.png?resize=300%2C266&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-105.png?resize=768%2C680&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-105.png?resize=1536%2C1360&amp;ssl=1 1536w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2026/01/image-105.png?w=1882&amp;ssl=1 1882w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2026/01/23/%eb%a7%a5%eb%b6%81-%ea%b0%a4%eb%9f%ad%ec%8b%9c-%eb%af%b8%eb%9f%ac%eb%a7%81-%eb%ac%b4%eb%a3%8c-scrcpy%eb%a1%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b2%8c-%eb%ac%b4%ec%84%a0/">맥북 갤럭시 미러링, 무료 scrcpy로 깔끔하게 해결하기 (무선 연결 성공 후기)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2026/01/23/%eb%a7%a5%eb%b6%81-%ea%b0%a4%eb%9f%ad%ec%8b%9c-%eb%af%b8%eb%9f%ac%eb%a7%81-%eb%ac%b4%eb%a3%8c-scrcpy%eb%a1%9c-%ea%b9%94%eb%81%94%ed%95%98%ea%b2%8c-%eb%ac%b4%ec%84%a0/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2601</post-id>	</item>
		<item>
		<title>어제오늘날씨 &#8211; 어제와 오늘 날씨를 비교하는 앱 만들기</title>
		<link>https://somsap.somsap.com/2025/11/13/%ec%96%b4%ec%a0%9c%ec%98%a4%eb%8a%98%eb%82%a0%ec%94%a8-%ec%96%b4%ec%a0%9c%ec%99%80-%ec%98%a4%eb%8a%98-%eb%82%a0%ec%94%a8%eb%a5%bc-%eb%b9%84%ea%b5%90%ed%95%98%eb%8a%94-%ec%95%b1-%eb%a7%8c%eb%93%a4/</link>
					<comments>https://somsap.somsap.com/2025/11/13/%ec%96%b4%ec%a0%9c%ec%98%a4%eb%8a%98%eb%82%a0%ec%94%a8-%ec%96%b4%ec%a0%9c%ec%99%80-%ec%98%a4%eb%8a%98-%eb%82%a0%ec%94%a8%eb%a5%bc-%eb%b9%84%ea%b5%90%ed%95%98%eb%8a%94-%ec%95%b1-%eb%a7%8c%eb%93%a4/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Thu, 13 Nov 2025 12:52:41 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<category><![CDATA[Kotlin]]></category>
		<category><![CDATA[NodeJS]]></category>
		<category><![CDATA[날씨앱]]></category>
		<category><![CDATA[안드로이드]]></category>
		<category><![CDATA[토이프로젝트]]></category>
		<category><![CDATA[풀스택]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=2166</guid>

					<description><![CDATA[<p>🧥 아침마다 옷장 앞에서 망부석? AI 코디가 해결해 드립니다! &#8216;어제오늘&#8217; 앱 전격 출시! &#8220;아… 오늘 뭐 입지?&#8221; 매일 아침, 우리 모두가 마주하는 심오한 철학적 질문이죠. 어제 입었던 옷을 또 입자니 왠지 찝찝하고, 새 옷을 꺼내자니 날씨에 안 맞을까 봐 걱정되고… 그렇게 옷장 앞에서 10분, 20분… 소중한 아침 시간이 흘러갑니다. 😭 혹시 이런 생각 해보셨나요? &#8220;어제 [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/11/13/%ec%96%b4%ec%a0%9c%ec%98%a4%eb%8a%98%eb%82%a0%ec%94%a8-%ec%96%b4%ec%a0%9c%ec%99%80-%ec%98%a4%eb%8a%98-%eb%82%a0%ec%94%a8%eb%a5%bc-%eb%b9%84%ea%b5%90%ed%95%98%eb%8a%94-%ec%95%b1-%eb%a7%8c%eb%93%a4/">어제오늘날씨 &#8211; 어제와 오늘 날씨를 비교하는 앱 만들기</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading">🧥 아침마다 옷장 앞에서 망부석? AI 코디가 해결해 드립니다! &#8216;어제오늘&#8217; 앱 전격 출시!</h1>



<p>&#8220;아… 오늘 뭐 입지?&#8221;</p>



<p>매일 아침, 우리 모두가 마주하는 심오한 철학적 질문이죠. 어제 입었던 옷을 또 입자니 왠지 찝찝하고, 새 옷을 꺼내자니 날씨에 안 맞을까 봐 걱정되고… 그렇게 옷장 앞에서 10분, 20분… 소중한 아침 시간이 흘러갑니다. 😭</p>



<p>혹시 이런 생각 해보셨나요? <strong>&#8220;어제 날씨랑만 비교해도 옷 입기 훨씬 편할 텐데!&#8221;</strong></p>



<p>바로 그 생각, 저희가 현실로 만들었습니다!</p>



<h3 class="wp-block-heading">✨ 날씨 기반 AI 패션 코디네이터, &#8216;어제오늘&#8217; 앱을 소개합니다!</h3>



<p>&#8216;어제오늘&#8217;은 단순히 날씨만 알려주는 앱이 아닙니다. <strong>어제와 오늘의 기온을 직관적인 그래프로 비교</strong>해주고, <strong>가장 핫한 K-패션 트렌드</strong>를 반영한 오늘의 착장을 AI가 직접 추천해주는, 당신의 손안에 생긴 전문 스타일리스트입니다.</p>



<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/via.placeholder.com/800x450.png?ssl=1" alt="앱 스크린샷" title="어제오늘날씨 - 어제와 오늘 날씨를 비교하는 앱 만들기 14"><br><em>(앱의 멋진 스크린샷을 여기에 넣어보세요!)</em></p>



<h3 class="wp-block-heading">&#8216;어제오늘&#8217; 앱의 핵심 기능 3가지!</h3>



<h4 class="wp-block-heading">1. 한눈에 끝! 어제 vs 오늘 기온 배틀 📈</h4>



<p>더 이상 숫자를 보고 머리 아파하지 마세요. &#8216;어제오늘&#8217; 앱은 어제와 오늘의 시간대별 기온 변화를 <strong>하나의 그래프</strong>에 깔끔하게 보여줍니다.</p>



<ul class="wp-block-list">
<li>&#8220;어? 오늘 아침은 어제보다 쌀쌀하네? 가디건 챙겨야지!&#8221;</li>



<li>&#8220;오후엔 어제보다 훨씬 덥구나! 외투는 차에 두고 가도 되겠다.&#8221;</li>
</ul>



<p>그래프만 쓱- 봐도 오늘 하루 기온 변화를 완벽하게 예측하고 대비할 수 있습니다.</p>



<h4 class="wp-block-heading">2. 내 손안의 AI 스타일리스트 🤖</h4>



<p>이 앱의 진짜 주인공! 바로 <strong>AI 패션 코디네이터</strong>입니다. 현재 날씨와 기온 변화에 맞춰, 가장 트렌디한 스타일을 추천해줘요.</p>



<ul class="wp-block-list">
<li><strong>힙스터를 위한 &#8216;캐주얼룩&#8217;</strong>: 요즘 서울에서 가장 핫한 와이드 팬츠, 오버핏 셔츠 스타일링을 만나보세요.</li>



<li><strong>직장인을 위한 &#8216;비즈니스룩&#8217;</strong>: 꾸민 듯 안 꾸민 듯, 단정하면서도 세련된 출근룩을 제안합니다.</li>



<li><strong>성별에 따른 맞춤 추천</strong>: 남성/여성별로 각각 다른 스타일을 제공하는 건 기본이죠!</li>
</ul>



<p>이제 &#8220;패알못&#8221;도 매일 아침 인스타그램 패셔니스타가 될 수 있습니다.</p>



<h4 class="wp-block-heading">3. 전 세계 어디서든, 설정은 내 마음대로 🌍</h4>



<p>여행을 가셨나요? 출장 중이신가요? &#8216;어제오늘&#8217;은 <strong>전 세계 모든 도시의 날씨</strong>를 지원합니다. GPS로 현재 위치를 자동으로 잡거나, 원하는 도시를 직접 검색할 수도 있어요.</p>



<p>물론, 앱 내 설정에서 언어를 직접 바꾸는 것도 가능합니다. 내게 가장 편한 방식으로 앱을 사용해 보세요!</p>



<h3 class="wp-block-heading">이런 분들께 강력 추천합니다!</h3>



<p>✅ 매일 아침 &#8216;입을 옷이 없다&#8217;고 느끼는 분<br>✅ 패션에 관심은 많지만, 어떻게 입어야 할지 막막한 분<br>✅ 출근룩, 등교룩 고민으로 10분을 허비하는 직장인과 학생<br>✅ 어제와 오늘의 미세한 기온 차이까지 챙기는 섬세한 날씨 요정🧚‍♀️</p>



<h3 class="wp-block-heading">이제 고민은 그만!</h3>



<p>더 이상 날씨 앱과 패션 앱을 따로 보지 마세요. &#8216;어제오늘&#8217; 앱 하나면 충분합니다.</p>



<p>지금 바로 다운로드하고, 매일 아침 10분의 여유와 스타일을 모두 챙기세요!</p>



<p>👇 <strong>지금 바로 다운로드하기</strong> 👇</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="https://play.google.com/apps/testing/com.somsap.ydaytday" target="_blank" rel="noreferrer noopener">[Google Play Store 링크]</a></div>
</div>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p><strong>기술 스택:</strong>&nbsp;Kotlin, Node.js, Express, OpenWeatherMap API, Room Database, Retrofit, MPAndroidChart, SQLite, Coroutines</p>



<p></p>



<h1 class="wp-block-heading" id="Privacy-Policy">개인정보처리방침 (Privacy Policy)</h1>



<p><strong>최종 수정일: 2025년 11월 27일</strong></p>



<p>본 개인정보처리방침은 Yesterday-Today(이하 &#8216;앱&#8217;)가 사용자의 개인정보를 수집, 이용, 공유하는 방식에 대해 설명합니다. 앱을 사용함으로써 귀하는 본 방침에 동의하는 것으로 간주됩니다.</p>



<h3 class="wp-block-heading">1. 수집하는 정보</h3>



<p>저희는 앱의 핵심 기능을 제공하기 위해 다음과 같은 최소한의 정보를 수집합니다.</p>



<ul class="wp-block-list">
<li><strong>위치 정보 (위도 및 경도):</strong>
<ul class="wp-block-list">
<li><strong>수집 목적:</strong> 현재 위치의 날씨 정보와 그에 맞는 의상 추천을 제공하기 위함입니다.</li>



<li><strong>수집 방식:</strong>
<ol class="wp-block-list">
<li><strong>GPS 기반 현재 위치:</strong> 사용자가 명시적으로 위치 권한(ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATION)을 허용한 경우에만 기기의 GPS를 통해 현재 위치 좌표를 수집합니다.</li>



<li><strong>수동 검색 위치:</strong> 사용자가 앱 내에서 직접 도시 이름을 검색할 경우, 해당 지역의 위치 정보를 수집합니다.</li>
</ol>
</li>
</ul>
</li>



<li><strong>언어 및 지역 설정:</strong>
<ul class="wp-block-list">
<li><strong>수집 목적:</strong> 앱의 UI를 사용자 기기의 언어로 표시하고, 지역명을 현지 언어로 보여주기 위함입니다.</li>



<li><strong>수집 방식:</strong> 사용자 기기의 기본 언어 및 지역 설정을 확인하거나, 앱 내 설정에서 사용자가 직접 선택한 언어 정보를 수집합니다.</li>
</ul>
</li>



<li><strong>광고 식별자:</strong>
<ul class="wp-block-list">
<li><strong>수집 목적:</strong> 사용자에게 맞춤형 광고를 제공하기 위함입니다.</li>



<li><strong>수집 방식:</strong> Google AdMob SDK를 통해 광고 식별자(ADID)를 수집할 수 있습니다. 이는 기기 설정에서 재설정하거나 비활성화할 수 있습니다.</li>
</ul>
</li>



<li><strong>서비스 이용 기록:</strong>
<ul class="wp-block-list">
<li><strong>수집 목적:</strong> 서비스 안정성 확보, 오류 해결 및 성능 개선을 위함입니다.</li>



<li><strong>수집 방식:</strong> 앱과 서버 간의 통신 과정에서 IP 주소, User-Agent와 같은 기술적 정보가 서버 로그에 자동으로 기록될 수 있습니다.</li>
</ul>
</li>
</ul>



<h3 class="wp-block-heading">2. 정보의 이용</h3>



<p>수집된 정보는 오직 다음과 같은 목적으로만 사용됩니다.</p>



<ul class="wp-block-list">
<li><strong>핵심 기능 제공:</strong> 사용자의 위치에 기반한 날씨 정보 및 의상 이미지 추천</li>



<li><strong>서비스 개선:</strong> 오류 분석 및 사용자 경험 향상을 위한 통계 분석</li>



<li><strong>광고 제공:</strong> Google AdMob을 통한 맞춤형 광고 표시</li>
</ul>



<h3 class="wp-block-heading">3. 정보의 제3자 제공 및 공유</h3>



<p>저희는 사용자의 개인정보를 판매하거나 임의로 제3자에게 제공하지 않습니다. 다만, 원활한 서비스 제공을 위해 다음과 같이 신뢰할 수 있는 외부 서비스와 정보를 공유할 수 있습니다.</p>



<ul class="wp-block-list">
<li><strong>Open-Meteo &amp; OpenWeatherMap:</strong> 날씨 정보 조회 및 위치 검색(지오코딩/역-지오코딩)을 위해 사용자의 위치 좌표와 언어 설정을 전송합니다.</li>



<li><strong>Google (Vertex AI &amp; Places API):</strong> 의상 이미지 생성 및 도시 이름 검색 시 오타 수정을 위해 서버에서 해당 API를 호출합니다. 이때 사용자의 개인 식별 정보는 전송되지 않습니다.</li>



<li><strong>Google AdMob:</strong> 맞춤형 광고를 표시하기 위해 광고 식별자를 공유합니다.</li>
</ul>



<p>각 서비스의 개인정보처리방침은 아래 링크에서 확인하실 수 있습니다.</p>



<ul class="wp-block-list">
<li><a href="https://policies.google.com/privacy" target="_blank" rel="noopener">Google Privacy Policy</a></li>



<li><a href="https://open-meteo.com/en/privacy-policy" target="_blank" rel="noopener">Open-Meteo Privacy Policy</a></li>



<li><a href="https://openweather.co.uk/privacy-policy" target="_blank" rel="noopener">OpenWeatherMap Privacy Policy</a></li>
</ul>



<h3 class="wp-block-heading">4. 데이터 보관 및 보안</h3>



<ul class="wp-block-list">
<li><strong>클라이언트 (앱):</strong> 빠른 서비스 제공을 위해, 날씨 및 의상 정보는 최대 6시간 동안 사용자 기기의 내부 저장소(<code>SharedPreferences</code>)에 캐시(저장)됩니다. 이 데이터는 앱을 삭제하면 함께 제거됩니다.</li>



<li><strong>서버:</strong> 서비스 성능 향상을 위해 날씨 및 의상 이미지 데이터를 서버 데이터베이스에 캐시합니다. 이 데이터는 개인을 식별할 수 없는 형태로 저장되며, 주기적으로 삭제됩니다.</li>



<li>저희는 수집된 정보를 보호하기 위해 합리적인 기술적, 관리적 조치를 취하고 있습니다.</li>
</ul>



<h3 class="wp-block-heading">5. 사용자의 권리 및 선택</h3>



<ul class="wp-block-list">
<li><strong>위치 정보:</strong> 언제든지 기기 설정에서 앱의 위치 정보 접근 권한을 비활성화할 수 있습니다. 이 경우, 현재 위치 기반 서비스는 제한되며 수동으로 지역을 검색해야 합니다.</li>



<li><strong>언어 설정:</strong> 앱 내 설정 메뉴를 통해 언제든지 표시 언어를 변경할 수 있습니다.</li>



<li><strong>맞춤형 광고:</strong> 기기의 광고 설정에서 광고 ID를 재설정하거나 맞춤형 광고 수신을 거부할 수 있습니다.</li>
</ul>



<h3 class="wp-block-heading">6. 아동의 개인정보 보호</h3>



<p>저희 앱은 <strong>만 13세 미만의 어린이</strong>를 대상으로 하지 않으며, 의도적으로 해당 연령의 사용자로부터 개인정보를 수집하지 않습니다. 만약 만 13세 미만 아동의 개인정보가 수집된 사실을 인지하게 될 경우, 즉시 해당 정보를 삭제할 것입니다.</p>



<h3 class="wp-block-heading">7. 개인정보처리방침의 변경</h3>



<p>본 방침은 법령이나 서비스의 변경사항을 반영하기 위해 개정될 수 있습니다. 방침이 변경될 경우, 스토어 등록정보 페이지를 통해 공지하겠습니다.</p>



<h3 class="wp-block-heading">8. 문의처</h3>



<p>본 개인정보처리방침에 대해 궁금한 점이 있으시면 아래 이메일로 문의해 주시기 바랍니다.</p>



<ul class="wp-block-list">
<li><strong>이메일:</strong> rome777@gmail.com</li>
</ul>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/11/13/%ec%96%b4%ec%a0%9c%ec%98%a4%eb%8a%98%eb%82%a0%ec%94%a8-%ec%96%b4%ec%a0%9c%ec%99%80-%ec%98%a4%eb%8a%98-%eb%82%a0%ec%94%a8%eb%a5%bc-%eb%b9%84%ea%b5%90%ed%95%98%eb%8a%94-%ec%95%b1-%eb%a7%8c%eb%93%a4/">어제오늘날씨 &#8211; 어제와 오늘 날씨를 비교하는 앱 만들기</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/11/13/%ec%96%b4%ec%a0%9c%ec%98%a4%eb%8a%98%eb%82%a0%ec%94%a8-%ec%96%b4%ec%a0%9c%ec%99%80-%ec%98%a4%eb%8a%98-%eb%82%a0%ec%94%a8%eb%a5%bc-%eb%b9%84%ea%b5%90%ed%95%98%eb%8a%94-%ec%95%b1-%eb%a7%8c%eb%93%a4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2166</post-id>	</item>
		<item>
		<title>[Shell Script] 다중 원격 서버 로그 분석 자동화 스크립트</title>
		<link>https://somsap.somsap.com/2025/10/21/shell-script-%eb%8b%a4%ec%a4%91-%ec%9b%90%ea%b2%a9-%ec%84%9c%eb%b2%84-%eb%a1%9c%ea%b7%b8-%eb%b6%84%ec%84%9d-%ec%9e%90%eb%8f%99%ed%99%94-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/</link>
					<comments>https://somsap.somsap.com/2025/10/21/shell-script-%eb%8b%a4%ec%a4%91-%ec%9b%90%ea%b2%a9-%ec%84%9c%eb%b2%84-%eb%a1%9c%ea%b7%b8-%eb%b6%84%ec%84%9d-%ec%9e%90%eb%8f%99%ed%99%94-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Tue, 21 Oct 2025 06:22:48 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<category><![CDATA[awk]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[BPEL]]></category>
		<category><![CDATA[grep]]></category>
		<category><![CDATA[로그 분석]]></category>
		<category><![CDATA[리눅스]]></category>
		<category><![CDATA[서버 관리]]></category>
		<category><![CDATA[서버 자동화]]></category>
		<category><![CDATA[성능 측정]]></category>
		<category><![CDATA[셸 스크립트]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=2035</guid>

					<description><![CDATA[<p>안녕하세요. 오늘은 여러 대의 애플리케이션 서버(AP)에 분산되어 쌓이는 로그를 매번 접속하지 않고, 한 번의 명령으로 취합하여 분석할 수 있는 유용한 셸 스크립트(analLog.sh)를 공유합니다. 이 스크립트는 특히 다수의 서버에서 특정 BPEL 프로세스의 완료 로그를 찾아 실행 시간 통계(최소, 최대, 평균 시간)를 빠르게 확인해야 할 때 매우 유용합니다. 스크립트의 주요 기능 analLog.sh 스크립트는 4대의 지정된 원격 서버(AP1~AP4)에 [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/10/21/shell-script-%eb%8b%a4%ec%a4%91-%ec%9b%90%ea%b2%a9-%ec%84%9c%eb%b2%84-%eb%a1%9c%ea%b7%b8-%eb%b6%84%ec%84%9d-%ec%9e%90%eb%8f%99%ed%99%94-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/">[Shell Script] 다중 원격 서버 로그 분석 자동화 스크립트</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<p>안녕하세요. 오늘은 여러 대의 애플리케이션 서버(AP)에 분산되어 쌓이는 로그를 매번 접속하지 않고, 한 번의 명령으로 취합하여 분석할 수 있는 유용한 셸 스크립트(<code>analLog.sh</code>)를 공유합니다.</p>



<p>이 스크립트는 특히 다수의 서버에서 특정 BPEL 프로세스의 완료 로그를 찾아 실행 시간 통계(최소, 최대, 평균 시간)를 빠르게 확인해야 할 때 매우 유용합니다.</p>



<h3 class="wp-block-heading">스크립트의 주요 기능</h3>



<p><code>analLog.sh</code> 스크립트는 4대의 지정된 원격 서버(AP1~AP4)에 <code>ssh</code>로 자동 접속하여 다음 작업을 수행합니다.</p>



<ol start="1" class="wp-block-list">
<li><strong>로그 필터링</strong>: 사용자가 지정한 디렉터리(<code>-f</code>)에서 특정 메시지명(<code>-m</code>)을 포함하는 <code>BPEL COMPLETED</code> 로그를 검색합니다.</li>



<li><strong>시간대 지정</strong>: 지정된 날짜(<code>-d</code>)와 시간(<code>-t</code>) 패턴에 일치하는 로그만 추출합니다.</li>



<li><strong>성능 데이터 추출</strong>: 로그에 기록된 <code>Elapsed time = [숫자] miliseconds</code> 패턴을 찾아 실행 시간(숫자) 값만 파싱합니다.</li>



<li><strong>통계 집계</strong>: <code>awk</code>를 사용하여 각 서버별 및 전체 서버의 **총 로그 건수(Count), 최소 시간(Min), 최대 시간(Max), 평균 시간(Avg)**을 밀리초(ms) 단위로 계산합니다.</li>



<li><strong>(선택) 분당/초당 처리량</strong>: <code>-l [분]</code> 옵션을 제공하면, 지정한 시간(분)을 기준으로 분당 및 초당 평균 처리 건수를 함께 계산해 줍니다.</li>
</ol>



<h3 class="wp-block-heading">사용 방법 (Usage)</h3>



<p>스크립트는 <code>getopts</code>를 사용하여 표준 옵션 인자를 받습니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">Usage: analLog.sh -m &#091;MESSAGE] -d &#091;DATE] -t &#091;TIME] -f &#091;DIRECTORY]
Try 'analLog.sh -help' for more information.
</code></pre></div>



<p><strong>옵션 상세:</strong></p>



<ul class="wp-block-list">
<li><code>-m</code>: 검색할 메시지 이름 (예: <code>AlarmReport.bpel</code>)</li>



<li><code>-d</code>: 검색할 날짜 (예: <code>20/07/02</code>)</li>



<li><code>-t</code>: 검색할 시간 (예: <code>01:17:</code> 또는 <code>01:[0-9][0-9]:</code>)</li>



<li><code>-f</code>: 검색할 원격 서버의 로그 디렉터리 경로 (예: <code>/mesap_log/EEXsvr/20200702</code>)</li>



<li><code>-l</code> (선택 사항): 분석 대상 시간(분). (예: <code>60</code>)</li>
</ul>



<p><strong>실행 예시:</strong></p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">./analLog.sh -m AlarmReport.bpel -d 25/10/21 -t 08:15: -f /mesap_log/EEXsvr/20251021
</code></pre></div>



<h3 class="wp-block-heading">전체 스크립트 코드</h3>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">#!/bin/bash

### Created by Ho Seop Lee of Aim System.

### Initialization

usageMsg="Usage: analLog.sh -m &#091;MESSAGE] -d &#091;DATE] -t &#091;TIME] -f &#091;DIRECTORY]
Try 'analLog.sh -help' for more information."

helpMsg='
\t-m\t\tMessage Name\t(ex: -m AlarmReport.bpel)
\t-d\t\tDate\t\t(ex: -d 20/07/02)
\t-t\t\tTime\t\t(ex: -t 01:17: or -t 01:&#091;0-9]&#091;0-9]:)
\t-f\t\tDirectory Path\t(ex: -f /mesap_log/EEXsvr/ or -f /mesap_log/EEXsvr/20200702)
\t-l (Optional)\tMinutes should be analyzed\t(ex: -l 60)
\t-h, -help, -?\tShow Help Info
'

cols=`tput cols`

for((i=0; i&lt;$cols; i++));
do
    line=$line'=';
done

function printLine()
{
    echo $line
}

usage()
{
    echo -e "$usageMsg"
    printLine
    exit 0
}

help()
{
    echo -e "$helpMsg"
    printLine
    exit 0
}

printLine

### Declaration Option Argument

if &#091;&#091; ! $@ =~ ^\-.+ ]]
then
    usage;
fi

while getopts m:d:t:f:l:h? OPT
do
    case $OPT in
        m) msg=$OPTARG;;
        d) date=$OPTARG;;
        t) time=$OPTARG;;
        f) dir=$OPTARG;; # &#091;FIXED] 원본의 'd)'를 'f)'로 수정
        l) howMinute=$OPTARG;;
        h) help;;
        ?) help;;
    esac
done

echo "Arg msg: $msg"
echo "Arg date: $date"
echo "Arg time: $time"
echo "Arg directory: $dir"
echo "Arg howMinute: $howMinute"

### Analyze log from each AP by remote ssh

# &#091;FIXED] AP1의 grep에 '-s' 옵션을 추가하여 다른 AP와 일관성 유지
# echo 'AP1 p7mesfab@172.25.4.17';
ap_summary=`ssh p7mesfab@172.25.4.17 grep "-sE '***** BPEL COMPLETED :: "${msg}"' "${dir}/*"" | grep -P "${date} ${time}" | grep -Po '(?&lt;=Elapsed time = )\w+(?= miliseconds)' | awk 'BEGIN {min=999999;max=1} {if($1&gt;max) {max=$1}; if($1&lt;min) {min=$1}; total+=$1; count+=1} END {if(count&gt;0) {print "AP1 count: "count" min: "min" max: "max" avg: "total/count}}'`

# echo 'AP1 p7mesfab@172.25.4.18';
ap_summary=$ap_summary"\n"`ssh p7mesfab@172.25.4.18 grep "-sE '***** BPEL COMPLETED :: "${msg}"' "${dir}/*"" | grep -P "${date} ${time}" | grep -Po '(?&lt;=Elapsed time = )\w+(?= miliseconds)' | awk 'BEGIN {min=999999;max=1} {if($1&gt;max) {max=$1}; if($1&lt;min) {min=$1}; total+=$1; count+=1} END {if(count&gt;0) {print "AP2 count: "count" min: "min" max: "max" avg: "total/count}}'`

# echo 'AP1 p7mesfab@172.25.4.19';
ap_summary=$ap_summary"\n"`ssh p7mesfab@172.25.4.19 grep "-sE '***** BPEL COMPLETED :: "${msg}"' "${dir}/*"" | grep -P "${date} ${time}" | grep -Po '(?&lt;=Elapsed time = )\w+(?= miliseconds)' | awk 'BEGIN {min=999999;max=1} {if($1&gt;max) {max=$1}; if($1&lt;min) {min=$1}; total+=$1; count+=1} END {if(count&gt;0) {print "AP3 count: "count" min: "min" max: "max" avg: "total/count}}'`

# echo 'AP1 p7mesfab@172.25.4.34';
ap_summary=$ap_summary"\n"`ssh p7mesfab@172.25.4.34 grep "-sE '***** BPEL COMPLETED :: "${msg}"' "${dir}/*"" | grep -P "${date} ${time}" | grep -Po '(?&lt;=Elapsed time = )\w+(?= miliseconds)' | awk 'BEGIN {min=999999;max=1} {if($1&gt;max) {max=$1}; if($1&lt;min) {min=$1}; total+=$1; count+=1} END {if(count&gt;0) {print "AP4 count: "count" min: "min" max: "max" avg: "total/count}}'`

ap_summar=`echo -e $ap_summary | grep "AP"`

### Show Analysis

echo
echo -e "$ap_summar" # &#091;FIXED] 줄바꿈이 적용된 '$ap_summar' 변수로 수정
echo
if &#091; -z $howMinute ]; then
    # &#091;FIXED] "Total Count: " 중복 오타 수정
    echo -e "$ap_summar" | awk 'BEGIN {min=99999;max=0} length($0) &gt; 0 {total+=$3; if($5&lt;min) {min=$5}; if($7&gt;max) {max=$7}; count+=1; avg_sum+=$9} END {print "Total Count: "total", Min(ms): "min", Max(ms): "max", Avg(ms): "avg_sum/count}'
elif &#091; $howMinute -gt 0 ]; then
    echo -e "$ap_summar" | awk -v howMinute=$howMinute 'BEGIN {min=99999;max=0} length($0) &gt; 0 {total+=$3; if($5&lt;min) {min=$5}; if($7&gt;max) {max=$7}; count+=1; avg_sum+=$9} END {print "Total Count: "total", Count/m: "total/howMinute", Count/s: "total/howMinute/60", Min(ms): "min", Max(ms): "max", Avg(ms): "avg_sum/count", Avg(s): "avg_sum/count/1000}'
fi
printLine
exit 0
</code></pre></div>



<h3 class="wp-block-heading">사용 방법 예시</h3>



<figure class="wp-block-image size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="615" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/10/image-2.png?resize=1024%2C615&#038;ssl=1" alt="image 2" class="wp-image-2041" title="[Shell Script] 다중 원격 서버 로그 분석 자동화 스크립트 15" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/10/image-2.png?resize=1024%2C615&amp;ssl=1 1024w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/10/image-2.png?resize=300%2C180&amp;ssl=1 300w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/10/image-2.png?resize=768%2C461&amp;ssl=1 768w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/10/image-2.png?resize=1536%2C923&amp;ssl=1 1536w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/10/image-2.png?w=1641&amp;ssl=1 1641w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></figure>



<p>시간 Option에 정규식 입력 시, Back Slash를 사용하려면 Back Slash를 두 번 써야 하는 불편함이 있습니다.</p>



<h3 class="wp-block-heading">마무리</h3>



<p>이 스크립트를 활용하면 여러 서버의 로그를 확인하기 위해 반복적으로 접속하고 <code>grep</code> 명령을 실행하는 수고를 덜 수 있습니다. IP 주소나 <code>grep</code> 패턴을 환경에 맞게 수정하여 다양하게 활용해 보시기 바랍니다.</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/10/21/shell-script-%eb%8b%a4%ec%a4%91-%ec%9b%90%ea%b2%a9-%ec%84%9c%eb%b2%84-%eb%a1%9c%ea%b7%b8-%eb%b6%84%ec%84%9d-%ec%9e%90%eb%8f%99%ed%99%94-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/">[Shell Script] 다중 원격 서버 로그 분석 자동화 스크립트</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/10/21/shell-script-%eb%8b%a4%ec%a4%91-%ec%9b%90%ea%b2%a9-%ec%84%9c%eb%b2%84-%eb%a1%9c%ea%b7%b8-%eb%b6%84%ec%84%9d-%ec%9e%90%eb%8f%99%ed%99%94-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2035</post-id>	</item>
		<item>
		<title>[가이드] IntelliJ/Android Studio에서 Amazon Q Chat 비활성화 문제 해결법</title>
		<link>https://somsap.somsap.com/2025/10/17/%ea%b0%80%ec%9d%b4%eb%93%9c-android-studio-intellij%ec%97%90%ec%84%9c-amazon-q-chat-%eb%b9%84%ed%99%9c%ec%84%b1%ed%99%94-%eb%ac%b8%ec%a0%9c-%ed%95%b4%ea%b2%b0%eb%b2%95/</link>
					<comments>https://somsap.somsap.com/2025/10/17/%ea%b0%80%ec%9d%b4%eb%93%9c-android-studio-intellij%ec%97%90%ec%84%9c-amazon-q-chat-%eb%b9%84%ed%99%9c%ec%84%b1%ed%99%94-%eb%ac%b8%ec%a0%9c-%ed%95%b4%ea%b2%b0%eb%b2%95/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Fri, 17 Oct 2025 05:27:53 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=1985</guid>

					<description><![CDATA[<p>서문 IntelliJ IDEA 또는 Android Studio에 Amazon Q를 설치했지만, 정작 채팅 기능이 회색으로 비활성화되어 클릭조차 할 수 없는 문제를 겪고 계신가요? 이 현상은 UI가 로딩되지 않는 &#8216;흰 화면&#8217; 문제와는 근본적으로 다릅니다. 이는 IDE가 채팅 기능을 활성화할 구성(Configuration) 조건을 충족하지 못했다고 판단한 상태입니다. 원인은 크게 두 가지로, 플러그인 간의 충돌 또는 IDE의 내장 UI 렌더링 엔진(JCEF) [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/10/17/%ea%b0%80%ec%9d%b4%eb%93%9c-android-studio-intellij%ec%97%90%ec%84%9c-amazon-q-chat-%eb%b9%84%ed%99%9c%ec%84%b1%ed%99%94-%eb%ac%b8%ec%a0%9c-%ed%95%b4%ea%b2%b0%eb%b2%95/">[가이드] IntelliJ/Android Studio에서 Amazon Q Chat 비활성화 문제 해결법</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h3 class="wp-block-heading">서문</h3>



<p>IntelliJ IDEA 또는 Android Studio에 Amazon Q를 설치했지만, 정작 채팅 기능이 회색으로 비활성화되어 클릭조차 할 수 없는 문제를 겪고 계신가요? 이 현상은 UI가 로딩되지 않는 &#8216;흰 화면&#8217; 문제와는 근본적으로 다릅니다. 이는 IDE가 채팅 기능을 활성화할 <strong>구성(Configuration) 조건</strong>을 충족하지 못했다고 판단한 상태입니다.</p>



<p>원인은 크게 두 가지로, <strong>플러그인 간의 충돌</strong> 또는 <strong>IDE의 내장 UI 렌더링 엔진(JCEF) 설정 오류</strong>일 가능성이 매우 높습니다. 이 글은 두 가지 원인을 모두 해결하여 비활성화된 Amazon Q Chat을 활성화하는 가장 확실한 방법을 안내합니다.</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h3 class="wp-block-heading">## 해결책 1: 플러그인 충돌 해결 (AWS Toolkit 비활성화)</h3>



<p>가장 먼저 시도해야 할 조치입니다. Amazon Q는 <code>AWS Core</code>와 <code>Amazon Q</code> 두 플러그인을 기반으로 동작합니다. 이때 구버전 <code>AWS Toolkit</code> 플러그인이 함께 활성화되어 있으면, 의존성이 꼬이면서 내부적으로 충돌을 일으키고 Amazon Q의 핵심 기능(채팅 등)을 비활성화시키는 주된 원인이 될 수 있습니다.</p>



<p><strong>Amazon Q Chat 사용이 주 목적이라면, <code>AWS Toolkit</code>은 비활성화하는 것이 안정적입니다.</strong></p>



<ol start="1" class="wp-block-list">
<li>IntelliJ IDEA 또는 Android Studio 메뉴에서 <strong><code>File</code> &gt; <code>Settings</code></strong> (macOS의 경우 <strong><code>IntelliJ IDEA/Android Studio</code> &gt; <code>Settings</code></strong>)로 이동합니다.</li>



<li><strong><code>Plugins</code></strong> 섹션의 <strong><code>Installed</code></strong> 탭을 클릭합니다.</li>



<li>아래와 같이 플러그인 상태가 올바르게 설정되었는지 확인하고 수정합니다.
<ul class="wp-block-list">
<li>✅ <strong><code>Amazon Q</code></strong>: <strong>활성화 (체크 켜기)</strong></li>



<li>✅ <strong><code>AWS Core</code></strong>: <strong>활성화 (체크 켜기)</strong></li>



<li>❌ <strong><code>AWS Toolkit</code></strong>: <strong>비활성화 (체크 끄기)</strong></li>
</ul>
</li>



<li><strong><code>Apply</code></strong> 버튼을 누른 후, IDE를 재시작하라는 메시지가 나타나면 **<code>Restart IDE</code>**를 클릭하여 IDE를 완전히 재시작합니다.</li>
</ol>



<p>[이미지: IntelliJ/Android Studio 플러그인 설정 화면에서 AWS Toolkit은 체크 해제하고, Amazon Q와 AWS Core는 체크한 상태를 명확히 보여주는 스크린샷]</p>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h3 class="wp-block-heading">## 해결책 2: JetBrains 부트 런타임(JBR) 변경 (JCEF 포함)</h3>



<p>플러그인 충돌을 해결한 후에도 문제가 지속된다면, 원인은 IDE가 채팅 UI를 그리는 방식에 있습니다. Amazon Q 채팅창은 내장된 웹 브라우저(JCEF)를 사용하는데, 현재 IDE의 부트 런타임에 이 기능이 없거나 호환되지 않는 경우입니다. JCEF가 포함된 런타임으로 직접 변경하여 이 문제를 해결할 수 있습니다.</p>



<ol start="1" class="wp-block-list">
<li><strong>&#8216;Choose Boot Java Runtime&#8217; 메뉴 실행</strong>:
<ul class="wp-block-list">
<li>키보드에서 <strong><code>Ctrl + Shift + A</code></strong> (macOS: <strong><code>Cmd + Shift + A</code></strong>)를 눌러 &#8216;Find Action&#8217; 검색창을 엽니다.</li>



<li>검색창에 <strong><code>Choose Boot Java Runtime for the IDE</code></strong> 를 입력하고 나타나는 항목을 선택하여 실행합니다.</li>
</ul>
</li>



<li><strong>&#8216;with JCEF&#8217; 포함 런타임 선택</strong>:
<ul class="wp-block-list">
<li><code>Choose Boot Java Runtime</code> 팝업 창이 나타나면, 현재 IDE에 설정된 런타임 정보가 표시됩니다.</li>



<li>드롭다운 메뉴를 열어 사용 가능한 런타임 목록을 확인합니다.</li>



<li>목록 중에서 이름에 <strong><code>with JCEF</code></strong> 라는 문구가 명확하게 포함된 버전의 런타임을 찾아 선택합니다. (예시: <code>jbr-17.0.10-windows-x64-b1207.14-with-jcef</code>)</li>
</ul>
</li>
</ol>



<p>[이미지: JCEF가 포함된 JBR을 선택하는 팝업 창 스크린샷. &#8216;with JCEF&#8217; 문구가 강조 표시됨]</p>



<ol start="3" class="wp-block-list">
<li><strong>저장 및 IDE 재시작</strong>:
<ul class="wp-block-list">
<li>JCEF 포함 런타임을 선택한 후, 팝업 창 하단의 <strong><code>Save and Restart</code></strong> 버튼을 클릭합니다.</li>



<li>IDE가 새로운 런타임으로 완전히 재시작됩니다.</li>
</ul>
</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h3 class="wp-block-heading">마무리</h3>



<p>Amazon Q Chat의 <strong>&#8216;비활성화&#8217;</strong> 문제는 복합적인 원인으로 발생할 수 있지만, 대부분의 경우 위에서 제시한 두 가지 방법으로 해결됩니다.</p>



<p>먼저 <strong><code>AWS Toolkit</code>을 비활성화</strong>하여 잠재적인 플러그인 충돌을 제거하고, 그 다음 IDE의 <strong>부트 런타임을 JCEF 포함 버전으로 설정</strong>하여 UI 렌더링에 필요한 환경을 보장하는 것이 가장 확실한 해결 순서입니다. 이 두 단계를 통해 비활성화되었던 Amazon Q Chat을 정상적으로 활성화할 수 있을 것입니다.</p>



<p></p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/10/17/%ea%b0%80%ec%9d%b4%eb%93%9c-android-studio-intellij%ec%97%90%ec%84%9c-amazon-q-chat-%eb%b9%84%ed%99%9c%ec%84%b1%ed%99%94-%eb%ac%b8%ec%a0%9c-%ed%95%b4%ea%b2%b0%eb%b2%95/">[가이드] IntelliJ/Android Studio에서 Amazon Q Chat 비활성화 문제 해결법</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/10/17/%ea%b0%80%ec%9d%b4%eb%93%9c-android-studio-intellij%ec%97%90%ec%84%9c-amazon-q-chat-%eb%b9%84%ed%99%9c%ec%84%b1%ed%99%94-%eb%ac%b8%ec%a0%9c-%ed%95%b4%ea%b2%b0%eb%b2%95/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1985</post-id>	</item>
		<item>
		<title>PyQt5 Excel → Markdown 변환기 (GUI)</title>
		<link>https://somsap.somsap.com/2025/09/04/pyqt5-excel-%e2%86%92-markdown-%eb%b3%80%ed%99%98%ea%b8%b0-gui/</link>
					<comments>https://somsap.somsap.com/2025/09/04/pyqt5-excel-%e2%86%92-markdown-%eb%b3%80%ed%99%98%ea%b8%b0-gui/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Thu, 04 Sep 2025 07:31:42 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=1683</guid>

					<description><![CDATA[<p>누구나 그대로 따라 만들어서 빌드/배포까지 할 수 있도록 단계별로 정리했습니다. 1) 환경 준비 2) 의존성&#160;설치 3) 프로젝트 구조 만들기 아이콘&#160;파일은 반드시&#160;img/icon/excel2md_transparent.ico에&#160;두세요. 4) 전체&#160;로직 5) 빌드/배포 리소스(아이콘&#160;폴더)를&#160;실행 파일에 동봉해야 런타임에&#160;접근&#160;가능합니다. 6) 동작&#160;확인&#160;체크리스트 7) 문제 해결 팁 PyInstaller 경고:</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/09/04/pyqt5-excel-%e2%86%92-markdown-%eb%b3%80%ed%99%98%ea%b8%b0-gui/">PyQt5 Excel → Markdown 변환기 (GUI)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<p>누구나 그대로 따라 만들어서 빌드/배포까지 할 수 있도록 단계별로 정리했습니다.</p>



<h2 class="wp-block-heading">1) 환경 준비</h2>



<ul class="wp-block-list">
<li>Windows 10/11, macOS 최신 버전 권장</li>



<li>Python 3.10.x 설치</li>



<li>가상환경 권장</li>
</ul>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"># (선택) 가상환경
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS/Linux
source .venv/bin/activate</code></pre></div>



<h2 class="wp-block-heading">2) 의존성&nbsp;설치</h2>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">pip install PyQt5 aspose-cells pyinstaller</code></pre></div>



<ul class="wp-block-list">
<li>PyQt5: GUI 프레임워크</li>



<li>aspose-cells: Excel → Markdown 저장</li>



<li>PyInstaller: 배포용&nbsp;실행파일 생성</li>
</ul>



<h2 class="wp-block-heading">3) 프로젝트 구조 만들기</h2>



<pre class="wp-block-code"><code>excel2md/
├──&nbsp;excel2md.py
└──&nbsp;img/
&nbsp;&nbsp;&nbsp;&nbsp;└──&nbsp;icon/
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;└──&nbsp;excel2md_transparent.ico</code></pre>



<p>아이콘&nbsp;파일은 반드시&nbsp;img/icon/excel2md_transparent.ico에&nbsp;두세요.</p>



<h2 class="wp-block-heading">4) 전체&nbsp;로직</h2>



<ul class="wp-block-list">
<li>각 시트를 개별&nbsp;.md로 저장</li>



<li>“Evaluation&nbsp;Only” 줄 제거</li>



<li>전체&nbsp;통합&nbsp;파일도&nbsp;함께&nbsp;생성</li>
</ul>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python">from PyQt5.QtWidgets import (
    QApplication, QWidget, QPushButton, QLabel, QFileDialog,
    QVBoxLayout, QHBoxLayout, QMessageBox, QProgressBar, QFrame
)
from PyQt5.QtGui import QIcon, QFont, QPalette, QColor
from PyQt5.QtCore import Qt
from aspose.cells import Workbook
import os
import sys
import platform


def get_resource_path(relative_path):
    """PyInstaller 환경에서 리소스 경로 가져오기"""
    if hasattr(sys, '_MEIPASS'):
        # PyInstaller 임시 디렉토리
        return os.path.join(sys._MEIPASS, relative_path)
    else:
        # 일반 실행 환경
        return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)


class ExcelToMarkdownApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setup_window()
        self.file_path = ""
        self.output_dir = ""
        self.init_ui()

    def setup_window(self):
        """윈도우 설정"""
        self.setWindowTitle("Excel to Markdown Converter")
        
        # PyInstaller 환경에서 아이콘 찾기
        icon_path = get_resource_path(os.path.join("img", "icon", "excel2md_transparent.ico"))
        
        if os.path.exists(icon_path):
            try:
                self.setWindowIcon(QIcon(icon_path))
            except Exception as e:
                print(f"아이콘 로드 실패: {e}")
        else:
            print(f"아이콘 파일을 찾을 수 없습니다: {icon_path}")
        
        self.resize(500, 600)
        self.setMinimumSize(450, 300)

    def init_ui(self):
        """UI 초기화"""
        # 메인 레이아웃
        main_layout = QVBoxLayout()
        main_layout.setSpacing(10)
        main_layout.setContentsMargins(20, 20, 20, 20)
        
        # 제목
        title = QLabel("Excel to Markdown Converter")
        title.setFont(QFont("맑은 고딕", 18, QFont.Bold))
        title.setAlignment(Qt.AlignCenter)
        title.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
        
        # 서브타이틀
        subtitle = QLabel("Excel 파일을 시트별/통합 Markdown 파일로 변환합니다")
        subtitle.setFont(QFont("맑은 고딕", 11))
        subtitle.setAlignment(Qt.AlignCenter)
        subtitle.setStyleSheet("color: #7f8c8d; margin-bottom: 20px;")
        
        # 파일 선택 영역
        file_section = self.create_file_section()
        
        # 출력 폴더 선택 영역
        output_section = self.create_output_section()
        
        # 진행 상태
        progress_section = self.create_progress_section()
        
        # 변환 버튼
        convert_button = QPushButton("변환 시작")
        convert_button.setFont(QFont("맑은 고딕", 12, QFont.Bold))
        convert_button.setMinimumHeight(45)
        convert_button.setStyleSheet("""
            QPushButton {
                background-color: #3498db;
                color: white;
                border: none;
                border-radius: 6px;
                padding: 0px;
            }
            QPushButton:hover {
                background-color: #2980b9;
            }
            QPushButton:pressed {
                background-color: #21618c;
            }
        """)
        convert_button.clicked.connect(self.run_convert)
        
        # 상태 라벨
        self.status_label = QLabel("")
        self.status_label.setFont(QFont("맑은 고딕", 10))
        self.status_label.setAlignment(Qt.AlignCenter)
        self.status_label.setStyleSheet("color: #7f8c8d;")
        
        # 레이아웃에 추가
        main_layout.addWidget(title)
        main_layout.addWidget(subtitle)
        main_layout.addWidget(file_section)
        main_layout.addWidget(output_section)
        main_layout.addWidget(progress_section)
        main_layout.addWidget(convert_button)
        main_layout.addWidget(self.status_label)
        
        self.setLayout(main_layout)

    def create_file_section(self):
        """파일 선택 섹션"""
        frame = QFrame()
        frame.setFrameStyle(QFrame.StyledPanel)
        frame.setStyleSheet("""
            QFrame {
                background-color: #f8f9fa;
                border: 1px solid #dee2e6;
                border-radius: 8px;
                padding: 0px;
            }
        """)
        
        layout = QVBoxLayout()
        layout.setSpacing(2)
        layout.setContentsMargins(15, 15, 15, 15)
        
        # 제목
        title = QLabel("📁 Excel 파일 선택")
        title.setFont(QFont("맑은 고딕", 12, QFont.Bold))
        title.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
        
        # 파일 경로 표시
        self.file_label = QLabel("파일을 선택하세요")
        self.file_label.setFont(QFont("맑은 고딕", 10))
        self.file_label.setStyleSheet("""
            color: #6c757d;
            background-color: white;
            border: 1px solid #ced4da;
            border-radius: 4px;
            padding: 0px;
            min-height: 25px;
        """)
        
        # 선택 버튼
        select_button = QPushButton("파일 선택")
        select_button.setFont(QFont("맑은 고딕", 10))
        select_button.setStyleSheet("""
            QPushButton {
                background-color: #6c757d;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 0px;
                min-height: 30px;
            }
            QPushButton:hover {
                background-color: #5a6268;
            }
        """)
        select_button.clicked.connect(self.pick_file)
        
        layout.addWidget(title)
        layout.addWidget(self.file_label)
        layout.addWidget(select_button)
        frame.setLayout(layout)
        
        return frame

    def create_output_section(self):
        """출력 폴더 선택 섹션"""
        frame = QFrame()
        frame.setFrameStyle(QFrame.StyledPanel)
        frame.setStyleSheet("""
            QFrame {
                background-color: #f8f9fa;
                border: 1px solid #dee2e6;
                border-radius: 8px;
                padding: 0px;
            }
        """)
        
        layout = QVBoxLayout()
        layout.setSpacing(2)
        layout.setContentsMargins(15, 15, 15, 15)
        
        # 제목
        title = QLabel("📂 출력 폴더 선택")
        title.setFont(QFont("맑은 고딕", 12, QFont.Bold))
        title.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
        
        # 폴더 경로 표시
        self.output_label = QLabel("폴더를 선택하세요")
        self.output_label.setFont(QFont("맑은 고딕", 10))
        self.output_label.setStyleSheet("""
            color: #6c757d;
            background-color: white;
            border: 1px solid #ced4da;
            border-radius: 4px;
            padding: 0px;
            min-height: 25px;
        """)
        
        # 선택 버튼
        select_button = QPushButton("폴더 선택")
        select_button.setFont(QFont("맑은 고딕", 10))
        select_button.setStyleSheet("""
            QPushButton {
                background-color: #6c757d;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 0px;
                min-height: 30px;
            }
            QPushButton:hover {
                background-color: #5a6268;
            }
        """)
        select_button.clicked.connect(self.pick_output)
        
        layout.addWidget(title)
        layout.addWidget(self.output_label)
        layout.addWidget(select_button)
        frame.setLayout(layout)
        
        return frame

    def create_progress_section(self):
        """진행 상태 섹션"""
        frame = QFrame()
        frame.setFrameStyle(QFrame.StyledPanel)
        frame.setStyleSheet("""
            QFrame {
                background-color: #f8f9fa;
                border: 1px solid #dee2e6;
                border-radius: 8px;
                padding: 0px;
            }
        """)
        
        layout = QVBoxLayout()
        layout.setSpacing(2)
        layout.setContentsMargins(15, 15, 15, 15)
        
        # 제목
        title = QLabel("진행 상태")
        title.setFont(QFont("맑은 고딕", 12, QFont.Bold))
        title.setStyleSheet("color: #2c3e50; margin-bottom: 2px;")
        
        # 프로그레스 바
        self.progress = QProgressBar()
        self.progress.setTextVisible(True)
        self.progress.setStyleSheet("""
            QProgressBar {
                border: 1px solid #ced4da;
                border-radius: 4px;
                background-color: white;
                height: 25px;
                text-align: center;
                font-weight: bold;
                color: #2c3e50;
                padding: 0px;
            }
            QProgressBar::chunk {
                background-color: #3498db;
                border-radius: 3px;
                margin: 1px;
            }
        """)
        
        layout.addWidget(title)
        layout.addWidget(self.progress)
        frame.setLayout(layout)
        
        return frame

    def pick_file(self):
        """파일 선택"""
        fname, _ = QFileDialog.getOpenFileName(
            self, 
            "Excel 파일 선택", 
            "", 
            "Excel Files (*.xlsx *.xls);;All Files (*)"
        )
        if fname:
            self.file_path = fname
            filename = os.path.basename(fname)
            self.file_label.setText(f"선택됨: {filename}")
            self.file_label.setFont(QFont("맑은 고딕", 10))
            self.file_label.setStyleSheet("""
                color: #28a745;
                background-color: white;
                border: 1px solid #28a745;
                border-radius: 4px;
                padding: 0px;
                min-height: 25px;
            """)
            self.update_status("Excel 파일이 선택되었습니다")

    def pick_output(self):
        """출력 폴더 선택"""
        folder = QFileDialog.getExistingDirectory(self, "출력 폴더 선택")
        if folder:
            self.output_dir = folder
            # 절대 경로 전체 표시
            self.output_label.setText(f"선택됨: {folder}")
            self.output_label.setFont(QFont("맑은 고딕", 10))
            self.output_label.setStyleSheet("""
                color: #28a745;
                background-color: white;
                border: 1px solid #28a745;
                border-radius: 4px;
                padding: 0px;
                min-height: 25px;
            """)
            self.update_status("출력 폴더가 선택되었습니다")

    def update_status(self, message):
        """상태 메시지 업데이트"""
        self.status_label.setText(message)
        self.status_label.setFont(QFont("맑은 고딕", 10))
        self.status_label.setStyleSheet("color: #28a745; font-weight: bold;")

    def update_progress(self, value, maximum):
        """진행률 업데이트"""
        self.progress.setMaximum(maximum)
        self.progress.setValue(value)
        if value &gt; 0:
            percentage = int((value / maximum) * 100)
            self.update_status(f"변환 중... {percentage}% 완료")

    def run_convert(self):
        """변환 실행"""
        if not self.file_path:
            QMessageBox.warning(self, "경고", "Excel 파일을 선택해주세요.")
            return
        if not self.output_dir:
            QMessageBox.warning(self, "경고", "출력 폴더를 선택해주세요.")
            return
        
        try:
            self.update_status("변환을 시작합니다...")
            self.progress.setValue(0)
            
            workbook = Workbook(self.file_path)
            sheet_count = len(workbook.worksheets)
            
            if sheet_count == 0:
                QMessageBox.warning(self, "경고", "Excel 파일에 시트가 없습니다.")
                return
            
            self.update_progress(0, sheet_count)
            combined_content = &#091;]

            for i in range(sheet_count):
                sheet = workbook.worksheets&#091;i]
                
                # 안전한 파일명 생성
                safe_name = sheet.name
                for ch in &#091;'&lt;', '&gt;', ':', '"', '/', '\\', '|', '?', '*', ' ']:
                    safe_name = safe_name.replace(ch, '_')
                
                md_filename = os.path.join(self.output_dir, f"{i+1:02d}_{safe_name}.md")
                
                # 새 워크북 생성하여 시트 복사
                new_wb = Workbook()
                new_sheet = new_wb.worksheets&#091;0]
                new_sheet.name = sheet.name
                
                # 데이터 복사
                max_row = sheet.cells.max_data_row
                max_col = sheet.cells.max_data_column
                
                for r in range(max_row + 1):
                    for c in range(max_col + 1):
                        val = sheet.cells.get(r, c).value
                        new_sheet.cells.get(r, c).put_value(val)
                
                # 파일 저장
                new_wb.save(md_filename)
                
                # 파일 내용 읽기 및 정리
                with open(md_filename, 'r', encoding='utf-8') as f:
                    content = f.read()
                
                # Evaluation Only 라인 제거
                lines = content.splitlines()
                filtered_lines = &#091;]
                for line in lines:
                    if "# Evaluation" in line or "Evaluation Only" in line:
                        break
                    filtered_lines.append(line)
                
                content = "\n".join(filtered_lines).rstrip('\n')
                
                # 정리된 내용으로 다시 저장
                with open(md_filename, 'w', encoding='utf-8') as f:
                    f.write(content)
                
                combined_content.append(content)
                self.update_progress(i + 1, sheet_count)
                QApplication.processEvents()
            
            # 통합 파일 생성
            base_name = os.path.splitext(os.path.basename(self.file_path))&#091;0]
            combined_filename = os.path.join(self.output_dir, f"{base_name}_ALL_COMBINED.md")
            
            with open(combined_filename, 'w', encoding='utf-8') as f:
                f.write("# Excel to Markdown 변환 결과\n\n")
                f.write("## 목차\n\n")
                
                for i in range(sheet_count):
                    sheet_name = workbook.worksheets&#091;i].name
                    anchor_name = sheet_name.replace(' ', '-').lower()
                    f.write(f"{i+1}. &#091;{sheet_name}](#{i+1:02d}-{anchor_name})\n")
                
                f.write("\n---\n\n")
                
                for i, content in enumerate(combined_content):
                    if content.strip():
                        sheet_name = workbook.worksheets&#091;i].name
                        anchor_name = sheet_name.replace(' ', '-').lower()
                        f.write(f"&lt;div id=\"{i+1:02d}-{anchor_name}\"&gt;&lt;/div&gt;\n\n")
                        f.write(f"## {i+1:02d}. {sheet_name}\n\n")
                        f.write(content.replace(f"# {sheet_name}", ""))
                        f.write("\n\n---\n\n")
            
            self.progress.setValue(0)
            self.update_status("변환이 완료되었습니다! 🎉")
            
            QMessageBox.information(
                self, 
                "변환 완료", 
                f"모든 시트 변환이 완료되었습니다!\n\n"
                f"📊 총 {sheet_count}개 시트\n"
                f"📁 {sheet_count + 1}개 파일 생성\n"
                f"📂 출력 위치: {self.output_dir}"
            )
            
        except Exception as e:
            self.progress.setValue(0)
            self.update_status("변환 중 오류가 발생했습니다")
            QMessageBox.critical(
                self, 
                "오류", 
                f"변환 중 오류가 발생했습니다:\n\n{str(e)}"
            )


if __name__ == "__main__":
    app = QApplication(sys.argv)
    
    # Windows에서 한글 폰트 설정
    if platform.system() == "Windows":
        app.setFont(QFont("맑은 고딕", 9))
    elif platform.system() == "Darwin":  # macOS
        app.setFont(QFont("Apple SD Gothic Neo", 10))
    else:  # Linux
        app.setFont(QFont("Arial", 9))
    
    window = ExcelToMarkdownApp()
    window.show()
    sys.exit(app.exec_())</code></pre></div>



<h2 class="wp-block-heading">5) 빌드/배포</h2>



<p>리소스(아이콘&nbsp;폴더)를&nbsp;실행 파일에 동봉해야 런타임에&nbsp;접근&nbsp;가능합니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">#&nbsp;리소스&nbsp;포함&nbsp;권장&nbsp;(img&nbsp;폴더)
pyinstaller&nbsp;--onefile&nbsp;--windowed&nbsp;--add-data&nbsp;"img;img"&nbsp;-i&nbsp;img\icon\excel2md_transparent.ico&nbsp;excel2md.py</code></pre></div>



<ul class="wp-block-list">
<li>결과 파일:&nbsp;dist/excel2md.exe&nbsp;(Windows)</li>



<li>실행&nbsp;중&nbsp;임시&nbsp;디렉토리 예:&nbsp;C:\Users\{사용자}\AppData\Local\Temp\_MEIxxxxx</li>
</ul>



<h2 class="wp-block-heading">6) 동작&nbsp;확인&nbsp;체크리스트</h2>



<ul class="wp-block-list">
<li>앱 타이틀/아이콘 정상 표시</li>



<li>Windows: 글꼴이&nbsp;“맑은 고딕”&nbsp;(굴림체 아님)</li>



<li>컴포넌트&nbsp;패딩&nbsp;0, 텍스트 잘&nbsp;보임</li>



<li>출력&nbsp;폴더&nbsp;라벨에 절대&nbsp;경로 그대로 표시</li>



<li>변환 후&nbsp;개별&nbsp;md +&nbsp;통합 md&nbsp;생성</li>
</ul>



<h2 class="wp-block-heading">7) 문제 해결 팁</h2>


<ul>
<li>PyInstaller 경고:</li>
</ul>


<ul class="wp-block-list">
<li>Hidden import&nbsp;&#8220;sip&#8221;&nbsp;경고는 보통 무시&nbsp;가능</li>



<li>필요&nbsp;시&nbsp;&#8211;hidden-import sip&nbsp;추가</li>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash">pyinstaller --onefile --windowed --add-data "img;img" -i img\icon\excel2md_transparent.ico excel2md.py
# 또는 아래 (결과 용량 같음)
pyinstaller&nbsp;--onefile&nbsp;--windowed&nbsp;--add-data&nbsp;"img;img"&nbsp;--hidden-import&nbsp;sip&nbsp;-i&nbsp;img\icon\excel2md_transparent.ico&nbsp;excel2md.py</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h3 class="wp-block-heading">부록: 실행 파일 아이콘과 창 아이콘 차이</h3>



<ul class="wp-block-list">
<li>-i&nbsp;&#8230;ico: 실행 파일 자체의&nbsp;아이콘 (탐색기에서 보임)</li>



<li>self.setWindowIcon(&#8230;): 앱 창/작업표시줄/Alt+Tab에 보이는&nbsp;런타임 아이콘 둘 다&nbsp;설정해야 일관됩니다.</li>



<li>요약
<ul class="wp-block-list">
<li>리소스 경로:&nbsp;get_resource_path()&nbsp;필수</li>



<li>폰트: OS별 한글 폰트 지정</li>



<li>UI: padding 0&nbsp;+ min-height + 레이아웃 마진</li>



<li>빌드:&nbsp;&#8211;add-data &#8220;img;img&#8221;&nbsp;꼭&nbsp;포함</li>
</ul>
</li>
</ul>



<p>소스파일</p>



<div class="wp-block-file"><a id="wp-block-file--media-f19069f2-2a6a-4478-ae6e-3e441e0db67e" href="https://somsap.somsap.com/wp-content/uploads/sites/6/2025/09/excel2md.zip">excel2md</a><a href="https://somsap.somsap.com/wp-content/uploads/sites/6/2025/09/excel2md.zip" class="wp-block-file__button wp-element-button" aria-describedby="wp-block-file--media-f19069f2-2a6a-4478-ae6e-3e441e0db67e" download>다운로드</a></div>



<p>실행파일</p>



<div class="wp-block-file"><a id="wp-block-file--media-c2c2c859-acb2-41e3-9f2c-54dce40699d6" href="https://somsap.somsap.com/wp-content/uploads/sites/6/2025/09/excel2md.exe">excel2md</a><a href="https://somsap.somsap.com/wp-content/uploads/sites/6/2025/09/excel2md.exe" class="wp-block-file__button wp-element-button" aria-describedby="wp-block-file--media-c2c2c859-acb2-41e3-9f2c-54dce40699d6" download>다운로드</a></div>



<p></p>
</ul>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/09/04/pyqt5-excel-%e2%86%92-markdown-%eb%b3%80%ed%99%98%ea%b8%b0-gui/">PyQt5 Excel → Markdown 변환기 (GUI)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/09/04/pyqt5-excel-%e2%86%92-markdown-%eb%b3%80%ed%99%98%ea%b8%b0-gui/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1683</post-id>	</item>
		<item>
		<title>게시판 전체 글 개수 파악 Python 스크립트</title>
		<link>https://somsap.somsap.com/2025/09/03/%ea%b2%8c%ec%8b%9c%ed%8c%90-%ec%a0%84%ec%b2%b4-%ea%b8%80-%ea%b0%9c%ec%88%98-%ed%8c%8c%ec%95%85-python-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/</link>
					<comments>https://somsap.somsap.com/2025/09/03/%ea%b2%8c%ec%8b%9c%ed%8c%90-%ec%a0%84%ec%b2%b4-%ea%b8%80-%ea%b0%9c%ec%88%98-%ed%8c%8c%ec%95%85-python-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Wed, 03 Sep 2025 01:13:05 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=1676</guid>

					<description><![CDATA[<p>AWS 기술 블로그 기준. 변형하여 다른 사이트에도 사용 가능. 사용법 및 참고 사항 이 스크립트는 WP 포스트에 코드로 직접 삽입하거나, 블로그 글 설명에 포함해 활용하기 좋으며, AWS 기술 블로그 전체 게시글 수를 빠르게 확인하는 데 효과적입니다. 추가로 크롤링된 데이터를 기반으로 통계, 트렌드 분석 등을 진행할 수도 있습니다. 사용 결과</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/09/03/%ea%b2%8c%ec%8b%9c%ed%8c%90-%ec%a0%84%ec%b2%b4-%ea%b8%80-%ea%b0%9c%ec%88%98-%ed%8c%8c%ec%95%85-python-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/">게시판 전체 글 개수 파악 Python 스크립트</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<p><a href="https://aws.amazon.com/ko/blogs/tech/" data-type="link" data-id="https://aws.amazon.com/ko/blogs/tech/" target="_blank" rel="noopener">AWS 기술 블로그</a> 기준. 변형하여 다른 사이트에도 사용 가능.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python">import requests
from bs4 import BeautifulSoup

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0 Safari/537.36"
}

def count_blog_posts(base_url, max_pages=100):
    total_count = 0
    for page_num in range(1, max_pages + 1):
        if page_num == 1:
            url = base_url  # 첫 페이지는 별도 URL
        else:
            url = f"{base_url}/page/{page_num}/"
        
        response = requests.get(url, headers=headers)
        if response.status_code != 200:
            print(f"페이지 {page_num} 접근 실패, 종료합니다.")
            break

        soup = BeautifulSoup(response.text, 'html.parser')

        posts = soup.find_all('h2', class_='blog-post-title')
        count = len(posts)
        if count == 0:
            print(f"페이지 {page_num}에 글이 없습니다. 크롤링 종료.")
            break

        total_count += count
        print(f"페이지 {page_num} 글 개수: {count}, 누적 합계: {total_count}")

    return total_count

base_url = "https://aws.amazon.com/ko/blogs/tech"
total_posts = count_blog_posts(base_url)
print(f"전체 게시글 수(추정): {total_posts}")
</code></pre></div>



<h2 class="wp-block-heading">사용법 및 참고 사항</h2>



<ul class="wp-block-list">
<li>AWS 공식 블로그는 크롤링 차단 방지를 위해 User-Agent 헤더를 반드시 지정해야 함.</li>



<li>첫 페이지는 별도의 URL 없이 기본 URL로 접근하고, 그 이후에는&nbsp;<code>/page/2/</code>,&nbsp;<code>/page/3/</code>&nbsp;식으로 페이징 처리됨.</li>



<li>글 제목이 포함된 HTML 구조에 맞게&nbsp;<code>h2.blog-post-title</code>&nbsp;태그를 기준으로 글 개수를 세도록 함.</li>



<li>글이 없는 페이지가 나오면 자동으로 크롤링 종료.</li>



<li>최대 페이지 수 설정(<code>max_pages</code>)으로 무한 루프 방지 가능.</li>



<li>실제 글 개수와 약간의 차이가 있을 수 있으나, 전체 글 수 추정에 유용.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<p>이 스크립트는 WP 포스트에 코드로 직접 삽입하거나, 블로그 글 설명에 포함해 활용하기 좋으며, AWS 기술 블로그 전체 게시글 수를 빠르게 확인하는 데 효과적입니다. 추가로 크롤링된 데이터를 기반으로 통계, 트렌드 분석 등을 진행할 수도 있습니다.</p>



<h2 class="wp-block-heading">사용 결과</h2>



<figure class="wp-block-image size-full"><img data-recalc-dims="1" loading="lazy" decoding="async" width="298" height="471" src="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/09/image-2.png?resize=298%2C471&#038;ssl=1" alt="image 2" class="wp-image-1680" title="게시판 전체 글 개수 파악 Python 스크립트 16" srcset="https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/09/image-2.png?w=298&amp;ssl=1 298w, https://i0.wp.com/somsap.somsap.com/wp-content/uploads/sites/6/2025/09/image-2.png?resize=190%2C300&amp;ssl=1 190w" sizes="auto, (max-width: 298px) 100vw, 298px" /></figure>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/09/03/%ea%b2%8c%ec%8b%9c%ed%8c%90-%ec%a0%84%ec%b2%b4-%ea%b8%80-%ea%b0%9c%ec%88%98-%ed%8c%8c%ec%95%85-python-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/">게시판 전체 글 개수 파악 Python 스크립트</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/09/03/%ea%b2%8c%ec%8b%9c%ed%8c%90-%ec%a0%84%ec%b2%b4-%ea%b8%80-%ea%b0%9c%ec%88%98-%ed%8c%8c%ec%95%85-python-%ec%8a%a4%ed%81%ac%eb%a6%bd%ed%8a%b8/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1676</post-id>	</item>
		<item>
		<title>🎬 안드로이드앱 동영상 얼굴 블러 (MoBlur) 앱 개발 가이드</title>
		<link>https://somsap.somsap.com/2025/09/03/%f0%9f%8e%ac-moblur-%ec%95%b1-%ea%b0%9c%eb%b0%9c-%ea%b0%80%ec%9d%b4%eb%93%9c/</link>
					<comments>https://somsap.somsap.com/2025/09/03/%f0%9f%8e%ac-moblur-%ec%95%b1-%ea%b0%9c%eb%b0%9c-%ea%b0%80%ec%9d%b4%eb%93%9c/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Wed, 03 Sep 2025 00:33:22 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=1568</guid>

					<description><![CDATA[<p>📋 목차 1. 개발 환경 준비 1.1 필수 소프트웨어 1.2 Android Studio 설정 1.3 가상 기기 생성 2. 프로젝트 설정 2.1 Gradle 설정 app/build.gradle.kts 주요 의존성: 2.2 권한 설정 AndroidManifest.xml: 3. 핵심 구조 이해 3.1 아키텍처 개요 3.2 주요 클래스별 역할 MainActivity UIManager PermissionManager VideoProcessor BlurProcessor FaceDetector BlurAnalyzer 3.3 핵심 처리 흐름 4. 빌드 및 [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/09/03/%f0%9f%8e%ac-moblur-%ec%95%b1-%ea%b0%9c%eb%b0%9c-%ea%b0%80%ec%9d%b4%eb%93%9c/">🎬 안드로이드앱 동영상 얼굴 블러 (MoBlur) 앱 개발 가이드</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">📋 목차</h2>



<ol class="wp-block-list">
<li id="1-개발-환경-준비"><a href="#1-개발-환경-준비">개발 환경 준비</a></li>



<li><a href="#2-프로젝트-설정">프로젝트 설정</a></li>



<li><a href="#3-핵심-구조-이해">핵심 구조 이해</a></li>



<li><a href="#4-빌드-및-테스트">빌드 및 테스트</a></li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">1. 개발 환경 준비</h2>



<h3 class="wp-block-heading">1.1 필수 소프트웨어</h3>



<ul class="wp-block-list">
<li><strong>Java JDK 17</strong>: Oracle 또는 OpenJDK</li>



<li><strong>Android Studio</strong>: Narwhal 2025.1.1 Patch 1</li>



<li><strong>Android SDK</strong>: API 36</li>
</ul>



<h3 class="wp-block-heading">1.2 Android Studio 설정</h3>



<pre class="wp-block-code"><code>SDK Manager에서 설치 필요:
- Android 14.0 (API 34)
- Android 13.0 (API 33) 
- Android 7.0 (API 24)
- Build Tools 34.0.0</code></pre>



<h3 class="wp-block-heading">1.3 가상 기기 생성</h3>



<ul class="wp-block-list">
<li>Pixel 7, API 34 권장</li>



<li>RAM 4GB 이상 할당</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading" id="2-프로젝트-설정">2. 프로젝트 설정</h2>



<h3 class="wp-block-heading">2.1 Gradle 설정</h3>



<p><strong>app/build.gradle.kts 주요 의존성:</strong></p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Gradle</div><pre><code class="language-gradle">dependencies {
    <em>// 기본 안드로이드</em>
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    
    <em>// 코틀린</em>
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    <em>// ML Kit 얼굴 인식</em>
    implementation("com.google.mlkit:face-detection:16.1.5")
    
    <em>// OpenCV (선택사항)</em>
    implementation("org.opencv:opencv-android:4.8.0")
}</code></pre></div>



<h3 class="wp-block-heading">2.2 권한 설정</h3>



<p><strong>AndroidManifest.xml:</strong></p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">XML</div><pre><code class="language-xml">&lt;uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /&gt;
&lt;uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /&gt;
&lt;uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /&gt;</code></pre></div>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading" id="3-핵심-구조-이해">3. 핵심 구조 이해</h2>



<h3 class="wp-block-heading">3.1 아키텍처 개요</h3>



<pre class="wp-block-code"><code>MainActivity (UI 컨트롤러)
├── UIManager (화면 관리)
├── PermissionManager (권한 관리)
├── VideoProcessor (동영상 처리)
├── BlurProcessor (블러 처리)
├── FaceDetector (얼굴 감지)
└── BlurAnalyzer (블러 분석)</code></pre>



<h3 class="wp-block-heading">3.2 주요 클래스별 역할</h3>



<h4 class="wp-block-heading">MainActivity</h4>



<ul class="wp-block-list">
<li><code>onCreate()</code>: 앱 초기화, 매니저들 생성</li>



<li><code>previewVideo()</code>: 선택한 동영상 미리보기</li>



<li><code>startProcess()</code>: 동영상 처리 시작</li>



<li><code>onRequestPermissionsResult()</code>: 권한 결과 처리</li>
</ul>



<h4 class="wp-block-heading">UIManager</h4>



<ul class="wp-block-list">
<li><code>initializeViews()</code>: UI 요소들 연결</li>



<li><code>applyTheme()</code>: 테마 적용</li>



<li><code>log()</code>: 로그 메시지 표시</li>



<li><code>updateProgress()</code>: 진행률 업데이트</li>
</ul>



<h4 class="wp-block-heading">PermissionManager</h4>



<ul class="wp-block-list">
<li><code>checkAndRequestPermissions()</code>: 필요한 권한 확인 및 요청</li>



<li><code>handlePermissionResult()</code>: 권한 승인/거부 결과 처리</li>
</ul>



<h4 class="wp-block-heading">VideoProcessor</h4>



<ul class="wp-block-list">
<li><code>processVideo()</code>: 메인 동영상 처리 로직</li>



<li><code>createOutputFormat()</code>: 출력 비디오 포맷 설정</li>



<li><code>processFrames()</code>: 프레임별 처리</li>



<li><code>copyAudioTrack()</code>: 오디오 트랙 복사</li>
</ul>



<h4 class="wp-block-heading">BlurProcessor</h4>



<ul class="wp-block-list">
<li><code>blurFaces()</code>: 얼굴 영역 블러 처리</li>



<li><code>blurWithAdaptiveRenderScript()</code>: GPU 가속 블러</li>



<li><code>blurWithOpenCV()</code>: OpenCV 블러 (폴백)</li>



<li><code>expandFaceRect()</code>: 얼굴 영역 확장</li>
</ul>



<h4 class="wp-block-heading">FaceDetector</h4>



<ul class="wp-block-list">
<li><code>detectFaces()</code>: ML Kit으로 얼굴 감지</li>



<li><code>hasSignificantChange()</code>: 얼굴 위치 변화 감지</li>
</ul>



<h4 class="wp-block-heading">BlurAnalyzer</h4>



<ul class="wp-block-list">
<li><code>calculateAdaptiveBlurRadius()</code>: 해상도별 블러 강도 계산</li>



<li><code>verifyAdaptiveBlurConsistency()</code>: 블러 일관성 검증</li>
</ul>



<h3 class="wp-block-heading">3.3 핵심 처리 흐름</h3>



<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">flowchart TD
    classDef brightBG fill:#ffffff,stroke:#cccccc,color:#222222;
    classDef decision fill:#fff3e0,stroke:#ffb74d,color:#222222,font-weight:bold;
    classDef process fill:#e1f5fe,stroke:#4fc3f7,color:#222222;
    classDef action fill:#e8f5e9,stroke:#81c784,color:#222222;
    classDef output fill:#f3e5f5,stroke:#ba68c8,color:#222222;
    linkStyle default stroke:#999999, stroke-width:1.5px;

    A[사용자가 동영상 선택]:::process --&gt; B[MediaMetadataRetriever]:::process
    B --&gt; C{권한 확인}:::decision
    C --&gt;|권한 없음| D[PermissionManager&lt;br/&gt;권한 요청]:::action
    D --&gt; C
    C --&gt;|권한 있음| E[VideoProcessor.processVideo&lt;br/&gt;시작]:::process

    E --&gt; F[MediaExtractor&lt;br/&gt;비디오 트랙 추출]:::process
    F --&gt; G[MediaCodec Decoder&lt;br/&gt;초기화]:::process
    G --&gt; H[MediaCodec Encoder&lt;br/&gt;초기화]:::process
    H --&gt; I[MediaMuxer&lt;br/&gt;초기화]:::process

    I --&gt; J[프레임 디코딩 루프 시작]:::process
    J --&gt; K[MediaCodec.dequeueOutputBuffer]:::process
    K --&gt; L{프레임 있음?}:::decision
    L --&gt;|없음| K
    L --&gt;|있음| M[YUV → RGB 변환]:::process

    M --&gt; N[FaceDetector.detectFaces&lt;br/&gt;ML Kit 얼굴 감지]:::process
    N --&gt; O{얼굴 발견?}:::decision
    O --&gt;|없음| P[원본 프레임 유지]:::process
    O --&gt;|있음| Q[BlurProcessor.blurFaces]:::process

    Q --&gt; R{RenderScript 사용 가능?}:::decision
    R --&gt;|가능| S[GPU 가속 블러 처리]:::process
    R --&gt;|불가능| T[OpenCV 블러 처리]:::process

    S --&gt; U[블러 처리된 프레임]:::process
    T --&gt; U
    P --&gt; U

    U --&gt; V[Surface에 렌더링]:::process
    V --&gt; W[MediaCodec Encoder&lt;br/&gt;프레임 인코딩]:::process
    W --&gt; X[MediaMuxer&lt;br/&gt;비디오 샘플 쓰기]:::process

    X --&gt; Y{마지막 프레임?}:::decision
    Y --&gt;|아니오| K
    Y --&gt;|예| Z[오디오 트랙 복사]:::process

    Z --&gt; AA[MediaExtractor&lt;br/&gt;오디오 샘플 읽기]:::process
    AA --&gt; BB[MediaMuxer&lt;br/&gt;오디오 샘플 쓰기]:::process
    BB --&gt; CC{오디오 끝?}:::decision
    CC --&gt;|아니오| AA
    CC --&gt;|예| DD[MediaMuxer.stop]:::process

    DD --&gt; EE[파일 저장 완료]:::output
    EE --&gt; FF[갤러리 등록&lt;br/&gt;MediaScannerConnection]:::output
    FF --&gt; GG[처리 완료 알림]:::output

    class A,B,E,F,G,H,I,J,K,M,N,P,Q,S,T,U,V,W,X,Z,AA,BB,DD brightBG,process;
    class C,L,O,R,Y,CC brightBG,decision;
    class D brightBG,action;
    class EE,FF,GG brightBG,output;
</pre></div>



<pre class="wp-block-code"><code>1. 동영상 선택 → MediaMetadataRetriever로 정보 추출
2. 권한 확인 → PermissionManager로 처리
3. 동영상 처리 시작 → VideoProcessor.processVideo()
4. 프레임 디코딩 → MediaCodec 사용
5. 얼굴 감지 → FaceDetector + ML Kit
6. 블러 처리 → BlurProcessor (RenderScript/OpenCV)
7. 프레임 인코딩 → MediaCodec 사용
8. 오디오 복사 → MediaExtractor/MediaMuxer
9. 최종 파일 저장 → 갤러리 등록</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading" id="4-빌드-및-테스트">4. 빌드 및 테스트</h2>



<h3 class="wp-block-heading">4.1 빌드 과정</h3>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"><em># Gradle 동기화</em>
./gradlew sync

<em># Clean 빌드</em>
./gradlew clean

<em># Debug APK 빌드</em>
./gradlew assembleDebug

<em># Release APK 빌드  </em>
./gradlew assembleRelease</code></pre></div>



<h3 class="wp-block-heading">4.2 테스트 방법</h3>



<h4 class="wp-block-heading">가상 기기 테스트</h4>



<ol class="wp-block-list">
<li>AVD Manager에서 가상 기기 실행</li>



<li>Run 버튼으로 앱 설치 및 실행</li>



<li>샘플 동영상으로 기능 테스트</li>
</ol>



<h4 class="wp-block-heading">실제 기기 테스트</h4>



<ol class="wp-block-list">
<li>개발자 옵션 → USB 디버깅 활성화</li>



<li>USB 연결 후 기기 선택</li>



<li>실제 동영상으로 성능 테스트</li>
</ol>



<h3 class="wp-block-heading">4.3 주요 테스트 항목</h3>



<ul class="wp-block-list">
<li>&nbsp;동영상 선택 및 미리보기</li>



<li>&nbsp;권한 요청 및 처리</li>



<li>&nbsp;얼굴 감지 정확도</li>



<li>&nbsp;블러 처리 품질</li>



<li>&nbsp;진행률 표시</li>



<li>&nbsp;메모리 사용량</li>



<li>&nbsp;배터리 소모량</li>
</ul>



<h3 class="wp-block-heading">4.4 문제 해결</h3>



<h4 class="wp-block-heading">빌드 오류</h4>



<ul class="wp-block-list">
<li>Gradle 캐시 삭제:&nbsp;<code>./gradlew clean</code></li>



<li>SDK 버전 확인: Tools → SDK Manager</li>



<li>의존성 충돌:&nbsp;<code>./gradlew dependencies</code>&nbsp;확인</li>
</ul>



<h4 class="wp-block-heading">런타임 오류</h4>



<ul class="wp-block-list">
<li>권한 확인: 설정 → 앱 → 권한</li>



<li>메모리 부족: 힙 크기 증가 또는 이미지 크기 축소</li>



<li>GPU 가속 실패: OpenCV 폴백 동작 확인</li>
</ul>



<h4 class="wp-block-heading">성능 최적화</h4>



<ul class="wp-block-list">
<li>프로파일러로 메모리/CPU 사용량 모니터링</li>



<li>불필요한 비트맵 생성 최소화</li>



<li>백그라운드 스레드에서 무거운 작업 처리</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity" />



<h2 class="wp-block-heading">🚀 개발 팁</h2>



<h3 class="wp-block-heading">코드 품질</h3>



<ul class="wp-block-list">
<li>KDoc 주석으로 모든 public 메서드 문서화</li>



<li>단일 책임 원칙으로 클래스 분리</li>



<li>의존성 주입으로 테스트 용이성 확보</li>
</ul>



<h3 class="wp-block-heading">성능 최적화</h3>



<ul class="wp-block-list">
<li>RenderScript로 GPU 가속 활용</li>



<li>적응형 블러로 해상도별 최적화</li>



<li>메모리 풀링으로 GC 압박 감소</li>
</ul>



<h3 class="wp-block-heading">사용자 경험</h3>



<ul class="wp-block-list">
<li>진행률 표시로 사용자 피드백 제공</li>



<li>에러 상황에 대한 명확한 안내</li>



<li>다크/라이트 테마 지원</li>
</ul>



<p>이 가이드를 통해 MoBlur 앱의 핵심 구조를 이해하고 효율적으로 개발할 수 있습니다.</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/09/03/%f0%9f%8e%ac-moblur-%ec%95%b1-%ea%b0%9c%eb%b0%9c-%ea%b0%80%ec%9d%b4%eb%93%9c/">🎬 안드로이드앱 동영상 얼굴 블러 (MoBlur) 앱 개발 가이드</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/09/03/%f0%9f%8e%ac-moblur-%ec%95%b1-%ea%b0%9c%eb%b0%9c-%ea%b0%80%ec%9d%b4%eb%93%9c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1568</post-id>	</item>
		<item>
		<title>📊 엑셀 시트별 마크다운 변환기 완전 가이드 (CUI)</title>
		<link>https://somsap.somsap.com/2025/08/28/%f0%9f%93%8a-%ec%97%91%ec%85%80-%ec%8b%9c%ed%8a%b8%eb%b3%84-%eb%a7%88%ed%81%ac%eb%8b%a4%ec%9a%b4-%eb%b3%80%ed%99%98%ea%b8%b0-%ec%99%84%ec%a0%84-%ea%b0%80%ec%9d%b4%eb%93%9c/</link>
					<comments>https://somsap.somsap.com/2025/08/28/%f0%9f%93%8a-%ec%97%91%ec%85%80-%ec%8b%9c%ed%8a%b8%eb%b3%84-%eb%a7%88%ed%81%ac%eb%8b%a4%ec%9a%b4-%eb%b3%80%ed%99%98%ea%b8%b0-%ec%99%84%ec%a0%84-%ea%b0%80%ec%9d%b4%eb%93%9c/#respond</comments>
		
		<dc:creator><![CDATA[somsap]]></dc:creator>
		<pubDate>Thu, 28 Aug 2025 00:13:31 +0000</pubDate>
				<category><![CDATA[개발/프로그래밍]]></category>
		<guid isPermaLink="false">https://somsap.somsap.com/?p=1644</guid>

					<description><![CDATA[<p>업무를 하다 보면 여러 시트로 구성된 엑셀 파일을 각각 마크다운 파일로 변환해야 할 때가 있습니다. 오늘은 Python을 사용해서 엑셀의 모든 시트를 개별 마크다운 파일로 자동 변환하는 방법을 단계별로 알아보겠습니다. 🎯 최종 결과물 이 가이드를 따라하면 다음과 같은 결과를 얻을 수 있습니다: 📁 input_sheets/ ├── 01_매출_현황.md ├── 02_재고_관리.md ├── 03_고객_정보.md ├── 04_예산_계획.md └── 🔗 input_ALL_COMBINED.md (모든 [&#8230;]</p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/08/28/%f0%9f%93%8a-%ec%97%91%ec%85%80-%ec%8b%9c%ed%8a%b8%eb%b3%84-%eb%a7%88%ed%81%ac%eb%8b%a4%ec%9a%b4-%eb%b3%80%ed%99%98%ea%b8%b0-%ec%99%84%ec%a0%84-%ea%b0%80%ec%9d%b4%eb%93%9c/">📊 엑셀 시트별 마크다운 변환기 완전 가이드 (CUI)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<p>업무를 하다 보면 여러 시트로 구성된 엑셀 파일을 각각 마크다운 파일로 변환해야 할 때가 있습니다. <br>오늘은 Python을 사용해서 엑셀의 모든 시트를 개별 마크다운 파일로 자동 변환하는 방법을 단계별로 알아보겠습니다.</p>



<h2 class="wp-block-heading">🎯 최종 결과물</h2>



<p>이 가이드를 따라하면 다음과 같은 결과를 얻을 수 있습니다:<br><br><code>📁 input_sheets/<br>  ├── 01_매출_현황.md<br>  ├── 02_재고_관리.md<br>  ├── 03_고객_정보.md<br>  ├── 04_예산_계획.md<br>  └── 🔗 input_ALL_COMBINED.md (모든 시트 통합본)<br></code></p>



<h2 class="wp-block-heading">📋 준비사항</h2>



<h2 class="wp-block-heading">1단계: Python 환경 확인</h2>



<p>먼저 Python이 설치되어 있는지 확인합니다.</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"><code>python --version<span style="font-size: 0.8em"></span></code></code></pre></div>



<p>Python 3.7 이상이 필요합니다. 없다면&nbsp;<a rel="noreferrer noopener" target="_blank" href="https://python.org/">python.org</a>에서 설치해주세요.</p>



<h2 class="wp-block-heading">2단계: 필요한 라이브러리 설치</h2>



<p>터미널(CMD)에서 다음 명령어를 실행합니다:</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"><code>pip install aspose-cells-python<span style="font-size: 0.8em"></span></code></code></pre></div>



<p><strong>참고</strong>: Windows에서는 .NET Framework 4.0 이상 또는 .NET 6.0이 필요합니다.</p>



<h2 class="wp-block-heading">3단계: 작업 폴더 및 파일 준비</h2>



<ol class="wp-block-list">
<li>새 폴더를 만들고 그 안에 변환할 엑셀 파일을&nbsp;<code>input.xlsx</code>&nbsp;이름으로 저장합니다.</li>



<li>같은 폴더에 Python 스크립트 파일을 만들 예정입니다.</li>
</ol>



<h2 class="wp-block-heading">💻 완전한 변환 코드</h2>



<p>다음 코드를&nbsp;<code>excel_to_markdown.py</code>&nbsp;파일로 저장하세요:</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code>from aspose.cells import Workbook
import os
import re

try:
    <em># 1. 파일 존재 여부 확인</em>
    if not os.path.exists("input.xlsx"):
        print("ERROR: input.xlsx 파일이 없습니다.")
        print("Python 파일과 같은 폴더에 input.xlsx 파일을 넣어주세요.")
        exit()
    
    print("📂 input.xlsx 파일을 처리 중...")
    
    <em># 2. 엑셀 파일 열기</em>
    workbook = Workbook("input.xlsx")
    
    <em># 3. 출력 폴더 생성</em>
    base_filename = os.path.splitext("input.xlsx")&#091;0]
    output_dir = f"{base_filename}_sheets"
    
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
        print(f"📁 출력 폴더 '{output_dir}' 생성했습니다.")
    
    <em># 4. 시트 개수 확인</em>
    sheet_count = len(workbook.worksheets)
    print(f"📊 총 {sheet_count}개의 시트를 발견했습니다.")
    
    <em># 5. 통합 파일을 위한 내용 저장소</em>
    combined_content = &#091;]
    
    <em># 6. 각 시트를 개별 MD 파일로 변환</em>
    for i in range(sheet_count):
        print(f"⏳ {i+1}번째 시트 처리 중...")
        
        <em># 6-1. 새 워크북 생성 및 시트 복사</em>
        new_workbook = Workbook()
        original_sheet = workbook.worksheets&#091;i]
        new_sheet = new_workbook.worksheets&#091;0]
        
        <em># 6-2. 원본 시트 이름 설정</em>
        new_sheet.name = original_sheet.name
        sheet_name = original_sheet.name
        
        <em># 6-3. 파일명으로 사용할 수 없는 문자 처리</em>
        safe_sheet_name = re.sub(r'&#091;&lt;&gt;:"/\\|?*]', '_', sheet_name)
        safe_sheet_name = safe_sheet_name.replace(' ', '_')
        safe_sheet_name = safe_sheet_name.strip()
        
        <em># 6-4. 데이터 복사</em>
        max_row = original_sheet.cells.max_data_row
        max_col = original_sheet.cells.max_data_column
        
        if max_row &gt;= 0 and max_col &gt;= 0:
            for row in range(max_row + 1):
                for col in range(max_col + 1):
                    source_cell = original_sheet.cells.get(row, col)
                    target_cell = new_sheet.cells.get(row, col)
                    target_cell.put_value(source_cell.value)
        
        <em># 6-5. 개별 MD 파일 저장</em>
        output_filename = os.path.join(output_dir, f"{i+1:02d}_{safe_sheet_name}.md")
        new_workbook.save(output_filename)
        
        <em># 6-6. 파일 후처리 (불필요한 내용 제거)</em>
        try:
            with open(output_filename, 'r', encoding='utf-8') as f:
                content = f.read()
            
            <em># "# Evaluation" 이후 모든 내용 제거</em>
            if "# Evaluation" in content:
                content = content.split("# Evaluation")&#091;0].rstrip()
            
            <em># 마지막 빈 줄들 제거</em>
            content = content.rstrip('\n')
            
            <em># 수정된 내용으로 파일 다시 저장</em>
            with open(output_filename, 'w', encoding='utf-8') as f:
                f.write(content)
            
            <em># 통합 파일용 내용 저장</em>
            combined_content.append(content)
                
        except Exception as e:
            print(f"⚠️  파일 후처리 중 오류: {e}")
        
        print(f"✅ {i+1}번째 시트 '{sheet_name}'를 '{output_filename}'로 저장했습니다.")
    
    <em># 7. 모든 시트를 합친 통합 MD 파일 생성</em>
    print("📝 통합 파일 생성 중...")
    combined_filename = os.path.join(output_dir, f"{base_filename}_ALL_COMBINED.md")
    
    try:
        with open(combined_filename, 'w', encoding='utf-8') as f:
            <em># 7-1. 목차 생성</em>
            f.write("## 📋 목차\n\n")
            for i in range(sheet_count):
                sheet_name = workbook.worksheets&#091;i].name
                anchor_name = sheet_name.replace(' ', '-').lower()
                f.write(f"{i+1}. &#091;{sheet_name}](#{i+1:02d}-{anchor_name})\n")
            f.write("\n---\n\n")
            
            <em># 7-2. 각 시트 내용 추가</em>
            for i, content in enumerate(combined_content):
                if content.strip():
                    sheet_name = workbook.worksheets&#091;i].name
                    anchor_name = sheet_name.replace(' ', '-').lower()
                    f.write(f"&lt;div id=\"{i+1:02d}-{anchor_name}\"&gt;&lt;/div&gt;\n\n")
                    f.write(f"## {i+1:02d}. {sheet_name}\n\n")
                    f.write(content.replace(f"# {sheet_name}", ""))
                    f.write("\n\n---\n\n")
        
        print(f"✅ 통합 파일 '{combined_filename}' 생성 완료!")
        
    except Exception as e:
        print(f"⚠️  통합 파일 생성 중 오류: {e}")
    
    <em># 8. 완료 메시지 및 결과 출력</em>
    print(f"\n🎉 모든 변환 작업이 완료되었습니다!")
    print(f"📁 '{output_dir}' 폴더에 총 {sheet_count + 1}개의 MD 파일이 생성되었습니다.")
    
    print(f"\n📋 생성된 파일 목록:")
    for filename in sorted(os.listdir(output_dir)):
        if filename.endswith('.md'):
            if "ALL_COMBINED" in filename:
                print(f"   🔗 {filename} (통합파일)")
            else:
                print(f"   - {filename}")
    
except Exception as e:
    print(f"❌ 오류 발생: {e}")
    print("파일이 열려있다면 닫고 다시 시도해주세요.")<span style="font-size: 1em"></span></code></code></pre></div>



<h2 class="wp-block-heading">🚀 실행 방법</h2>



<h2 class="wp-block-heading">1단계: 파일 준비</h2>



<ul class="wp-block-list">
<li>변환할 엑셀 파일을&nbsp;<code>input.xlsx</code>&nbsp;이름으로 저장</li>



<li><code>excel_to_markdown.py</code>&nbsp;파일과 같은 폴더에 배치</li>
</ul>



<h2 class="wp-block-heading">2단계: 스크립트 실행</h2>



<p>터미널에서 다음 명령어 실행:</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"><code>python excel_to_markdown.py<span style="font-size: 0.8em"></span></code></code></pre></div>



<h2 class="wp-block-heading">3단계: 결과 확인</h2>



<p>실행이 완료되면 다음과 같은 폴더 구조가 생성됩니다:<br><code>📁 input_sheets/<br>  ├── 01_매출_현황.md      (첫 번째 시트)<br>  ├── 02_재고_관리.md      (두 번째 시트)<br>  ├── 03_고객_정보.md      (세 번째 시트)<br>  └── input_ALL_COMBINED.md (모든 시트 통합)<br></code></p>



<h2 class="wp-block-heading">✨ 주요 기능 소개</h2>



<h2 class="wp-block-heading">🎯 스마트한 파일명 처리</h2>



<ul class="wp-block-list">
<li>시트 순서에 맞게&nbsp;<code>01_</code>,&nbsp;<code>02_</code>&nbsp;번호 자동 부여</li>



<li>파일명에 사용할 수 없는 특수문자를 자동으로&nbsp;<code>_</code>로 변경</li>



<li>띄어쓰기도&nbsp;<code>_</code>로 자동 변환</li>
</ul>



<h2 class="wp-block-heading">📚 통합 파일 생성</h2>



<ul class="wp-block-list">
<li>모든 시트를 하나의 마크다운 파일로 통합</li>



<li>클릭 가능한 목차 자동 생성</li>



<li>각 시트별로 명확한 구분선 추가</li>
</ul>



<h2 class="wp-block-heading">🧹 자동 정리 기능</h2>



<ul class="wp-block-list">
<li>불필요한 &#8220;# Evaluation&#8221; 섹션 자동 제거</li>



<li>마지막 빈 줄들 자동 정리</li>



<li>깔끔한 마크다운 형식으로 변환</li>
</ul>



<h2 class="wp-block-heading">🔧 커스터마이징 옵션</h2>



<h2 class="wp-block-heading">다른 파일명 사용하기</h2>



<p><code>input.xlsx</code>&nbsp;대신 다른 파일명을 사용하려면 코드에서 해당 부분을 수정하세요:</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code><em># 이 부분을 원하는 파일명으로 변경</em>
if not os.path.exists("your_file.xlsx"):
    print("ERROR: your_file.xlsx 파일이 없습니다.")
    exit()

workbook = Workbook("your_file.xlsx")<span style="font-size: 1em"></span></code></code></pre></div>



<h2 class="wp-block-heading">출력 폴더명 변경하기</h2>



<p>출력 폴더명을 바꾸려면:</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><code><em># 기본: input_sheets</em>
output_dir = f"{base_filename}_sheets"

<em># 변경 예시: input_markdown</em>
output_dir = f"{base_filename}_markdown"<span style="font-size: 1em"></span></code></code></pre></div>



<h2 class="wp-block-heading">🆚 기존 도구들과의 비교</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>구분</th><th>이 스크립트</th><th>온라인 도구</th><th>수동 변환</th></tr></thead><tbody><tr><td>시트별 분리</td><td>✅ 자동</td><td>❌ 불가능</td><td>⏰ 시간 소요</td></tr><tr><td>통합 파일</td><td>✅ 자동 생성</td><td>❌ 불가능</td><td>⏰ 수동 작업</td></tr><tr><td>파일명 정리</td><td>✅ 스마트 처리</td><td>❌ 제한적</td><td>⏰ 수동 편집</td></tr><tr><td>오프라인 사용</td><td>✅ 가능</td><td>❌ 인터넷 필요</td><td>✅ 가능</td></tr><tr><td>비용</td><td>🆓 무료</td><td>💰 일부 유료</td><td>🆓 무료</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">🐛 문제 해결</h2>



<h2 class="wp-block-heading">자주 발생하는 오류들</h2>



<p><strong>1. ModuleNotFoundError: No module named &#8216;aspose&#8217;</strong></p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Bash</div><pre><code class="language-bash"># 해결방법: 라이브러리 재설치
pip install aspose-cells-python</code></pre></div>



<p><strong>2. 파일을 찾을 수 없습니다</strong></p>



<ul class="wp-block-list">
<li><code>input.xlsx</code>&nbsp;파일이 Python 스크립트와 같은 폴더에 있는지 확인</li>



<li></li>



<li>파일명이 정확한지 확인 (대소문자 구분)</li>
</ul>



<p><strong>3. 엑셀 파일이 열려있는 상태</strong></p>



<ul class="wp-block-list">
<li>엑셀 파일을 닫고 다시 실행</li>



<li>다른 프로그램에서 파일을 사용 중이지 않은지 확인</li>
</ul>



<h2 class="wp-block-heading">🎁 추가 활용 팁</h2>



<h2 class="wp-block-heading">배치 처리 스크립트</h2>



<p>여러 엑셀 파일을 한 번에 처리하고 싶다면:</p>



<div class="my-codeblock-wrap"><div class="my-codeblock-label">Python</div><pre><code class="language-python"><br><code>import glob<br><br><em># 폴더 내 모든 .xlsx 파일 처리</em><br>for excel_file in glob.glob("*.xlsx"):<br>    print(f"처리 중: {excel_file}")<br>    <em># 위의 변환 코드 실행</em><br></code></code></pre></div>



<h2 class="wp-block-heading">자동화 스케줄링</h2>



<p>Windows 작업 스케줄러나 Linux cron을 사용해서 정기적으로 자동 변환하도록 설정할 수 있습니다.</p>



<h2 class="wp-block-heading">🎊 마무리</h2>



<p>이제 복잡한 엑셀 파일도 몇 초 만에 깔끔한 마크다운 파일로 변환할 수 있습니다!</p>



<p><strong>이 스크립트의 장점:</strong></p>



<ul class="wp-block-list">
<li>⚡&nbsp;<strong>빠른 처리</strong>: 수십 개 시트도 몇 초 내 변환</li>



<li>🎯&nbsp;<strong>정확한 분리</strong>: 각 시트가 개별 파일로 깔끔하게 분리</li>



<li>📚&nbsp;<strong>통합 관리</strong>: 전체 내용을 한 번에 볼 수 있는 통합 파일</li>



<li>🔧&nbsp;<strong>커스터마이징</strong>: 필요에 따라 쉽게 수정 가능</li>
</ul>



<p>업무 효율성을 크게 높여줄 이 도구를 활용해서 더 생산적인 작업 환경을 만들어보세요!</p>



<p></p>
<p>게시물 <a rel="nofollow" href="https://somsap.somsap.com/2025/08/28/%f0%9f%93%8a-%ec%97%91%ec%85%80-%ec%8b%9c%ed%8a%b8%eb%b3%84-%eb%a7%88%ed%81%ac%eb%8b%a4%ec%9a%b4-%eb%b3%80%ed%99%98%ea%b8%b0-%ec%99%84%ec%a0%84-%ea%b0%80%ec%9d%b4%eb%93%9c/">📊 엑셀 시트별 마크다운 변환기 완전 가이드 (CUI)</a>이 <a rel="nofollow" href="https://somsap.somsap.com">솜삽 블로그</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://somsap.somsap.com/2025/08/28/%f0%9f%93%8a-%ec%97%91%ec%85%80-%ec%8b%9c%ed%8a%b8%eb%b3%84-%eb%a7%88%ed%81%ac%eb%8b%a4%ec%9a%b4-%eb%b3%80%ed%99%98%ea%b8%b0-%ec%99%84%ec%a0%84-%ea%b0%80%ec%9d%b4%eb%93%9c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1644</post-id>	</item>
	</channel>
</rss>
