<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ㅎ_이야기</title>
    <link>https://ph702.tistory.com/</link>
    <description>:)</description>
    <language>ko</language>
    <pubDate>Tue, 2 Jun 2026 14:36:29 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>박히응</managingEditor>
    <image>
      <title>ㅎ_이야기</title>
      <url>https://tistory1.daumcdn.net/tistory/7570888/attach/29836a19d56b4abd84dd60ce32a7336b</url>
      <link>https://ph702.tistory.com</link>
    </image>
    <item>
      <title>[ISP 확인] IP에 해당하는 ISP(인터넷 서비스 제공자) 확인</title>
      <link>https://ph702.tistory.com/14</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용자의 ISP 파악&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlgVBL/dJMb99SpOX1/KYYPtZJjd1LirKeYAVasS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlgVBL/dJMb99SpOX1/KYYPtZJjd1LirKeYAVasS0/img.png&quot; data-alt=&quot;실 사용 파일 : 대상 log, GeoLite2-ASN.mmdb, asn_lookup.py, venv&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlgVBL/dJMb99SpOX1/KYYPtZJjd1LirKeYAVasS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlgVBL%2FdJMb99SpOX1%2FKYYPtZJjd1LirKeYAVasS0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;105&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실 사용 파일 : 대상 log, GeoLite2-ASN.mmdb, asn_lookup.py, venv&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구성된 ubuntu 환경&lt;/li&gt;
&lt;li&gt;GeoLite2-ASN.mmdb 다운로드 필요&lt;/li&gt;
&lt;li&gt;ubuntu 환경에서 python 라이브러리 설치 불가 시 가상환경(venv) 구성
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;apt install python3.12-venv #설치&lt;/li&gt;
&lt;li&gt;python3 -m venv venv #가상환경 구성&lt;/li&gt;
&lt;li&gt;source venv/bin/activate #가상환경 사용&lt;/li&gt;
&lt;li&gt;deactivate #가상환경 종료&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;asn_lookup.py 내용 중 GeoLite2-ASN.mmdb 위치 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;52&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nkcjB/dJMcafSDc6m/eNc9sxaDZbuBlVkQw30Phk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nkcjB/dJMcafSDc6m/eNc9sxaDZbuBlVkQw30Phk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nkcjB/dJMcafSDc6m/eNc9sxaDZbuBlVkQw30Phk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnkcjB%2FdJMcafSDc6m%2FeNc9sxaDZbuBlVkQw30Phk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;371&quot; height=&quot;52&quot; data-origin-width=&quot;371&quot; data-origin-height=&quot;52&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;2. asn_lookup.py 내용 중 대상 로그 지정&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;636&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doap7y/dJMcaiBLBOH/CMPZJrM3AygHWqrmjQskU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doap7y/dJMcaiBLBOH/CMPZJrM3AygHWqrmjQskU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doap7y/dJMcaiBLBOH/CMPZJrM3AygHWqrmjQskU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdoap7y%2FdJMcaiBLBOH%2FCMPZJrM3AygHWqrmjQskU0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;636&quot; height=&quot;112&quot; data-origin-width=&quot;636&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;3. &lt;span style=&quot;background-color: #dddddd;&quot;&gt;python3 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;asn_lookup.py&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;asn_lookup.py&lt;/p&gt;
&lt;pre id=&quot;code_1764924420321&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import maxminddb
import pandas as pd
import os
import time

# --- 설정 변수 ---
# GeoLite2-ASN 데이터베이스 파일 경로
DB_PATH = '/root/isp_check/GeoLite2-ASN.mmdb'

# 입력 파일 리스트
INPUT_FILES = [
    'KT_20251027.log', 'KT_20251026.log', 'KT_20251025.log', 'KT_20251024.log',
    'KT_20251023.log', 'KT_20251022.log', 'KT_20251021.log', 'KT_20251020.log',
    'SK_20251027.log', 'SK_20251026.log', 'SK_20251025.log', 'SK_20251024.log',
    'SK_20251023.log', 'SK_20251022.log', 'SK_20251021.log', 'SK_20251020.log'
]

# --- ISP 분류 함수 (키워드 보강됨) ---
def classify_isp(org_name):
    &quot;&quot;&quot;
    조직 이름을 받아 KT, SK, LG, Other로 분류합니다.
    인수합병된 전신 기업명(Hanaro, Dacom 등)과 자회사(Skylife, HelloVision 등)를 포함합니다.
    &quot;&quot;&quot;
    if not isinstance(org_name, str):
        return 'Other'

    org_upper = org_name.upper()

    # 1. KT 그룹 키워드
    # KORNET, QOOK, KTF, SKYLIFE(스카이라이프), HCN(현대HCN 인수)
    kt_keywords = [
        'KT', 'KOREA TELECOM', 'KORNET', 'OLLEH', 'HCN',
        'SKYLIFE', 'KTF', 'QOOK', 'K T', 'KT CORPORATION', 'KT-IDC'
    ]

    # 2. SK 그룹 키워드
    # SKB, SKT, HANARO(하나로텔레콤), DREAMLINE(드림라인), SK TELINK
    sk_keywords = [
        'SK', 'SK BROADBAND', 'SK TELECOM', 'SKT', 'SKB',
        'HANARO', 'DREAMLINE', 'SK TELINK', 'SK STOA'
    ]

    # 3. LG 그룹 키워드
    # DACOM(데이콤), POWERCOMM(파워콤), HELLOVISION(헬로비전), CJ HELLO
    lg_keywords = [
        'LG', 'LGU+', 'DACOM', 'POWERCOMM', 'UPLUS', 'LG TELECOM', 'LGT',
        'HELLOVISION', 'CJ HELLO', 'CJHELLO'
    ]

    # 분류 로직
    if any(k in org_upper for k in kt_keywords):
        return 'KT'
    elif any(k in org_upper for k in sk_keywords):
        return 'SK'
    elif any(k in org_upper for k in lg_keywords):
        return 'LG'
    else:
        return 'Other'

# --- 메인 로직 ---

try:
    reader = maxminddb.open_database(DB_PATH)
except FileNotFoundError:
    print(f&quot;오류: ASN 데이터베이스 파일 ({DB_PATH})을 찾을 수 없습니다.&quot;)
    exit()
except AttributeError:
    reader = maxminddb.open(DB_PATH)

total_ips_processed = 0
print(f&quot;총 {len(INPUT_FILES)}개의 파일 처리를 시작합니다.&quot;)

for input_file in INPUT_FILES:
    start_time = time.time()
    output_file = f&quot;{input_file}_isp_result.csv&quot;

    print(f&quot;\n--- 파일 처리 시작: {input_file} ---&quot;)

    results = []

    try:
        # 파일 읽기 (공백 구분자)
        df = pd.read_csv(input_file, sep='\s+', engine='python', header=None, usecols=[1])

        # IP 조회
        for index, row in df.iterrows():
            ip_str = str(row[1]).strip()

            if not ip_str:
                continue

            try:
                record = reader.get(ip_str)
                asn = record.get('autonomous_system_number', '') if record else ''
                org = record.get('autonomous_system_organization', 'UNKNOWN') if record else 'UNKNOWN'

                results.append({
                    'Original_IP': ip_str,
                    'ASN': asn,
                    'Organization': org
                })
                total_ips_processed += 1

            except (ValueError, Exception):
                results.append({
                    'Original_IP': ip_str,
                    'ASN': 'ERROR',
                    'Organization': 'UNKNOWN'
                })

        # DataFrame 생성 및 결과 저장
        results_df = pd.DataFrame(results)
        results_df.to_csv(output_file, index=False, encoding='utf-8-sig')

        # --- 요약 로직 (Grouping) ---
        results_df['ISP_Group'] = results_df['Organization'].apply(classify_isp)
        group_counts = results_df['ISP_Group'].value_counts()
        total_count = len(results_df)

        # 파일 끝에 요약 추가
        with open(output_file, 'a', encoding='utf-8-sig') as f:
            f.write('\n\n')
            f.write('#ISP Summary\n')

            # LG, KT, SK, Other 순서대로 출력 (데이터 없어도 0.00%로 출력됨)
            target_isps = ['LG', 'KT', 'SK', 'Other']

            for isp in target_isps:
                count = group_counts.get(isp, 0)
                if total_count &amp;gt; 0:
                    percent = (count / total_count * 100)
                else:
                    percent = 0.0

                # 요청하신 포맷 적용
                f.write(f&quot;{isp} : {percent:.2f} %\n&quot;)

        end_time = time.time()
        print(f&quot;처리 완료. IP 수: {len(df)}, 소요 시간: {end_time - start_time:.2f}초&quot;)
        print(f&quot;결과 저장됨: {output_file}&quot;)

    except FileNotFoundError:
        print(f&quot;경고: 입력 파일 {input_file}을 찾을 수 없습니다. 건너뜁니다.&quot;)
    except Exception as e:
        print(f&quot;경고: 파일 {input_file} 처리 중 오류 발생: {e}&quot;)

reader.close()
print(f&quot;\n===== 모든 파일 처리 완료! 총 처리된 IP 수: {total_ips_processed} =====&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RtXia/dJMcajgm1UL/wYgU6ZYdSNacEAGu8h63a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RtXia/dJMcajgm1UL/wYgU6ZYdSNacEAGu8h63a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RtXia/dJMcajgm1UL/wYgU6ZYdSNacEAGu8h63a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRtXia%2FdJMcajgm1UL%2FwYgU6ZYdSNacEAGu8h63a1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;166&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>리눅스</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/14</guid>
      <comments>https://ph702.tistory.com/14#entry14comment</comments>
      <pubDate>Fri, 5 Dec 2025 17:48:54 +0900</pubDate>
    </item>
    <item>
      <title>[음성 대본 추출] Whisper ai를 통한 .srt 파일 추출</title>
      <link>https://ph702.tistory.com/12</link>
      <description>&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/l5zlJ/dJMb9O1Dg4r/JZ9IQFBi04w6uU12ARsCHk/whisper.ipynb?attach=1&amp;amp;knm=tfile.ipynb&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;whisper.ipynb&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;0.02MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;api 미사용(과금 가능성)으로 영상 파일을 대상으로 음성 대본(.srt) 추출&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDHqgb/btsQpWfsv8N/Xi02tZwDl1sUzqnQnpuTFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDHqgb/btsQpWfsv8N/Xi02tZwDl1sUzqnQnpuTFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDHqgb/btsQpWfsv8N/Xi02tZwDl1sUzqnQnpuTFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDHqgb%2FbtsQpWfsv8N%2FXi02tZwDl1sUzqnQnpuTFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1582&quot; height=&quot;210&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;실행 자체는 로컬환경 리소스 절약을 위해 Google Colab을 사용 (로컬환경에서도 실행 가능)&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;구글 코랩 또는 로컬 등 구동 가능한 환경
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://colab.research.google.com/drive/1e1sCe3s4rvEj6nbM_BXEd6iX_BRHHMol?hl=ko#scrollTo=RjZDewfkvyoC&quot;&gt;https://colab.research.google.com/drive/1e1sCe3s4rvEj6nbM_BXEd6iX_BRHHMol?hl=ko#scrollTo=RjZDewfkvyoC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&amp;nbsp;구글 드라이브 (경로 생성은 자동)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;913&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdSb12/btsQqtLdVrf/YlDyfUBqQlImJYm3pbLd1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdSb12/btsQqtLdVrf/YlDyfUBqQlImJYm3pbLd1K/img.png&quot; data-alt=&quot;colab 환경에서 개별실행이 아니라면 모두 실행(빨간박스 표시)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdSb12/btsQqtLdVrf/YlDyfUBqQlImJYm3pbLd1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdSb12%2FbtsQqtLdVrf%2FYlDyfUBqQlImJYm3pbLd1K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;913&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;913&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;colab 환경에서 개별실행이 아니라면 모두 실행(빨간박스 표시)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;기본 모듈 설치 및 마운트&lt;/li&gt;
&lt;li&gt;.srt 파일 추출
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가 프롬프트 입력 (추출 전 프롬프트 질문 有, 고유명사 등을 입력 권장)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 모듈 설치 및 마운트&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1763440139910&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# @title 1. 초기 설정(라이브러리 설치) 및 마운트
# 필수 패키지 설치 (ffmpeg, whisper, tqdm)
!apt -y install ffmpeg &amp;gt; /dev/null
!pip -q install git+https://github.com/openai/whisper.git tqdm

# 구글 드라이브 마운트
from google.colab import drive
drive.mount('/content/drive')

import os
import torch
import whisper
import subprocess
from pathlib import Path
from tqdm.notebook import tqdm

# GPU 사용 가능 여부 확인
device_check = &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;
print(f&quot;\n✅ 설정 완료. (현재 디바이스: {device_check.upper()})&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;.srt 파일 추출 (메인 실행)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1763440181174&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# @title 2. Whisper 자막 추출 실행

# ==========================================
# A. 설정 및 경로 정의
# ==========================================
BASE_DIR = Path(&quot;/content/drive/MyDrive/whisper&quot;)
INPUT_DIR = BASE_DIR / &quot;whisper_input&quot;
OUTPUT_DIR = BASE_DIR / &quot;whisper_output&quot;
TMP_DIR = Path(&quot;/content/whisper_tmp&quot;)

for p in (INPUT_DIR, OUTPUT_DIR, TMP_DIR):
    p.mkdir(parents=True, exist_ok=True)

MODEL_SIZE = &quot;medium&quot;
VIDEO_EXTS = {&quot;.mp4&quot;, &quot;.mov&quot;, &quot;.mkv&quot;, &quot;.m4v&quot;, &quot;.avi&quot;, &quot;.mp3&quot;, &quot;.wav&quot;, &quot;.m4a&quot;, &quot;.flac&quot;, &quot;.wma&quot;}
USE_PREPROCESS = False
PREPROCESS_FILTER = &quot;loudnorm,highpass=f=100&quot;

# ==========================================
# B. 프롬프트 및 디바이스 설정
# ==========================================
DEVICE = &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;
FP16 = (DEVICE == &quot;cuda&quot;)

print(f&quot;  작업 시작 (디바이스: {DEVICE.upper()})&quot;)

# 사용자 키워드 입력
print(&quot;\n&quot; + &quot;-&quot;*40)
print(&quot;  [힌트] 영상의 주제, 고유명사, 전문용어를 입력하세요 (없으면 Enter)&quot;)
user_hint = input(&quot;키워드 입력 &amp;gt; &quot;).strip()

# ▼ [수정됨] 인사말 제거 및 효율성 위주의 지시형 프롬프트
base_prompt = &quot;한국어 자막을 생성합니다. 문장의 끝에는 반드시 마침표를 찍고, 문맥에 맞는 정확한 띄어쓰기와 맞춤법을 준수하세요.&quot;

final_prompt = base_prompt
if user_hint:
    final_prompt += f&quot; 특히 다음 전문 용어의 표기를 정확히 지키세요: {user_hint}&quot;

print(f&quot;  적용된 프롬프트: \&quot;{final_prompt}\&quot;&quot;)
print(&quot;-&quot;*40 + &quot;\n&quot;)

# ==========================================
# C. 내부 처리 함수
# ==========================================
def format_timestamp(seconds: float):
    ms = int((seconds % 1) * 1000)
    seconds = int(seconds)
    hrs = seconds // 3600
    mins = (seconds % 3600) // 60
    secs = seconds % 60
    return f&quot;{hrs:02d}:{mins:02d}:{secs:02d},{ms:03d}&quot;

def write_srt(segments, file_path):
    with open(file_path, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:
        for i, seg in enumerate(segments, start=1):
            start = format_timestamp(seg['start'])
            end = format_timestamp(seg['end'])
            text = seg['text'].strip()
            f.write(f&quot;{i}\n{start} --&amp;gt; {end}\n{text}\n\n&quot;)

def preprocess_audio(src, dst):
    cmd = [
        &quot;ffmpeg&quot;, &quot;-y&quot;, &quot;-i&quot;, str(src),
        &quot;-vn&quot;, &quot;-ac&quot;, &quot;1&quot;, &quot;-ar&quot;, &quot;16000&quot;,
        &quot;-af&quot;, PREPROCESS_FILTER,
        str(dst)
    ]
    subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

# ==========================================
# D. 배치 실행 로직
# ==========================================
def run_batch():
    files = []
    for root, _, names in os.walk(INPUT_DIR):
        for n in names:
            if n.startswith(&quot;.&quot;) or n.endswith(&quot;.part&quot;): continue
            if Path(n).suffix.lower() in VIDEO_EXTS:
                files.append(Path(root) / n)
    files.sort()

    if not files:
        print(f&quot;❌ '{INPUT_DIR}' 폴더에 처리할 파일이 없습니다.&quot;)
        return

    print(f&quot;⏳ 모델 로딩 중... ({MODEL_SIZE})&quot;)
    try:
        model = whisper.load_model(MODEL_SIZE, device=DEVICE)
    except Exception as e:
        print(f&quot;❌ 모델 로드 실패: {e}&quot;)
        return
    print(&quot;✅ 모델 로드 완료.&quot;)

    for f in tqdm(files, desc=&quot;전체 진행률&quot;):
        srt_out = OUTPUT_DIR / f&quot;{f.stem}.srt&quot;

        if srt_out.exists() and srt_out.stat().st_size &amp;gt; 100:
            print(f&quot;⏩ 스킵: {f.name}&quot;)
            continue

        print(f&quot;  처리 중: {f.name}&quot;)
        
        src_file = f
        tmp_wav = None

        try:
            if USE_PREPROCESS:
                tmp_wav = TMP_DIR / f&quot;{f.stem}_temp.wav&quot;
                preprocess_audio(f, tmp_wav)
                src_file = tmp_wav

            # Whisper 추론 실행
            result = model.transcribe(
                str(src_file),
                language=&quot;ko&quot;,
                fp16=FP16,
                initial_prompt=final_prompt, # 수정된 프롬프트 적용
                verbose=False,
                condition_on_previous_text=False,
                temperature=(0.0, 0.2, 0.4, 0.6, 0.8, 1.0),
                compression_ratio_threshold=2.4,
                logprob_threshold=-1.0,
                no_speech_threshold=0.6
            )

            write_srt(result[&quot;segments&quot;], srt_out)
            print(f&quot;   -&amp;gt;   저장 완료&quot;)

        except Exception as e:
            print(f&quot;   -&amp;gt; ❌ 에러 발생: {e}&quot;)

        finally:
            if tmp_wav and tmp_wav.exists():
                try: tmp_wav.unlink()
                except: pass
    
    print(&quot;\n  모든 작업이 완료되었습니다.&quot;)

if __name__ == &quot;__main__&quot;:
    run_batch()&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1571&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VunGg/btsQqmlilRu/Rlh5rsd4E8p5cwtxi9z0qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VunGg/btsQqmlilRu/Rlh5rsd4E8p5cwtxi9z0qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VunGg/btsQqmlilRu/Rlh5rsd4E8p5cwtxi9z0qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVunGg%2FbtsQqmlilRu%2FRlh5rsd4E8p5cwtxi9z0qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1571&quot; height=&quot;202&quot; data-origin-width=&quot;1571&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;649&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9gDxw/btsQyqzS040/3cdmKoqtX5OMsZ4drjo8Xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9gDxw/btsQyqzS040/3cdmKoqtX5OMsZ4drjo8Xk/img.png&quot; data-alt=&quot;모든 음성 추출이 완벽하지않다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9gDxw/btsQyqzS040/3cdmKoqtX5OMsZ4drjo8Xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9gDxw%2FbtsQyqzS040%2F3cdmKoqtX5OMsZ4drjo8Xk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;649&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;649&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;모든 음성 추출이 완벽하지않다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/12</guid>
      <comments>https://ph702.tistory.com/12#entry12comment</comments>
      <pubDate>Fri, 12 Sep 2025 18:31:52 +0900</pubDate>
    </item>
    <item>
      <title>[SBS 편성표 크롤링] 크롬 확장프로그램 위젯</title>
      <link>https://ph702.tistory.com/11</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;실행일 기준 금일, 작일 SBS TV, radio 의 프로그램명, 방송 시간 확인&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;점검 간 데이터 확인 소요 시간 약 66~75% 감소로 효율성 증진&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4ANcu/btsPCpv2bEs/ZkMGi40PA0KmHRelUjtEWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4ANcu/btsPCpv2bEs/ZkMGi40PA0KmHRelUjtEWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4ANcu/btsPCpv2bEs/ZkMGi40PA0KmHRelUjtEWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4ANcu%2FbtsPCpv2bEs%2FZkMGi40PA0KmHRelUjtEWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;최종 폴더 구조:

sbs-extension/
├── images/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
├── manifest.json
├── popup.html
├── popup.js
├── background.js
├── content-script.js
├── style.css
└── README.txt&quot; loading=&quot;lazy&quot; width=&quot;620&quot; height=&quot;235&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;크롬&amp;nbsp;브라우저를&amp;nbsp;열고&amp;nbsp;주소창에&amp;nbsp;chrome://extensions&amp;nbsp;를&amp;nbsp;입력하여&amp;nbsp;확장&amp;nbsp;프로그램&amp;nbsp;관리&amp;nbsp;페이지로&amp;nbsp;이동합니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기존에 있던 'SBS 편성표 뷰어'가 있다면 '삭제' 버튼을 눌러 완전히 제거합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;페이지 오른쪽 상단에 있는 '개발자 모드(Developer mode)' 스위치를 켭니다.&lt;/li&gt;
&lt;li&gt;'압축해제된 확장 프로그램을 로드합니다(Load unpacked)' 버튼을 클릭합니다.&lt;/li&gt;
&lt;li&gt;파일 탐색기가 열리면, 1단계에서 생성한 sbs-extension 폴더를 선택하고 '폴더 선택' 버튼을 누릅니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;설치가&amp;nbsp;완료되면&amp;nbsp;크롬&amp;nbsp;브라우저&amp;nbsp;오른쪽&amp;nbsp;상단&amp;nbsp;툴바에&amp;nbsp;아이콘이&amp;nbsp;추가됩니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;툴바의 아이콘을 클릭하면 편성표 뷰어 팝업창이 나타납니다.&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;'  TV 편성표' 또는 '  라디오 편성표' 버튼을 클릭하면, 잠시 후 해당 채널의 어제와 오늘 편성표를 불러와 화면에 표시합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;background.js&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778103032&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// content-script를 페이지에 주입하고 결과를 반환하는 함수
async function scrapeDataFromUrl(url) {
    let tab;
    try {
        // 보이지 않는 새 탭을 생성하여 페이지를 엽니다.
        tab = await chrome.tabs.create({ url: url, active: false });

        // 페이지 로딩이 완료될 때까지 기다립니다.
        await new Promise((resolve, reject) =&amp;gt; {
            const maxWaitTime = 15000; // 15초 타임아웃
            const timeout = setTimeout(() =&amp;gt; {
                reject(new Error(&quot;페이지 로딩 시간 초과&quot;));
            }, maxWaitTime);

            const listener = (tabId, changeInfo) =&amp;gt; {
                if (tabId === tab.id &amp;amp;&amp;amp; changeInfo.status === 'complete') {
                    clearTimeout(timeout);
                    chrome.tabs.onUpdated.removeListener(listener);
                    resolve();
                }
            };
            chrome.tabs.onUpdated.addListener(listener);
        });

        // 페이지에 content-script.js를 주입하고 실행 결과를 받습니다.
        const results = await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            files: ['content-script.js']
        });

        if (results &amp;amp;&amp;amp; results.length &amp;gt; 0) {
            return results[0].result; // content-script.js가 반환한 값
        } else {
            throw new Error(&quot;스크립트 실행에 실패했습니다.&quot;);
        }
    } finally {
        // 작업이 끝나면 생성했던 탭을 닫습니다.
        if (tab) {
            await chrome.tabs.remove(tab.id);
        }
    }
}

// popup.js로부터 메시지를 수신합니다.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) =&amp;gt; {
    if (request.action === &quot;fetchSchedule&quot;) {
        scrapeDataFromUrl(request.url)
            .then(response =&amp;gt; sendResponse(response))
            .catch(error =&amp;gt; sendResponse({ error: error.message }));
        return true; // 비동기 응답을 위해 true를 반환합니다.
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;content-script.js&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778256400&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 이 함수는 SBS 웹페이지의 컨텍스트에서 실행됩니다.
function scrapeSchedule() {
    const schedule = [];
    // 페이지가 완전히 렌더링된 후이므로, 요소를 찾을 수 있습니다.
    const programElements = document.querySelectorAll('.scheduler_program_w');

    if (programElements.length === 0) {
        return { error: &quot;편성표 요소를 찾을 수 없습니다. 웹사이트 구조가 변경되었을 수 있습니다.&quot; };
    }

    programElements.forEach(el =&amp;gt; {
        const timeEl = el.querySelector('.spt_hours');
        const titleEl = el.querySelector('.spi_title');
        if (timeEl &amp;amp;&amp;amp; titleEl) {
            let programTitle = titleEl.innerText.trim();
            // '보이는 라디오'가 있으면 텍스트 대신 스타일링을 위한 span 태그를 추가합니다.
            if (el.querySelector('.scheduler_label_w_type_bora')) {
                programTitle += ' &amp;lt;span class=&quot;bora-tag&quot;&amp;gt;보라&amp;lt;/span&amp;gt;';
            }
            schedule.push({ time: timeEl.innerText.trim(), program: programTitle });
        }
    });
    return { data: schedule }; // 결과를 객체 형태로 반환
}

// 이 스크립트의 마지막 실행 결과가 executeScript의 반환값이 됩니다.
scrapeSchedule();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;manifest.json&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778283576&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;manifest_version&quot;: 3,
  &quot;name&quot;: &quot;SBS 편성표 뷰어&quot;,
  &quot;version&quot;: &quot;2.0&quot;,
  &quot;description&quot;: &quot;SBS TV와 라디오의 어제, 오늘 편성표를 보여줍니다.&quot;,
  &quot;permissions&quot;: [
    &quot;scripting&quot;,
    &quot;tabs&quot;
  ],
  &quot;host_permissions&quot;: [
    &quot;https://*.sbs.co.kr/*&quot;
  ],
  &quot;action&quot;: {
    &quot;default_popup&quot;: &quot;popup.html&quot;,
    &quot;default_icon&quot;: {
      &quot;16&quot;: &quot;images/icon16.png&quot;,
      &quot;48&quot;: &quot;images/icon48.png&quot;,
      &quot;128&quot;: &quot;images/icon128.png&quot;
    }
  },
  &quot;icons&quot;: {
    &quot;16&quot;: &quot;images/icon16.png&quot;,
    &quot;48&quot;: &quot;images/icon48.png&quot;,
    &quot;128&quot;: &quot;images/icon128.png&quot;
  },
  &quot;background&quot;: {
    &quot;service_worker&quot;: &quot;background.js&quot;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;popup.html&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778304833&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;ko&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;SBS 편성표&amp;lt;/title&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div class=&quot;container&quot;&amp;gt;
        &amp;lt;div class=&quot;header&quot;&amp;gt;
            &amp;lt;img src=&quot;images/icon48.png&quot; alt=&quot;아이콘&quot; class=&quot;icon&quot;&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;h1 class=&quot;title&quot;&amp;gt;SBS 편성표 뷰어&amp;lt;/h1&amp;gt;
                &amp;lt;p id=&quot;initial-guide&quot; class=&quot;guide-text&quot;&amp;gt;보고 싶은 편성표 버튼을 클릭하세요.&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div class=&quot;button-group&quot;&amp;gt;
            &amp;lt;button id=&quot;tv-btn&quot; class=&quot;btn btn-tv&quot;&amp;gt;  TV 편성표&amp;lt;/button&amp;gt;
            &amp;lt;button id=&quot;radio-btn&quot; class=&quot;btn btn-radio&quot;&amp;gt;  라디오 편성표&amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;div id=&quot;schedule-display&quot; class=&quot;display-area&quot;&amp;gt;
             &amp;lt;div id=&quot;loader&quot; class=&quot;loader hidden&quot;&amp;gt;&amp;lt;/div&amp;gt;
             &amp;lt;div id=&quot;schedule-content&quot;&amp;gt;&amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;script src=&quot;popup.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;popup.js&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778358811&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;document.addEventListener('DOMContentLoaded', () =&amp;gt; {
    const tvBtn = document.getElementById('tv-btn');
    const radioBtn = document.getElementById('radio-btn');
    const scheduleContent = document.getElementById('schedule-content');
    const loader = document.getElementById('loader');
    const initialGuide = document.getElementById('initial-guide');

    tvBtn.addEventListener('click', () =&amp;gt; fetchAndDisplaySchedule('tv'));
    radioBtn.addEventListener('click', () =&amp;gt; fetchAndDisplaySchedule('radio'));

    const formatDate = (date, format) =&amp;gt; {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        if (format === 'param') return `${year}${month}${day}`;
        return `${year}년 ${month}월 ${day}일`;
    };

    // 백그라운드 스크립트에게 데이터 요청 메시지를 보내는 함수
    async function getScheduleData(url) {
        return new Promise((resolve, reject) =&amp;gt; {
            chrome.runtime.sendMessage({ action: &quot;fetchSchedule&quot;, url: url }, (response) =&amp;gt; {
                if (chrome.runtime.lastError) {
                    reject(new Error(chrome.runtime.lastError.message));
                } else if (response &amp;amp;&amp;amp; response.error) {
                    reject(new Error(response.error));
                } else {
                    resolve(response.data);
                }
            });
        });
    }

    async function fetchAndDisplaySchedule(type) {
        scheduleContent.innerHTML = '';
        if(initialGuide) initialGuide.style.display = 'none';
        loader.classList.remove('hidden');

        try {
            const today = new Date();
            const yesterday = new Date(today);
            yesterday.setDate(today.getDate() - 1);

            const todayParam = formatDate(today, 'param');
            const yesterdayParam = formatDate(yesterday, 'param');

            const baseUrl = type === 'tv' 
                ? 'https://www.sbs.co.kr/schedule/index.html?type=tv&amp;amp;channel=SBS&amp;amp;pmDate='
                : 'https://www.sbs.co.kr/schedule/index.html?type=ra&amp;amp;channel=Power&amp;amp;pmDate=';
            
            const yesterdayData = await getScheduleData(baseUrl + yesterdayParam);
            const todayData = await getScheduleData(baseUrl + todayParam);
            
            let html = createScheduleListHtml(formatDate(yesterday), yesterdayData);
            html += createScheduleListHtml(formatDate(today), todayData);
            scheduleContent.innerHTML = html;

        } catch (error) {
            console.error(&quot;Error in fetchAndDisplaySchedule:&quot;, error);
            scheduleContent.innerHTML = `
                &amp;lt;div class=&quot;error-box&quot;&amp;gt;
                    &amp;lt;p class=&quot;error-title&quot;&amp;gt;오류: 편성표 로딩 실패&amp;lt;/p&amp;gt;
                    &amp;lt;p class=&quot;error-message&quot;&amp;gt;${error.message}. 확장 프로그램을 새로고침하거나 재설치해 보세요.&amp;lt;/p&amp;gt;
                &amp;lt;/div&amp;gt;
            `;
        } finally {
            loader.classList.add('hidden');
        }
    }

    function createScheduleListHtml(dateStr, data) {
        let listHtml = `&amp;lt;div class=&quot;date-divider&quot;&amp;gt;${dateStr}&amp;lt;/div&amp;gt;`;
        listHtml += '&amp;lt;ul class=&quot;schedule-list&quot;&amp;gt;';

        if (!data || data.length === 0) {
            listHtml += '&amp;lt;li class=&quot;no-data&quot;&amp;gt;편성 정보가 없습니다.&amp;lt;/li&amp;gt;';
        } else {
            data.forEach((item, index) =&amp;gt; {
                const startTime = item.time;
                const endTime = (index &amp;lt; data.length - 1) ? data[index + 1].time : '';
                const timeDisplay = endTime ? `${startTime} ~ ${endTime}` : `${startTime} ~`;

                listHtml += `
                    &amp;lt;li class=&quot;schedule-item&quot;&amp;gt;
                        &amp;lt;span class=&quot;time-info&quot;&amp;gt;${timeDisplay}&amp;lt;/span&amp;gt;
                        &amp;lt;span class=&quot;program-title&quot;&amp;gt;${item.program}&amp;lt;/span&amp;gt;
                    &amp;lt;/li&amp;gt;
                `;
            });
        }
        listHtml += '&amp;lt;/ul&amp;gt;';
        return listHtml;
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;style.css&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778393489&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/* 기본 스타일 */
body {
    font-family: 'Noto Sans KR', sans-serif;
    width: 400px;
    background-color: #f3f4f6; /* bg-gray-100 */
    margin: 0;
    padding: 0;
}

.container {
    padding: 1rem; /* p-4 */
}

/* 헤더 */
.header {
    display: flex;
    align-items: center;
    text-align: left;
    margin-bottom: 1rem; /* mb-4 */
}

.icon {
    height: 2.5rem; /* h-10 */
    width: 2.5rem; /* w-10 */
    margin-right: 0.75rem; /* mr-3 */
}

.title {
    font-size: 1.125rem; /* text-lg */
    font-weight: 700; /* font-bold */
    color: #1f2937; /* text-gray-800 */
}

.guide-text {
    font-size: 0.75rem; /* text-xs */
    color: #6b7280; /* text-gray-500 */
}

/* 버튼 */
.button-group {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.5rem; /* gap-2 */
    margin-bottom: 1rem; /* mb-4 */
}

.btn {
    width: 100%;
    color: white;
    font-weight: 700;
    padding: 0.5rem 1rem; /* py-2 px-4 */
    border-radius: 0.5rem; /* rounded-lg */
    border: none;
    cursor: pointer;
    transition: background-color 0.2s;
}

.btn-tv {
    background-color: #3b82f6; /* bg-blue-500 */
}
.btn-tv:hover {
    background-color: #2563eb; /* hover:bg-blue-600 */
}

.btn-radio {
    background-color: #22c55e; /* bg-green-500 */
}
.btn-radio:hover {
    background-color: #16a34a; /* hover:bg-green-600 */
}

/* 편성표 표시 영역 */
.display-area {
    margin-top: 1rem; /* mt-4 */
    text-align: left;
    min-height: 200px;
    background-color: white;
    padding: 0.75rem; /* p-3 */
    border-radius: 0.5rem; /* rounded-lg */
    box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05); /* shadow-inner */
}

.hidden {
    display: none;
}

.loader {
    border: 4px solid #f3f3f3;
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 32px;
    height: 32px;
    animation: spin 1s linear infinite;
    margin: 20px auto;
}
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

/* 편성표 목록 */
.date-divider {
    margin-top: 0.75rem; /* my-3 */
    margin-bottom: 0.75rem;
    padding: 0.375rem; /* p-1.5 */
    background-color: #f3f4f6; /* bg-gray-100 */
    color: #1f2937; /* text-gray-800 */
    font-weight: 700;
    border-radius: 0.25rem; /* rounded */
    text-align: center;
    font-size: 0.75rem; /* text-xs */
}

.schedule-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.schedule-item {
    display: flex;
    align-items: baseline;
    padding: 0.375rem; /* p-1.5 */
    border-radius: 0.25rem; /* rounded */
}
.schedule-item:hover {
    background-color: #f9fafb; /* hover:bg-gray-50 */
}

.time-info {
    font-weight: 500; /* font-medium */
    color: #2563eb; /* text-blue-600 */
    width: 6rem; /* w-24 */
    flex-shrink: 0; /* shrink-0 */
}

.program-title {
    color: #374151; /* text-gray-700 */
}

.no-data {
    color: #9ca3af; /* text-gray-400 */
    text-align: center;
    padding-top: 0.5rem; /* py-2 */
    padding-bottom: 0.5rem;
}

/* 오류 메시지 */
.error-box {
    text-align: center;
    color: #dc2626; /* text-red-600 */
    padding: 0.75rem; /* p-3 */
}
.error-title {
    font-weight: 700;
}
.error-message {
    font-size: 0.875rem; /* text-sm */
    margin-top: 0.5rem; /* mt-2 */
    color: #4b5563; /* text-gray-600 */
}

/* '보라' 태그 스타일 (신규 추가) */
.bora-tag {
    display: inline-block;
    background-color: #f97316; /* 주황색 */
    color: white;
    padding: 1px 5px;
    border-radius: 4px;
    font-size: 0.7rem;
    font-weight: 700;
    margin-left: 6px;
    vertical-align: middle; /* 텍스트와 세로 정렬을 맞춤 */
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;README.txt&lt;/blockquote&gt;
&lt;pre id=&quot;code_1753778446465&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SBS 편성표 뷰어 (크롬 확장 프로그램)
SBS TV와 파워FM 라디오의 어제, 오늘 편성표를 실시간으로 크롤링하여 보여주는 크롬 확장 프로그램입니다.

⚙️ 설치 방법 (중요)
기존에 설치된 확장 프로그램이 있다면 반드시 먼저 삭제하고 아래 순서대로 새로 설치해주세요.

1단계: 파일 준비
프로젝트는 아래와 같은 폴더 구조를 가져야 합니다. 각 파일을 해당 위치에 맞게 준비해주세요.

최종 폴더 구조:

sbs-extension/
├── images/
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
├── manifest.json
├── popup.html
├── popup.js
├── background.js
├── content-script.js
├── style.css
└── README.txt

2단계: 크롬에 확장 프로그램 설치
크롬 브라우저를 열고 주소창에 chrome://extensions 를 입력하여 확장 프로그램 관리 페이지로 이동합니다.

기존에 있던 'SBS 편성표 뷰어'가 있다면 '삭제' 버튼을 눌러 완전히 제거합니다.

페이지 오른쪽 상단에 있는 '개발자 모드(Developer mode)' 스위치를 켭니다.

'압축해제된 확장 프로그램을 로드합니다(Load unpacked)' 버튼을 클릭합니다.

파일 탐색기가 열리면, 1단계에서 생성한 sbs-extension 폴더를 선택하고 '폴더 선택' 버튼을 누릅니다.

&amp;gt; 사용 방법
설치가 완료되면 크롬 브라우저 오른쪽 상단 툴바에 아이콘이 추가됩니다.

툴바의 아이콘을 클릭하면 편성표 뷰어 팝업창이 나타납니다.

'  TV 편성표' 또는 '  라디오 편성표' 버튼을 클릭하면, 잠시 후 해당 채널의 어제와 오늘 편성표를 불러와 화면에 표시합니다.

중요사항 : 폴더와 파일 중 하나라도 없을 경우 동작하지않을 수 있습니다.

================================================================================

각 파일의 역할과 기능

  manifest.json - 확장프로그램 설정 파일
- 확장프로그램의 기본 정보 (이름, 버전, 설명)
- 권한 설정: scripting, tabs, SBS 사이트 접근 권한
- 팝업 UI와 아이콘 설정
- 백그라운드 스크립트 등록

  background.js - 백그라운드 서비스 워커
- 핵심 기능: SBS 웹사이트에서 편성표 데이터 크롤링
- 보이지 않는 탭을 생성하여 SBS 페이지 접근
- content-script를 주입하여 데이터 추출
- popup.js와 메시지 통신으로 데이터 전달
- 15초 타임아웃 설정으로 안정성 확보

  content-script.js - 웹페이지 데이터 추출
- SBS 웹페이지의 DOM에서 편성표 정보 추출
- .scheduler_program_w 클래스에서 시간과 프로그램명 파싱
- &quot;보이는 라디오&quot; 프로그램에 특별 태그 추가
- 추출된 데이터를 구조화하여 반환

  popup.html - 사용자 인터페이스
- 확장프로그램 팝업창의 HTML 구조
- TV/라디오 편성표 선택 버튼
- 로딩 스피너와 결과 표시 영역
- 깔끔한 한국어 UI 제공

⚡ popup.js - 팝업 로직 처리
- 버튼 클릭 이벤트 처리
- 어제/오늘 날짜 계산 및 URL 생성
- background.js와 메시지 통신
- 편성표 데이터를 HTML로 변환하여 표시
- 에러 처리 및 로딩 상태 관리

  style.css - 스타일시트
- 팝업창의 시각적 디자인 (400px 너비)
- 버튼, 편성표 목록, 로딩 스피너 스타일
- &quot;보라&quot; 태그 특별 스타일링
- 반응형 그리드 레이아웃

 ️ images/ - 아이콘 파일들
- icon16.png: 툴바용 작은 아이콘
- icon48.png: 중간 크기 아이콘
- icon128.png: 확장프로그램 관리 페이지용 큰 아이콘

  동작 원리
1. 사용자가 팝업에서 TV/라디오 버튼 클릭
2. popup.js가 background.js에 데이터 요청 메시지 전송
3. background.js가 SBS 사이트에 숨겨진 탭으로 접근
4. content-script.js가 웹페이지에서 편성표 데이터 추출
5. 추출된 데이터가 popup.js로 전달되어 UI에 표시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfNTpj/btsPCjo5j4K/kaeQ7U8fGLIrCkYg4GHFZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfNTpj/btsPCjo5j4K/kaeQ7U8fGLIrCkYg4GHFZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfNTpj/btsPCjo5j4K/kaeQ7U8fGLIrCkYg4GHFZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfNTpj%2FbtsPCjo5j4K%2FkaeQ7U8fGLIrCkYg4GHFZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;460&quot; height=&quot;632&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwekXr/btsPBIiD4SF/z7rQwKY364iC4gNp7KogEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwekXr/btsPBIiD4SF/z7rQwKY364iC4gNp7KogEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwekXr/btsPBIiD4SF/z7rQwKY364iC4gNp7KogEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwekXr%2FbtsPBIiD4SF%2Fz7rQwKY364iC4gNp7KogEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;460&quot; height=&quot;598&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;632&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x3xbh/btsPB1hUci5/rEIACLuh6nkxS6ZFqNKFEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x3xbh/btsPB1hUci5/rEIACLuh6nkxS6ZFqNKFEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3xbh/btsPB1hUci5/rEIACLuh6nkxS6ZFqNKFEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx3xbh%2FbtsPB1hUci5%2FrEIACLuh6nkxS6ZFqNKFEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;632&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;632&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4LiTl/btsPBgNuGQP/ZwQxjQmZzkL7V64HF5nDR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4LiTl/btsPBgNuGQP/ZwQxjQmZzkL7V64HF5nDR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4LiTl/btsPBgNuGQP/ZwQxjQmZzkL7V64HF5nDR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4LiTl%2FbtsPBgNuGQP%2FZwQxjQmZzkL7V64HF5nDR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;417&quot; height=&quot;496&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>자바스크립트</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/11</guid>
      <comments>https://ph702.tistory.com/11#entry11comment</comments>
      <pubDate>Tue, 29 Jul 2025 17:44:11 +0900</pubDate>
    </item>
    <item>
      <title>[유저 체크] 입력 패스워드 사용하는 유저 체크</title>
      <link>https://ph702.tistory.com/9</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;서버들&lt;/span&gt; 접속 후 root 계정과 solbox 계정을 함께 사용하는 리스트를&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1745547641757&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[root@DA-CC-SC02 passwd]# pwd
/home/passwdtest/script/passwd
[root@DA-CC-SC02 passwd]# ls
fail_svrlist  svrlist  updated_svrlist  user_check.py  win_svrlist&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용하는 계정명(root 와 solbox 인지) *다른 사용자라면 코딩 내용 변경할 것&lt;/li&gt;
&lt;li&gt;svrlist 내 (1.호스트명) (2.IP 주소) (3.OS 정보)가 입력되어야함
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;ex) HOST01-SERVER01&amp;nbsp; &amp;nbsp; 192.168.1.123 CentOS-6.10-6-x86_64&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;user_check.py 실행&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;중간자 값의 패스워드 입력 *pass(변경 대상 패스워드)word 해당 형식인지 확인할 것&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;pre id=&quot;code_1745547904000&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import paramiko
import socket
import sys
import os

devnull = open(os.devnull, 'w')
sys.stderr = devnull

def user_check(current_middle, ip):
    passwd_root = &quot;Cdn5&quot; + current_middle + &quot;)0&quot;        # root passwd 패턴
    passwd_solbox = &quot;Cdn3&quot; + current_middle + &quot;)0&quot;      # solbox passwd 패턴

    def try_ssh(username, password):
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        try:
            ssh.connect(ip, port=22, username=username, password=password, timeout=3)
            return ssh
        except (paramiko.AuthenticationException, paramiko.SSHException, socket.timeout, paramiko.ssh_exception.NoValidConnectionsError, paramiko.ssh_exception.SSHException):
            return None

    ssh = try_ssh(&quot;root&quot;, passwd_root)
    if ssh:
        try:
            stdin, stdout, stderr = ssh.exec_command(&quot;cat /etc/shadow | grep solbox | wc -l&quot;)
            output = stdout.read().decode().strip()
            user_info = &quot;root/solbox&quot; if output == &quot;1&quot; else &quot;root&quot;
        finally:
            ssh.close()
        return user_info

    ssh = try_ssh(&quot;solbox&quot;, passwd_solbox)
    if ssh:
        ssh.close()
        return &quot;root/solbox&quot;

    return None


def input_passwd():
    print(&quot;비밀번호 설정------------------------------------&quot;)
    current_middle = input(&quot;현재 비밀번호의 중간 부분을 입력하세요: &quot;)

    print(&quot;------------------------------------------------&quot;)
    return current_middle


def main():
    input_file = &quot;/home/passwdtest/script/passwd/svrlist&quot;
    output_file = &quot;/home/passwdtest/script/passwd/updated_svrlist&quot;
    fail_file = &quot;/home/passwdtest/script/passwd/fail_svrlist&quot;
    win_file = &quot;/home/passwdtest/script/passwd/win_svrlist&quot;

    current_middle = input(&quot;현행화 패스워드를 입력하세요 (중간자 값): &quot;)

    current_line_number = 0

    with open(input_file, 'r') as f:
        total_lines = len([line for line in f if line.strip() and not line.startswith('#')])

    with open(input_file, 'r') as infile, open(output_file, 'w') as outfile, open(fail_file, 'w') as fail_outfile, open(win_file, 'w') as win_outfile:

        for line in infile:
            if line.startswith('#') or not line.strip():
                continue
            current_line_number += 1
            parts = line.strip().split()
            if len(parts) &amp;lt; 3:
                continue

            server_name, ip, os_info = parts[:3]
            is_windows = &quot;window&quot; in os_info.lower()

            output_message = &quot;&quot;
            user_info = None
            updated_line = &quot;&quot;
            fail_line = &quot;&quot;

            if not is_windows:
                user_info = user_check(current_middle, ip)

            if is_windows:
                win_outfile.write(line)
                output_message = f&quot;{line.strip()} (Windows 서버로 분리됨)&quot;
            elif user_info:
                updated_line = f&quot;{server_name} {ip} {user_info} \&quot;{os_info}\&quot;\n&quot;
                outfile.write(updated_line)
                output_message = updated_line.strip()
            else:
                fail_line = f&quot;{line.strip()} fail\n&quot;
                fail_outfile.write(fail_line)
                output_message = fail_line.strip()

            print(f&quot;[{current_line_number}/{total_lines}] 입력 사항 : {output_message}&quot;)

if __name__ == '__main__':
    try:
        main()
    finally:
        sys.stderr.close()
        sys.stderr = original_stderr&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;378&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ka9b7/btsNxUetbwv/P6jpDKXqFveQ4Rsj8IoKn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ka9b7/btsNxUetbwv/P6jpDKXqFveQ4Rsj8IoKn0/img.png&quot; data-alt=&quot;성공 서버 : updated_svrlist, 실패 서버 : fail_svrlist, 윈도우 사용 서버 : win_svrlist&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ka9b7/btsNxUetbwv/P6jpDKXqFveQ4Rsj8IoKn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKa9b7%2FbtsNxUetbwv%2FP6jpDKXqFveQ4Rsj8IoKn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1658&quot; height=&quot;378&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;378&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;성공 서버 : updated_svrlist, 실패 서버 : fail_svrlist, 윈도우 사용 서버 : win_svrlist&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: 413px; top: 212.281px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>파이썬</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/9</guid>
      <comments>https://ph702.tistory.com/9#entry9comment</comments>
      <pubDate>Fri, 25 Apr 2025 11:31:33 +0900</pubDate>
    </item>
    <item>
      <title>sed 스크립트</title>
      <link>https://ph702.tistory.com/8</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;sed 명령어를 이용한 문장 변환 및 삭제 스크립트&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1744252604439&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;str_edit.sh&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;'str_edit.sh' 실행&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;목적에 맞는 번호 입력
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;입력 시 문장 바꾸기
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;변경 전 문자 입력&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;변경 후 문자 입력&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;입력 시 문장 삭제
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;삭제할 문자 입력&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;pre id=&quot;code_1744252821583&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

echo &quot;1 : 문자 바꾸기&quot;
echo &quot;2 : 문자 삭제&quot;
read -p &quot;목적에 맞는 번호를 선택 부탁드립니다 : &quot; choice

if [ &quot;$choice&quot; == &quot;1&quot; ]; then
    read -p &quot;변경 전 문자: &quot; search
    read -p &quot;변경 후 문자: &quot; replace
    find . -type f -exec sed -i &quot;s/${search}/${replace}/g&quot; {} +
    echo &quot;모든 파일에서 '${search}' &amp;rarr; '${replace}' 로 변경했습니다.&quot;

elif [ &quot;$choice&quot; == &quot;2&quot; ]; then
    read -p &quot;삭제할 문자: &quot; delete
    find . -type f -exec sed -i &quot;s/${delete}//g&quot; {} +
    echo &quot;모든 파일에서 '${delete}' 를 삭제했습니다.&quot;

else
        echo &quot;잘못된 입력입니다. 번호만 입력해주세요.&quot;
fi&lt;/code&gt;&lt;/pre&gt;</description>
      <category>리눅스</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/8</guid>
      <comments>https://ph702.tistory.com/8#entry8comment</comments>
      <pubDate>Thu, 10 Apr 2025 11:40:31 +0900</pubDate>
    </item>
    <item>
      <title>IP 추출 스크립트</title>
      <link>https://ph702.tistory.com/7</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;관리 용이를 위한 IP(x.x.x.x) 패턴 추출로 IP 리스트 출력&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1744250558821&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ph702@DA-CC-SC02 ip_extraction]$ ls
list  result  script&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;'list' 파일 내 IP 추출을 위한 내용 삽입&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;'script' 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;pre id=&quot;code_1744250655229&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

# 서버 목록 파일 경로
server_list_file=&quot;list&quot;

# 서버 리스트 파일에서 IP 주소만 추출
server_ips=$(grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' &quot;$server_list_file&quot;)

# IP 리스트를 결과 파일에 출력
if [[ -n &quot;$server_ips&quot; ]]; then
    echo &quot;$server_ips&quot; | sort -h | uniq &amp;gt; result
    echo &quot;IP 주소 리스트가 result 파일에 저장되었습니다.&quot;
else
    echo &quot;IP 주소를 찾을 수 없습니다.&quot;
fi&lt;/code&gt;&lt;/pre&gt;</description>
      <category>리눅스</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/7</guid>
      <comments>https://ph702.tistory.com/7#entry7comment</comments>
      <pubDate>Thu, 10 Apr 2025 11:06:30 +0900</pubDate>
    </item>
    <item>
      <title>멀티_커맨드</title>
      <link>https://ph702.tistory.com/6</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버 리스트를 통한 일괄적인 명령어 실행 후 결과값을 result 파일에 저장&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1744096045912&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ph702@DA-CC-SC02 command]$ ls
list  result  script&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;list 파일 내에 서버 IP 기입&lt;/li&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;./script '(명령어)' 입력
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;ex) ./script 'df -h'&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;pre id=&quot;code_1744096000865&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

SCRIPT_DIR=$(dirname &quot;$0&quot;)
list=&quot;$SCRIPT_DIR/list&quot;

# list 파일 확인
if [ ! -f $list ]; then
    echo &quot;list 파일을 생성해주세요.&quot;
    exit 1
fi

if [ ! -s $list ]; then
    echo &quot;list 안에 server ip를 확인해주세요.&quot;
    exit 1
fi

# 명령어 확인
if [ $# -lt 1 ]; then
    echo &quot;명령어를 확인해주세요&quot;
    exit 1
fi

COMMAND=$*

C=0
S=$(cat $list | wc -l)

# 사용자 정보
echo -n &quot;계정 ID를 입력해주세요: &quot;
read USERNAME
echo -n &quot;패스워드를 입력해주세요: &quot;
read -s PASSWORD
echo

# 결과 파일
OUTPUT_FILE=&quot;result&quot;
&amp;gt; &quot;$OUTPUT_FILE&quot;

for i in $(cat $list); do
    if [ &quot;$(echo $i | grep -c '#')&quot; -eq 0 ]; then
        C=$((C+1))
        echo &quot;====== $C / $S =======&quot;
        echo &quot;$i:$COMMAND&quot;

        echo &quot;====== $i ======&quot; &amp;gt;&amp;gt; &quot;$OUTPUT_FILE&quot;

        # SSH 실행 + 출력 제거 + 출력 기반 타임아웃 적용
        sshpass -p &quot;$PASSWORD&quot; ssh -o StrictHostKeyChecking=no -t &quot;$USERNAME@$i&quot; \
            &quot;bash -c \&quot;$COMMAND\&quot;&quot; 2&amp;gt;&amp;amp;1 \
            | grep -v &quot;Connection to&quot; \
            | (
                while IFS= read -r -t 1 line; do
                    echo &quot;$line&quot; &amp;gt;&amp;gt; &quot;$OUTPUT_FILE&quot;
                done

                # read -t 가 timeout (출력멈춤) &amp;rarr; SSH 종료
                pkill -P $$ ssh 2&amp;gt;/dev/null
            )

        echo &quot;&quot; &amp;gt;&amp;gt; &quot;$OUTPUT_FILE&quot;
        echo &quot; $i 서버의 명령어 실행 결과를 $OUTPUT_FILE에 저장하였습니다.&quot;
    fi
done

echo &quot;명령어 실행이 완료되었습니다.&quot;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>리눅스</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/6</guid>
      <comments>https://ph702.tistory.com/6#entry6comment</comments>
      <pubDate>Tue, 8 Apr 2025 16:09:11 +0900</pubDate>
    </item>
    <item>
      <title>[.py -&amp;gt; .exe] PyInstaller 모듈로 exe 변환</title>
      <link>https://ph702.tistory.com/5</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;확장자 변환&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;81&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daUEgM/btsNfHFQJVP/tHBVHGoVUk4x3GEWBo5uY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daUEgM/btsNfHFQJVP/tHBVHGoVUk4x3GEWBo5uY0/img.png&quot; data-alt=&quot;.exe 확장자 파일은 이미 실행했기에 생성된 것&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daUEgM/btsNfHFQJVP/tHBVHGoVUk4x3GEWBo5uY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaUEgM%2FbtsNfHFQJVP%2FtHBVHGoVUk4x3GEWBo5uY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;612&quot; height=&quot;81&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;81&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;.exe 확장자 파일은 이미 실행했기에 생성된 것&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;pyinstaller 모듈 설치 (최초 1회)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;
&lt;pre id=&quot;code_1744277373803&quot; class=&quot;cmake&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;pip install pyinstaller&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;명령 프롬프트 상 대상 경로의 대상 파일 변환
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li style=&quot;list-style-type: decimal; color: #000000;&quot;&gt;
&lt;pre id=&quot;code_1744277414596&quot; class=&quot;jboss-cli&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;pyinstaller --onefile (파이썬_파일명).py&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;참고 사항&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot; data-start=&quot;59&quot; data-end=&quot;73&quot;&gt;✅ 기본 옵션 모음&lt;/h3&gt;
&lt;div style=&quot;color: #333333; text-align: start;&quot;&gt;옵션설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-start=&quot;75&quot; data-end=&quot;456&quot;&gt;
&lt;tbody data-start=&quot;103&quot; data-end=&quot;456&quot;&gt;
&lt;tr data-start=&quot;103&quot; data-end=&quot;142&quot;&gt;
&lt;td&gt;--onefile&lt;/td&gt;
&lt;td&gt;모든 파일을 하나의 .exe로 만듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;143&quot; data-end=&quot;213&quot;&gt;
&lt;td&gt;--noconsole&lt;/td&gt;
&lt;td&gt;콘솔 창 없이 GUI 프로그램처럼 실행됨 (예: tkinter, PyQt 등 GUI 앱용)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;214&quot; data-end=&quot;246&quot;&gt;
&lt;td&gt;--console&lt;/td&gt;
&lt;td&gt;콘솔 창을 띄움 (기본값)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;247&quot; data-end=&quot;283&quot;&gt;
&lt;td&gt;--icon=icon.ico&lt;/td&gt;
&lt;td&gt;실행 파일 아이콘 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;284&quot; data-end=&quot;319&quot;&gt;
&lt;td&gt;--name=이름&lt;/td&gt;
&lt;td&gt;생성되는 .exe 이름 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;320&quot; data-end=&quot;374&quot;&gt;
&lt;td&gt;--add-data&lt;/td&gt;
&lt;td&gt;외부 파일이나 폴더를 함께 포함시킴 (예: 이미지, 텍스트 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;375&quot; data-end=&quot;424&quot;&gt;
&lt;td&gt;--hidden-import&lt;/td&gt;
&lt;td&gt;자동으로 감지되지 않는 모듈을 명시적으로 포함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-start=&quot;425&quot; data-end=&quot;456&quot;&gt;
&lt;td&gt;--clean&lt;/td&gt;
&lt;td&gt;빌드 전에 임시 파일들 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot; data-start=&quot;216&quot; data-end=&quot;276&quot;&gt;
&lt;li data-start=&quot;250&quot; data-end=&quot;276&quot;&gt;결과 파일은 dist 폴더에 생성됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;73&quot; data-start=&quot;59&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;73&quot; data-start=&quot;59&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;</description>
      <category>뇌</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/5</guid>
      <comments>https://ph702.tistory.com/5#entry5comment</comments>
      <pubDate>Tue, 8 Apr 2025 16:02:26 +0900</pubDate>
    </item>
    <item>
      <title>[인증 기간 확인] SSL 인증 체크 스크립트</title>
      <link>https://ph702.tistory.com/3</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #222222; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;resolve 옵션을 통해 'list' 내의&lt;span&gt;&amp;nbsp;&lt;/span&gt;IP 대상 서버들을 일괄적으로 SSL 인증 체크&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;pre id=&quot;code_1744276745714&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[ph702@DA-CC-SC02 ssl_check]$ ls
list  result  script&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;list&quot; 파일 내에 IP 주소 기입&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;sh script '도메인' '/컨텐츠' &quot; 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;pre id=&quot;code_1744276813563&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash

# 입력값 확인
if [ $# -lt 1 ]; then
    echo &quot;사용법: $0 &amp;lt;도메인&amp;gt; [/컨텐츠]&quot;
    exit 1
fi

domain=$1
content=${2:-&quot;/&quot;}  # 경로가 입력되지 않으면 기본값 &quot;/&quot;
result_file=&quot;result&quot;

# 기존 결과 파일 삭제
rm -rf &quot;$result_file&quot;

# IP 리스트 파일 확인
if [ ! -f list ]; then
    echo &quot;list 파일이 존재하지 않습니다.&quot;
    exit 1
fi

# IP 리스트를 사용하여 HTTPS 요청 수행
while read -r ip; do
    # 빈 줄 또는 주석 줄은 무시
    if [[ -z &quot;$ip&quot; || &quot;$ip&quot; =~ ^# ]]; then
        continue
    fi

    echo &quot;검사 중: ${ip}&quot;

    # HTTPS 요청 수행
    curl -vo /dev/null --resolve &quot;${domain}:443:${ip}&quot; &quot;https://${domain}${content}&quot; 2&amp;gt; stdout
    sleep 0.3

    # 인증서 만료 날짜 추출
    expire_date=$(grep -i &quot;expire date&quot; stdout)

    # 결과 저장
    {
        echo &quot;${ip}  ${domain}&quot;
        if [ -n &quot;$expire_date&quot; ]; then
            echo &quot;*  ${expire_date}&quot;
        else
            echo &quot;*  인증서 정보 없음&quot;
        fi
        echo &quot;&quot;
    } &amp;gt;&amp;gt; &quot;$result_file&quot;

done &amp;lt; list

# 임시 파일 삭제
rm -f stdout

echo &quot;검사가 완료되었습니다. 결과는 '${result_file}' 파일을 확인하세요.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;pre id=&quot;code_1744277186728&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[ph702@DA-CC-SC02 ssl_check]$ ./script
사용법: ./script &amp;lt;도메인&amp;gt; [/컨텐츠]

[ph702@DA-CC-SC02 ssl_check]$ ./script edu2.sba.kr
검사 중: 114.108.156.38
검사 중: 114.108.157.34
검사 중: 114.108.158.72
검사 중: 182.162.107.44
검사 중: 182.162.14.6
검사 중: 211.237.6.32
검사가 완료되었습니다. 결과는 'result' 파일을 확인하세요.

[ph702@DA-CC-SC02 ssl_check]$ cat result
114.108.156.38  edu2.sba.kr
*  *  expire date: Apr 10 23:59:59 2026 GMT

114.108.157.34  edu2.sba.kr
*  *  expire date: Apr 10 23:59:59 2026 GMT

114.108.158.72  edu2.sba.kr
*  *  expire date: Apr 10 23:59:59 2026 GMT

182.162.107.44  edu2.sba.kr
*  *  expire date: Apr 10 23:59:59 2026 GMT

182.162.14.6  edu2.sba.kr
*  *  expire date: Apr 10 23:59:59 2026 GMT

211.237.6.32  edu2.sba.kr
*  *  expire date: Apr 10 23:59:59 2026 GMT&lt;/code&gt;&lt;/pre&gt;</description>
      <category>리눅스</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/3</guid>
      <comments>https://ph702.tistory.com/3#entry3comment</comments>
      <pubDate>Thu, 3 Apr 2025 14:41:00 +0900</pubDate>
    </item>
    <item>
      <title>[일정 출력] 구글 캘린더 -&amp;gt; SLACK 자정 금일 일정 공유 자동화</title>
      <link>https://ph702.tistory.com/1</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 목적&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;GCP 플랫폼을 통해 구글 캘린더 일정을 SLACK&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실제 경로 내 파일&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5OODk/btsNhb7erXq/fkwLInlmtL7tO3MBr2SHm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5OODk/btsNhb7erXq/fkwLInlmtL7tO3MBr2SHm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5OODk/btsNhb7erXq/fkwLInlmtL7tO3MBr2SHm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5OODk%2FbtsNhb7erXq%2FfkwLInlmtL7tO3MBr2SHm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;101&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;101&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;선행 조건&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;실행을 위한 '인증토큰.json' 이 필요 (GCP에서 계정에 대한 토큰 발급)&lt;/li&gt;
&lt;li&gt;수신 웹후크 사이트 설정 (하기링크는 실제 사용한&amp;nbsp; url 입니다.)
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://solbox.slack.com/services/B088G67VBC5?settings=1&amp;amp;utm_source=in-prod&amp;amp;utm_medium=inprod-link_app_settings-user_card-click&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://solbox.slack.com/services/B088G67VBC5?settings=1&amp;amp;utm_source=in-prod&amp;amp;utm_medium=inprod-link_app_settings-user_card-click&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVcHIs/btsNf6loQBu/UU9FU1Xtnz9JlLSXu6bF21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVcHIs/btsNf6loQBu/UU9FU1Xtnz9JlLSXu6bF21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVcHIs/btsNf6loQBu/UU9FU1Xtnz9JlLSXu6bF21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVcHIs%2FbtsNf6loQBu%2FUU9FU1Xtnz9JlLSXu6bF21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;801&quot; height=&quot;752&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 순서&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;.py 파일 실행 (.exe 확장자 변환 시 .exe 실행)&amp;nbsp;&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;금일, 명일 def 함수 별도 생성으로 선택 실행 (main 함수 주석 처리)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;작성 내용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;schedule.py&lt;/p&gt;
&lt;pre id=&quot;code_1745547199594&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import requests
import time
import queue
import re
from datetime import datetime, timedelta, timezone
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

# --- 설정 구간 ---
# Google Calendar API 설정
SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
SERVICE_ACCOUNT_FILE = '/root/ph/schedule/charged-chain-442610-k7-7697a201c316.json'
CALENDAR_ID = 'solbox.com_dppveauh7fmtabefn11oc0gh1s@group.calendar.google.com'

# Slack Webhook URL
SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/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'

def get_todays_events():
    # 구글 캘린더 금일 일정 불러오기
    credentials = Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
    service = build('calendar', 'v3', credentials=credentials)

    # 오늘 날짜 범위 설정 (KST 시간대 사용)
    now = datetime.now(timezone(timedelta(hours=9))).replace(hour=0, minute=0, second=0, microsecond=0)
    end_of_day = datetime.now(timezone(timedelta(hours=9))).replace(hour=23, minute=59, second=59, microsecond=0)

    # KST 시간을 ISO 포맷으로 변환
    now = now.isoformat()
    end_of_day = end_of_day.isoformat()

    events_result = service.events().list(
        calendarId=CALENDAR_ID,
        timeMin=now,
        timeMax=end_of_day,
        singleEvents=True,
        orderBy='startTime'
    ).execute()
    events = events_result.get('items', [])

    return events

def format_event(event):
    # 이벤트 제목(요약)에서 CW-숫자 형태를 찾아 하이퍼링크로 변환
    event_summary = event.get('summary', 'No Title')

    # 'CW-' 뒤에 숫자가 있는 부분을 찾아 링크로 변경
    formatted_event = re.sub(r'(CW-\d+)', r'&amp;lt;https://jira.solbox.com/browse/\1|\1&amp;gt;', event_summary)

    return formatted_event

def send_to_slack_via_webhook(events):
    # 1. 메시지 본문 생성
    # 오늘 날짜 (KST 기준)
    today_date = datetime.now(timezone(timedelta(hours=9))).strftime(&quot;%Y-%m-%d&quot;)
    header = f&quot;{today_date} 예정 작업 공유 드립니다.&quot;

    if not events:
        message = f&quot;금일 예정된 일정이 없습니다.&quot;
    else:
        message = f&quot;{header}\n\n&quot;
        for event in events:
            formatted_event = format_event(event)
            message += f&quot;- {formatted_event}\n&quot;

    # 2. 큐(Queue) 초기화 및 데이터 적재
    # 전송할 데이터(payload)를 큐에 넣습니다.
    send_queue = queue.Queue()
    payload = {'text': message}
    send_queue.put(payload)

    # 3. 큐 소비 (Consumer Logic) - 안정적 전송 및 재시도 로직
    while not send_queue.empty():
        current_payload = send_queue.get()

        try:
            response = requests.post(SLACK_WEBHOOK_URL, json=current_payload)

            # 전송 성공 (200 OK)
            if response.status_code == 200:
                print(&quot;Webhook으로 슬랙 메시지가 성공적으로 전송되었습니다.&quot;)

            # 속도 제한(Rate Limit) 감지 (429 Too Many Requests) -&amp;gt; 재시도
            elif response.status_code == 429:
                # 슬랙 헤더에 'Retry-After'가 있으면 그만큼, 없으면 5초 대기
                retry_after = int(response.headers.get('Retry-After', 5))
                print(f&quot;전송 속도 제한(429) 감지! {retry_after}초 후 재시도합니다.&quot;)

                time.sleep(retry_after)         # 대기
                send_queue.put(current_payload) # 실패한 메시지를 큐에 다시 넣기 (Retry)

            # 그 외 전송 실패
            else:
                print(f&quot;슬랙 메시지 전송 실패: {response.status_code}, {response.text}&quot;)

        except Exception as e:
            print(f&quot;전송 중 예외 발생: {e}&quot;)

        # [안전장치] 큐 작업 완료 처리 및 기본 대기 (Slack 부하 방지용)
        send_queue.task_done()
        time.sleep(1)

def main():
    events = get_todays_events()
    send_to_slack_via_webhook(events)

if __name__ == &quot;__main__&quot;:
    main()&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;charged-chain-442610-k7-7697a201c316.json (토큰 파일)&lt;/p&gt;
&lt;pre id=&quot;code_1744273013217&quot; class=&quot;json&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;service_account&quot;,
  &quot;project_id&quot;: &quot;charged-chain-442610-k7&quot;,
  &quot;private_key_id&quot;: &quot;7697a201c316792aad3e8512fb3ed4af3b686f79&quot;,
  &quot;private_key&quot;: &quot;-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC68UOWT83xkR9p\n5FFmvksa7X+yfb/lUUtgddaqj1/nOjRPDJ1gMq1jgWpY5ES6X86Kv5wUEKQJ1QIP\nRKEGfxEqC+0zy28GLLOd2pX8jgtmBDl/QGS91RFQRiYBlOPWctAMlgwiS5ObU6u5\nx0/hDf5mW4LoomaCF30N3CW5ZB/Nejs5tAjw+Y36PZtPJLxKh8oyfV067YiJsPtV\nJiLmfPqoEjz36O0jL3LSaJ4P46IMnM43cnXahTs6I8rJUqxDP0g2XoPgV33X0iNP\n/m5LEQFziEZJZWlECGxvb0RojjK0X/czENnDhj3YskMsmRrVBrTyzJiDGIQWPClJ\nFSq2IPtrAgMBAAECggEAHxW2INzlSElS2WdLN9PlpjUeuj3ZtlZ1u7TsfJD5p/fQ\nMeLNmmzi0vx2hBB08y6yJ/UjjVyVAkOGo6ZMRknDv2ObTjUCIZs2RMXPkd7Gu3Bx\nZRz+g2hWCCyZ7kJlPkf6G1Wp79v0T+wyTmJ7gFc02a1Wz7sNY41nUHIuXazt8tsa\nTqJ2oINTCgUXzmxgv+pXiVcnYDjtxnI3kMiipJ4FTa2a/6Nxpym2n8AJZNoXK+yY\nEQuzwwGq8tOi3oHdw0suQ8nvtRy7Z/9/Emj8TxkzYARUHOHLc8QX65a6I3txthYa\n1utqz/qSEzYNlE8qpVD1FFgeeFXx9A7zUEUBvbvviQKBgQD9CZAgdZDZLRLKvyco\nX12Fj/9wMxnbtXSh7vNbFehBFMrmyMAJWN0QVZmECB8Y000w9aXMbHA7DcU6b4XH\nIsarGiTlEdHwaEFiM3egQPD0GeeqVnJm34afuGqWIrGUAowkYyhi+UW13eMUAT7l\nhshMjNdoN9KOV57ejuQSW/zTYwKBgQC9IZe0Nrh7i1QwAjDVHNwHxoAF+EetRD52\nk2c9Mk1h+GmJS9gna5JMdwm7v9Mxup1KIO2pDJrtcFonkyNNeQ80lcEYvSarW9Wz\nW/nimsBNB+Ir149oo3OnQNQVPlewH+zeB2CkSLXS10Z0xzyuKKGaiLiAnOugunJp\nM9juSOHqWQKBgGoyRkPpM4aCLT3cJeICzCxO+ASt3a9hI3cG4ymaMySFRna/UCFc\nI0NEua44/lwb6mye3BvEcwHF0L2qqnmd9cU/rrZY2URNbQt60Dz4pGe+K4VIzLCy\nJT0JV+p02xRkUU7AMuX++ivO2Qu/ThdkjtHZ1lnN+9dznKCJVd0CsERDAoGAVj6E\nTCSL2aJ+YGoPVI8VcuI8rPw7yzIMfcvXzxsqGFvL3FTem5M9ImtB4ACoUMv1P8Fm\nPqlF2LJcGiHJfmGO4n7Lj/lpMcjt2R0/BOtmd3n50943QhMPARzZ2VoVaHYWcGTS\n1/dkGmIaedQEwrI6hxqDb/qepCuBUqHW8UoA4vkCgYEA3D5ow0RvQmC5L7tsQk4z\nk+Yrk3A6f7TLpYeNFrz6QHju8t9PgH2f7YS+C52/FBgMESADBXEkoGzdBv/SCQC0\nk8ktmHq/7vEekZ9jDGbNfp5KCTpjNju8x7/TCkWgIFrUcymWIQD9VAzKrZWKP+5b\nDLU4q4UGEDMIWnjRrCN8XK8=\n-----END PRIVATE KEY-----\n&quot;,
  &quot;client_email&quot;: &quot;hcalender@charged-chain-442610-k7.iam.gserviceaccount.com&quot;,
  &quot;client_id&quot;: &quot;103302217323202264995&quot;,
  &quot;auth_uri&quot;: &quot;https://accounts.google.com/o/oauth2/auth&quot;,
  &quot;token_uri&quot;: &quot;https://oauth2.googleapis.com/token&quot;,
  &quot;auth_provider_x509_cert_url&quot;: &quot;https://www.googleapis.com/oauth2/v1/certs&quot;,
  &quot;client_x509_cert_url&quot;: &quot;https://www.googleapis.com/robot/v1/metadata/x509/hcalender%40charged-chain-442610-k7.iam.gserviceaccount.com&quot;,
  &quot;universe_domain&quot;: &quot;googleapis.com&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cr54a/btsNfQQC7Bq/CEw4WSNIX8wK3WhRJ0UBJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cr54a/btsNfQQC7Bq/CEw4WSNIX8wK3WhRJ0UBJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cr54a/btsNfQQC7Bq/CEw4WSNIX8wK3WhRJ0UBJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCr54a%2FbtsNfQQC7Bq%2FCEw4WSNIX8wK3WhRJ0UBJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1100&quot; height=&quot;325&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고내용&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;google console platform&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://console.cloud.google.com/apis/credentials?inv=1&amp;amp;invt=AbmeZw&amp;amp;project=charged-chain-442610-k7&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://console.cloud.google.com/apis/credentials?inv=1&amp;amp;invt=AbmeZw&amp;amp;project=charged-chain-442610-k7&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764923587923&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Google 클라우드 플랫폼&quot; data-og-description=&quot;로그인 Google 클라우드 플랫폼으로 이동&quot; data-og-host=&quot;accounts.google.com&quot; data-og-source-url=&quot;https://console.cloud.google.com/apis/credentials?inv=1&amp;amp;invt=AbmeZw&amp;amp;project=charged-chain-442610-k7&quot; data-og-url=&quot;https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fconsole.cloud.google.com%2Fapis%2Fcredentials%3Finv%3D1%26invt%3DAbmeZw%26project%3Dcharged-chain-442610-k7&amp;amp;dsh=S1010287079%3A1764923578485805&amp;amp;followup=https%3A%2F%2Fconsole.cloud.google.com%2Fapis%2Fcredentials%3Finv%3D1%26invt%3DAbmeZw%26project%3Dcharged-chain-442610-k7&amp;amp;ifkv=Ac2yZaXk28pHt-H6Kce6FP6TVTo5CRISFVdPvTYu4R57_YjmStW6rJn7aw6t_8ZG_uzoHnSRBqLNDQ&amp;amp;osid=1&amp;amp;passive=1209600&amp;amp;service=cloudconsole&amp;amp;flowName=WebLiteSignIn&amp;amp;flowEntry=ServiceLogin&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://console.cloud.google.com/apis/credentials?inv=1&amp;amp;invt=AbmeZw&amp;amp;project=charged-chain-442610-k7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://console.cloud.google.com/apis/credentials?inv=1&amp;amp;invt=AbmeZw&amp;amp;project=charged-chain-442610-k7&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Google 클라우드 플랫폼&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;로그인 Google 클라우드 플랫폼으로 이동&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;accounts.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2025-12-05 webhook 부하(Rate Limit) 방지용 큐(Queue) 형식으로 수정&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>파이썬</category>
      <author>박히응</author>
      <guid isPermaLink="true">https://ph702.tistory.com/1</guid>
      <comments>https://ph702.tistory.com/1#entry1comment</comments>
      <pubDate>Fri, 10 Jan 2025 22:27:01 +0900</pubDate>
    </item>
  </channel>
</rss>