<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발자노트</title>
    <link>https://devnote.tistory.com/</link>
    <description>devnote 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Fri, 19 Jun 2026 21:24:47 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>dev-K</managingEditor>
    <image>
      <title>개발자노트</title>
      <url>https://tistory1.daumcdn.net/tistory/8475514/attach/2ad63d134cd0475b82cf0f94c36ad0da</url>
      <link>https://devnote.tistory.com</link>
    </image>
    <item>
      <title>[데이터베이스] 2. 관계형 데이터베이스</title>
      <link>https://devnote.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;관계형 데이터베이스란,&lt;br&gt;&quot;관계있는&quot; 여러 개의 테이블을 연결해놓은 것.&lt;br&gt;&amp;nbsp;&lt;br&gt;애초에 데이터베이스라는 것이 여러 개의 테이블이다.&lt;br&gt;&lt;b&gt;이 테이블들간의 어떤 관계를 설정하여 연결하면&lt;/b&gt;&lt;br&gt;&lt;b&gt;그게 관계형 데이터베이스다. (RDB, Relationship DataBase)&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;예시로, 어떤 가게에서 주문을 한다고 가정하자.&lt;br&gt;이 상황에서 생각할 수 있는 테이블들은 아래와 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;[사람]&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 32.4419%; height: 114px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 24px;&quot;&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;MEMBER_ ID&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;이름&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 24px;&quot;&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;김철수&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 24px;&quot;&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;박짱구&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 24px;&quot;&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 16.6667%; height: 24px; text-align: center;&quot;&gt;&lt;b&gt;최유리&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 18px;&quot;&gt;&lt;td style=&quot;width: 16.6667%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;...&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 16.6667%; height: 18px; text-align: center;&quot;&gt;&lt;b&gt;...&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&amp;nbsp;&lt;br&gt;[상품]&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 72.4419%; height: 193px;&quot; border=&quot;1&quot; data-ke-style=&quot;style15&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;PRODUCT_ID&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;상품명&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;가격&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;볼펜&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;1,000원&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;연필&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;800원&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;지우개&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;500원&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;...&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;...&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt;...&lt;/b&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이 상황에서, 김철수가 볼펜 하나를 구매하는 상황을 생각해볼 수 있다.&lt;br&gt;이 문장과 동치는, 이 테이블에서 관계가 발생했다는 것,&lt;br&gt;그리고 관계형 데이터베이스를 생각해볼 수 있다는 것이다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;사람&quot; 테이블에서 &quot;MEMBER_ID&quot;가 &quot;1&quot;이고 &quot;이름&quot;이 &quot;김철수&quot;인 사람이, 
&quot;상품&quot; 테이블에서 &quot;PRODUCT_ID&quot;가 &quot;1&quot;이고 &quot;상품명&quot;이 &quot;볼펜&quot;이고, &quot;가격&quot;이 &quot;1,000원&quot;인 상품을 
&quot;주문&quot; 했다.&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;쓸데없이 말이 길게 느껴지는 이유는, 불필요한 정보가 포함되었기 때문이다.&lt;br&gt;저 표를 보면, MEMBER_ID가 1인 사람의 이름이 김철수 라는 사실을 누가 모르겠는가?&lt;br&gt;따라서 이렇게 바꿀 수 있다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;사람&quot; 테이블에서 &quot;MEMBER_ID&quot;가 &quot;1&quot;이고 &quot;이름&quot;이 &quot;김철수&quot;인 사람이, 
--&amp;gt; &quot;사람&quot; 테이블에서 &quot;MEMBER_ID&quot;가 &quot;1&quot;인 사람이&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&amp;nbsp;&lt;br&gt;이름을 뺀 이유는, 동명이인을 고려한 것이고 학창시절 출석번호를 생각하면 된다.&lt;br&gt;&amp;nbsp;&lt;br&gt;같은 맥락으로 &quot;상품&quot;테이블도, &quot;PRODUCT_ID&quot;로만 부른다면&lt;br&gt;복잡한 문장은 아래와 같이 바뀐다.&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&quot;사람&quot; 테이블에서 &quot;MEMBER_ID&quot;가 &quot;1&quot;인 사람이, 
&quot;상품&quot; 테이블에서 &quot;PRODUCT_ID&quot;가 &quot;1&quot;인 상품을 
&quot;주문&quot; 했다.&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: justify;&quot;&gt;&amp;nbsp;&lt;br&gt;여기에서 MEMBER_ID와 PRODUCT_ID는 각각 사람, 상품 테이블의 기본키이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;기본키는 다른 객체와 중복되지 않으면서&amp;nbsp;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;관계형 데이터베이스는, 두 테이블을 연결짓는 사건 또한 테이블로 정의한다.&lt;br&gt;따라서 앞서 텍스트로 표현했던 &quot;주문&quot; 이라는 사건 또한 아래 테이블로 정의할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>데이터베이스</category>
      <author>dev-K</author>
      <guid isPermaLink="true">https://devnote.tistory.com/6</guid>
      <comments>https://devnote.tistory.com/6#entry6comment</comments>
      <pubDate>Fri, 9 Jan 2026 05:11:50 +0900</pubDate>
    </item>
    <item>
      <title>[데이터베이스] 1. 데이터베이스 기본용어</title>
      <link>https://devnote.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;엑셀같은 프로그램을 자주 접하거나 공부해봤다면 입문하기 좋을 거야.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 만들 여러 프로그램들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램들은 &lt;b&gt;&lt;u&gt;기본적인 입출력, 처리/가공 동작&lt;/u&gt; &lt;/b&gt;등을 할 것이지만&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;외부에서 정보를 읽어오기도 하고,&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;또한 외부로 정보를 내보내기도 할 것이며,&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;외부 시스템을 통해 다른 사용자와의 실시간 상호작용도 지원하겠지.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 동작들을 구현하기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 외적으로 알아야하는 개념이 &lt;b&gt;네트워크와 데이터베이스&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부와 소통한다고 하는 관점에서 비유해볼 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;데이터베이스는 출발지와 목적지의 좌표&lt;/b&gt;&lt;/u&gt;이고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;네트워크는 이동수단&lt;/b&gt;&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;걸어가든 대중교통을 타고 가든, 비행기를 타고 가든간에&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어디로 가야 하는지 모르면 어떤 이동수단을 써도 의미가 없다.&lt;br /&gt;마찬가지로, 어떤 데이터를 읽고 쓰는지가 정해지지 않으면 네트워크 통신도 의미가 없다.&lt;br /&gt;그래서 데이터베이스 개념을 먼저 이해하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;✔ 단위 개념 (엑셀을 생각하면 쉽다.)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 데이터베이스 = &lt;b&gt;여러 테이블들의 집합&lt;/b&gt;이다.&lt;br /&gt;-. 테이블 = &lt;b&gt;행(row)과 열(column)로 구성된 데이터의 집합&lt;/b&gt;이다.&lt;br /&gt;-. 행(row) = &lt;b&gt;한 객체(사람, 주문, 상품 등)에 대한 하나의 기록&lt;/b&gt;이다.&lt;br /&gt;-. 열(column) = &lt;b&gt;각 기록을 구성하는 속성(이름, 전화번호, 가격 등)&lt;/b&gt;이다.&lt;br /&gt;-. 셀(cell) = &lt;b&gt;행과 열이 만나는 지점의 실제 값&lt;/b&gt;이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 스키마(schema) = &lt;b&gt;데이터베이스의 구조(테이블&amp;middot;열&amp;middot;관계 설계도)&lt;/b&gt;이다.&lt;br /&gt;-. 기본키(Primary Key) = &lt;b&gt;각 행을 고유하게 식별하는 값&lt;/b&gt;이다.&lt;br /&gt;-. 외래키(Foreign Key) = &lt;b&gt;다른 테이블의 기본키를 참조하는 열&lt;/b&gt;이다.&lt;br /&gt;-. 인덱스(index) = &lt;b&gt;검색 속도를 높이기 위한 구조&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; ✔ 확장&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;416&quot; data-start=&quot;298&quot; data-ke-size=&quot;size16&quot;&gt;-. 데이터베이스 관리 시스템(DBMS) = &lt;b&gt;데이터베이스를 생성&amp;middot;저장&amp;middot;수정&amp;middot;조회하도록 관리하는 소프트웨어 시스템&lt;/b&gt;이다.&lt;br /&gt;(ex. Oracle, MySQL, PostgreSQL, MariaDB, SQL Server 등)&lt;/p&gt;
&lt;p data-end=&quot;502&quot; data-start=&quot;418&quot; data-ke-size=&quot;size16&quot;&gt;-. 데이터(data) = &lt;b&gt;의미를 해석하기 전의 사실이나 값&lt;/b&gt;이다.&lt;br /&gt;-. 정보(information) = 데이터를 가공&amp;middot;분석하여 의미가 생긴 것이다.&lt;/p&gt;
&lt;p data-end=&quot;595&quot; data-start=&quot;504&quot; data-ke-size=&quot;size16&quot;&gt;-. 정형 데이터 = &lt;b&gt;행과 열 구조로 표현될 수 있는 데이터&lt;/b&gt;이다.&lt;br /&gt;-. 비정형 데이터 = 문서, 이미지, 영상처럼 &lt;b&gt;고정된 표 구조로 표현하기 어려운 데이터&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;692&quot; data-start=&quot;597&quot; data-ke-size=&quot;size16&quot;&gt;-. 파일(file) = 컴퓨터에 저장되는 데이터의 기본 단위이며, 데이터베이스에서는 파일 자체가 아니라 &lt;b&gt;파일의 정보(경로, 이름, 크기 등 메타데이터)를 주로 관리&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>데이터베이스</category>
      <author>dev-K</author>
      <guid isPermaLink="true">https://devnote.tistory.com/5</guid>
      <comments>https://devnote.tistory.com/5#entry5comment</comments>
      <pubDate>Wed, 7 Jan 2026 20:51:21 +0900</pubDate>
    </item>
    <item>
      <title>업무 자동화 툴 킷(Automation toolkit, A.T) - 3일차</title>
      <link>https://devnote.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ writer.py 역할&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. writer.py는 가공된 DataFrame을 결과물로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 입력 : List[pd.DataFrame] / processor에서 받음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 출력 : Excel 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ 실제 코드&lt;/p&gt;
&lt;pre id=&quot;code_1767693631988&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/pipeline/writer.py

from pathlib import Path
from typing import List

import pandas as pd

from app.utils.logger import get_logger

logger = get_logger(__name__)


def write_excel(
    dfs: List[pd.DataFrame],
    output_dir: str,
    prefix: str = &quot;result&quot;
) -&amp;gt; None:
    &quot;&quot;&quot;
    DataFrame 리스트를 Excel 파일로 저장한다.
    &quot;&quot;&quot;
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    for idx, df in enumerate(dfs, start=1):
        file_name = f&quot;{prefix}_{idx}.xlsx&quot;
        file_path = output_path / file_name

        try:
            df.to_excel(file_path, index=False)
            logger.info(f&quot;Excel 저장 완료: {file_path}&quot;)
        except Exception:
            logger.exception(f&quot;Excel 저장 실패: {file_path}&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ main.py 수정&lt;/p&gt;
&lt;pre id=&quot;code_1767693752057&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from app.pipeline.writer import write_excel

@app.command()
def run(...):
    ...
    dfs = read_input(input_path)
    processed_dfs = process_dataframes(dfs)

    write_excel(processed_dfs, output_path)

    logger.info(&quot;전체 파이프라인 완료&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4️⃣ 실행코드 및 결과&lt;/p&gt;
&lt;pre id=&quot;code_1767693869717&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python -m app.main --input-path ./examples --output-path ./output&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1417&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfrCSA/dJMcagxpP76/EwJWzWWB2m2cYE3pMVlnkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfrCSA/dJMcagxpP76/EwJWzWWB2m2cYE3pMVlnkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfrCSA/dJMcagxpP76/EwJWzWWB2m2cYE3pMVlnkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfrCSA%2FdJMcagxpP76%2FEwJWzWWB2m2cYE3pMVlnkk%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;1417&quot; height=&quot;662&quot; data-origin-width=&quot;1417&quot; data-origin-height=&quot;662&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>업무 자동화 툴 킷(Automation toolkit, A.T)</category>
      <author>dev-K</author>
      <guid isPermaLink="true">https://devnote.tistory.com/4</guid>
      <comments>https://devnote.tistory.com/4#entry4comment</comments>
      <pubDate>Tue, 6 Jan 2026 20:04:46 +0900</pubDate>
    </item>
    <item>
      <title>업무 자동화 툴 킷(Automation toolkit, A.T) - 2일차</title>
      <link>https://devnote.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;1️⃣ 테스트용 파일 준비 (examples\sample.csv)&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767519925462&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;name,age,score
Alice,30,85
Bob,25,90&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt; &lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;2️⃣&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; main.py 작성 및 연동&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767519964581&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/main.py

import typer

from app.pipeline.reader import read_input
from app.utils.logger import get_logger

app = typer.Typer()
logger = get_logger(__name__)


@app.command()
def run(
    input_path: str = typer.Option(..., help=&quot;입력 파일 또는 폴더 경로&quot;),
    output_path: str = typer.Option(&quot;./output&quot;, help=&quot;출력 경로&quot;),
):
    logger.info(&quot;자동화 파이프라인 시작&quot;)

    dataframes = read_input(input_path)

    logger.info(f&quot;총 DataFrame 개수: {len(dataframes)}&quot;)
    logger.info(&quot;작업 완료&quot;)


if __name__ == &quot;__main__&quot;:
    app()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvz501/dJMcaiorUkZ/5lJvosIVcGS8BFvzzwIXN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvz501/dJMcaiorUkZ/5lJvosIVcGS8BFvzzwIXN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvz501/dJMcaiorUkZ/5lJvosIVcGS8BFvzzwIXN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdvz501%2FdJMcaiorUkZ%2F5lJvosIVcGS8BFvzzwIXN1%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;977&quot; height=&quot;186&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;3️⃣&lt;span&gt; processor.py&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;-. 이 파일은 입력(DataFrame)을 받아서, 규칙 기반으로 가공된 DataFrame을 반환한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #222222; text-align: start;&quot;&gt;&lt;span&gt;-. 파일 입출력은 하지 않고, 순수 함수에 가깝게 작성하는 것이 원칙.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1767520676857&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/pipeline/processor.py

from typing import List

import pandas as pd

from app.utils.logger import get_logger

logger = get_logger(__name__)


def process_dataframes(dfs: List[pd.DataFrame]) -&amp;gt; List[pd.DataFrame]:
    &quot;&quot;&quot;
    여러 DataFrame을 받아 규칙 기반으로 가공한다.
    &quot;&quot;&quot;
    processed: List[pd.DataFrame] = []

    for idx, df in enumerate(dfs):
        logger.info(f&quot;DataFrame 처리 시작: index={idx}&quot;)
        try:
            processed_df = _process_single_dataframe(df)
            processed.append(processed_df)
        except Exception:
            logger.exception(f&quot;DataFrame 처리 실패: index={idx}&quot;)

    logger.info(f&quot;총 처리 완료 DataFrame 수: {len(processed)}&quot;)
    return processed


def _process_single_dataframe(df: pd.DataFrame) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    DataFrame 하나에 대한 가공 로직
    &quot;&quot;&quot;
    df = df.copy()

    df = _normalize_columns(df)
    df = _apply_rules(df)

    return df


def _normalize_columns(df: pd.DataFrame) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    컬럼명 정규화
    &quot;&quot;&quot;
    df.columns = (
        df.columns
        .str.strip()
        .str.lower()
        .str.replace(&quot; &quot;, &quot;_&quot;)
    )
    logger.debug(f&quot;정규화된 컬럼: {list(df.columns)}&quot;)
    return df


def _apply_rules(df: pd.DataFrame) -&amp;gt; pd.DataFrame:
    &quot;&quot;&quot;
    예시 규칙:
    - score &amp;gt;= 90 : grade = 'A'
    - score &amp;gt;= 80 : grade = 'B'
    - 그 외 : 'C'
    &quot;&quot;&quot;
    if &quot;score&quot; not in df.columns:
        logger.warning(&quot;score 컬럼이 없어 규칙 적용 스킵&quot;)
        return df

    def classify(score):
        if score &amp;gt;= 90:
            return &quot;A&quot;
        if score &amp;gt;= 80:
            return &quot;B&quot;
        return &quot;C&quot;

    df[&quot;grade&quot;] = df[&quot;score&quot;].apply(classify)
    logger.info(&quot;grade 컬럼 생성 완료&quot;)

    return df&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;scroe에 대한 grade 기준을 나누는 것으로 간단한 예시를 구현.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DataFrame -&amp;gt; DataFrame으로 가공한다는 큰 틀을 벗어나지 않는다면, 입력 데이터에 따른 구현방식은 수정 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4️⃣ main 함수에 processor 연결&lt;/p&gt;
&lt;pre id=&quot;code_1767521039183&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/main.py

import typer

from app.pipeline.reader import read_input
from app.utils.logger import get_logger
from app.pipeline.processor import process_dataframes

app = typer.Typer()
logger = get_logger(__name__)


@app.command()
def run(
    input_path: str = typer.Option(..., help=&quot;입력 파일 또는 폴더 경로&quot;),
    output_path: str = typer.Option(&quot;./output&quot;, help=&quot;출력 경로&quot;),
):
    logger.info(&quot;자동화 파이프라인 시작&quot;)

    dfs = read_input(input_path)
    processed_dfs = process_dataframes(dfs)
    logger.info(f&quot;가공 완료 DataFrame 수: {len(processed_dfs)}&quot;)
    logger.info(&quot;작업 완료&quot;)


if __name__ == &quot;__main__&quot;:
    app()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행결과&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4DH9w/dJMcadgleui/BksvxVJfL83fdrYHfCgv80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4DH9w/dJMcadgleui/BksvxVJfL83fdrYHfCgv80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4DH9w/dJMcadgleui/BksvxVJfL83fdrYHfCgv80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4DH9w%2FdJMcadgleui%2FBksvxVJfL83fdrYHfCgv80%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;906&quot; height=&quot;390&quot; data-origin-width=&quot;906&quot; data-origin-height=&quot;390&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>업무 자동화 툴 킷(Automation toolkit, A.T)</category>
      <author>dev-K</author>
      <guid isPermaLink="true">https://devnote.tistory.com/3</guid>
      <comments>https://devnote.tistory.com/3#entry3comment</comments>
      <pubDate>Sun, 4 Jan 2026 19:04:25 +0900</pubDate>
    </item>
    <item>
      <title>업무 자동화 툴 킷(Automation toolkit, A.T) - 1일차</title>
      <link>https://devnote.tistory.com/2</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ 업무환경 및 필수 프로그램 설치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. Windows 11 환경에서 CMD 창을 이용하여 가상환경을 조성.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 파이썬은 최신 버전이 설치되어있는 상태 (현재 기준 3.12)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 프로젝트 폴더 생성 및 이동 : 원하는 작업 경로에서&lt;/p&gt;
&lt;pre id=&quot;code_1767350985526&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mkdir automation_toolkit
cd automation_toolkit&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 가상환경 생성&lt;/p&gt;
&lt;pre id=&quot;code_1767351062348&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;python -m venv venv
venv\Scripts\activate&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 기본 패키지 설치&lt;/p&gt;
&lt;pre id=&quot;code_1767351111402&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip install pandas openpyxl httpx typer fastapi uvicorn python-dotenv&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 설치 시 모습 ( pip list 명령어 )&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uxCEA/dJMcabv5XHz/Isj6hPvKpHq5CKlqTNtk1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uxCEA/dJMcabv5XHz/Isj6hPvKpHq5CKlqTNtk1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uxCEA/dJMcabv5XHz/Isj6hPvKpHq5CKlqTNtk1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuxCEA%2FdJMcabv5XHz%2FIsj6hPvKpHq5CKlqTNtk1K%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;599&quot; height=&quot;687&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ requirements.txt 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. readme.md 파일과 함께 앞으로 모든 프로젝트의 기본이 되는 파일.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 전달/협업/배포 시 필수&lt;/p&gt;
&lt;pre id=&quot;code_1767351280876&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ 폴더 구조 생성&lt;/p&gt;
&lt;pre id=&quot;code_1767351313616&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mkdir app app\pipeline app\utils tests examples
type nul &amp;gt; app\main.py
type nul &amp;gt; app\config.py
type nul &amp;gt; app\pipeline\reader.py
type nul &amp;gt; app\pipeline\processor.py
type nul &amp;gt; app\pipeline\writer.py
type nul &amp;gt; app\utils\logger.py
type nul &amp;gt; app\utils\validator.py
type nul &amp;gt; README.md&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CxEhi/dJMb99ZlR7y/RU6HJE12TVIUGVCZi47gtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CxEhi/dJMb99ZlR7y/RU6HJE12TVIUGVCZi47gtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CxEhi/dJMb99ZlR7y/RU6HJE12TVIUGVCZi47gtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCxEhi%2FdJMb99ZlR7y%2FRU6HJE12TVIUGVCZi47gtK%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;773&quot; height=&quot;322&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4️⃣ 편집기 설치 및 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 추천 : VS Code / 이유 : 간편함. 가벼움. 가독성 Good.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 파일(File) - 폴더 열기(Open Folder) - 아까 만든 app 폴더를 한 번 클릭 후 [폴더 열기] 클릭.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5️⃣ 가장 먼저 만들 파일 : logger.py (app\utils)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 먼저 만드는 이유 : 유지보수용 핵심 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 상세 이유 : 프로그램 돌아가는 이유, 안 돌아가는 이유, 까먹었을 때 어떻게 동작했더라? &amp;rarr; 로그 확인 필수&lt;/p&gt;
&lt;pre id=&quot;code_1767352099539&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/utils/logger.py
import logging
from pathlib import Path

LOG_DIR = Path(&quot;logs&quot;)
LOG_DIR.mkdir(exist_ok=True)

def get_logger(name: str) -&amp;gt; logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)

    if not logger.handlers:
        formatter = logging.Formatter(
            &quot;[%(asctime)s] [%(levelname)s] %(name)s - %(message)s&quot;
        )

        file_handler = logging.FileHandler(LOG_DIR / &quot;app.log&quot;, encoding=&quot;utf-8&quot;)
        file_handler.setFormatter(formatter)

        console_handler = logging.StreamHandler()
        console_handler.setFormatter(formatter)

        logger.addHandler(file_handler)
        logger.addHandler(console_handler)

    return logger&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 코드 설명 : [2026-01-02 14:30:01] [INFO] reader - file loaded 이런 식으로 출력됨. 시간,레벨,이름,메시지&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6️⃣ 두 번째로 만들 파일 : reader.py (app\pipeline)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 실제 구현에 있어 순차적으로 (reader, processor, writer)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. reader.py의 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 입력 경로가 파일인지 폴더인지 판별&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 데이터를 pandas.DataFrame으로 변환(정규화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 과정 기록 (logger)&lt;/p&gt;
&lt;pre id=&quot;code_1767352929946&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# app/pipeline/reader.py

from pathlib import Path
from typing import List

import pandas as pd

from app.utils.logger import get_logger

logger = get_logger(__name__)

SUPPORTED_EXTENSIONS = {&quot;.csv&quot;, &quot;.xlsx&quot;, &quot;.xls&quot;}


def read_input(path: str) -&amp;gt; List[pd.DataFrame]:
    &quot;&quot;&quot;
    입력 경로를 받아 CSV / Excel 파일들을 읽어 DataFrame 리스트로 반환한다.
    &quot;&quot;&quot;
    input_path = Path(path)

    if not input_path.exists():
        logger.error(f&quot;입력 경로가 존재하지 않음: {input_path}&quot;)
        raise FileNotFoundError(f&quot;{input_path} not found&quot;)

    if input_path.is_file():
        logger.info(f&quot;파일 입력 감지: {input_path}&quot;)
        return [_read_file(input_path)]

    if input_path.is_dir():
        logger.info(f&quot;폴더 입력 감지: {input_path}&quot;)
        return _read_directory(input_path)

    logger.error(f&quot;지원하지 않는 입력 타입: {input_path}&quot;)
    raise ValueError(&quot;Unsupported input type&quot;)


def _read_directory(dir_path: Path) -&amp;gt; List[pd.DataFrame]:
    dataframes: List[pd.DataFrame] = []

    for file_path in dir_path.iterdir():
        if file_path.suffix.lower() in SUPPORTED_EXTENSIONS:
            try:
                logger.info(f&quot;파일 로드 중: {file_path.name}&quot;)
                df = _read_file(file_path)
                dataframes.append(df)
            except Exception as e:
                logger.exception(f&quot;파일 로드 실패: {file_path.name}&quot;)

    logger.info(f&quot;총 로드된 파일 수: {len(dataframes)}&quot;)
    return dataframes


def _read_file(file_path: Path) -&amp;gt; pd.DataFrame:
    ext = file_path.suffix.lower()

    if ext == &quot;.csv&quot;:
        df = pd.read_csv(file_path)
    elif ext in {&quot;.xlsx&quot;, &quot;.xls&quot;}:
        df = pd.read_excel(file_path)
    else:
        logger.error(f&quot;지원하지 않는 파일 형식: {file_path}&quot;)
        raise ValueError(f&quot;Unsupported file extension: {ext}&quot;)

    logger.info(
        f&quot;데이터 로드 완료: {file_path.name} &quot;
        f&quot;(rows={len(df)}, cols={len(df.columns)})&quot;
    )
    return df&lt;/code&gt;&lt;/pre&gt;</description>
      <category>업무 자동화 툴 킷(Automation toolkit, A.T)</category>
      <author>dev-K</author>
      <guid isPermaLink="true">https://devnote.tistory.com/2</guid>
      <comments>https://devnote.tistory.com/2#entry2comment</comments>
      <pubDate>Fri, 2 Jan 2026 20:23:39 +0900</pubDate>
    </item>
    <item>
      <title>파이썬 실습 프로젝트 : 업무 자동화 툴 킷(Automation toolkit, A.T)</title>
      <link>https://devnote.tistory.com/1</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;1️⃣ 시나리오&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 엑셀로 매일 정리하는 데이터가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 이 데이터를 자동으로 가공(첨삭, 필터 등) &amp;rarr; API 전송 (암호화) &amp;rarr; 리포트 생성 (출력) 하는 프로그램을 만들고자 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2️⃣ 핵심 기능 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 모든 프로그래밍 언어가 그렇듯, 입력 &amp;rarr; 처리 &amp;rarr; 출력 의 과정을 거친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 입력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. Excel / CSV 파일 (테이블로 정리된 데이터베이스)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. REST API 응답 (프로토콜, JSON)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 폴더 단위 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 데이터 정제(가공)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 규칙 기반 분류&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 병렬 처리(multiprocessing, asyncio)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 예외 자동 기록 (유지보수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✅ 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. Excel / PDF 리포트 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. DB 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 메일 발송 or 파일 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3️⃣ 기술스택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. Python 3.11&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. pandas&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. openpyxl&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. requests / httpx&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. Typer&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. logging&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. FastAPI&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4️⃣ 폴더 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;automation_toolkit/ &lt;br /&gt;├──&amp;nbsp;app/ &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;main.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;CLI&amp;nbsp;진입점 &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;config.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;설정 &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├── pipeline/&lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;reader.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;입력 &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;processor.py&amp;nbsp;#&amp;nbsp;처리 &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;writer.py&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;#&amp;nbsp;출력 &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;utils/ &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;├──&amp;nbsp;logger.py &lt;br /&gt;│&amp;nbsp;&amp;nbsp;&amp;nbsp;│&amp;nbsp;&amp;nbsp;&amp;nbsp;└──&amp;nbsp;validator.py &lt;br /&gt;├──&amp;nbsp;tests/ &lt;br /&gt;├──&amp;nbsp;examples/ &lt;br /&gt;├──&amp;nbsp;README.md &lt;br /&gt;└──&amp;nbsp;requirements.txt&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5️⃣ 구현 순서(3~4주 기준)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. Typer 기반 CLI&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 파일 입력 &amp;rarr; 처리 &amp;rarr; 출력 구조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 로그 / 예외처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. API 연동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 병렬처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. 리포트 생성 (예외 기록 등)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-. README 정리&lt;/p&gt;</description>
      <category>업무 자동화 툴 킷(Automation toolkit, A.T)</category>
      <author>dev-K</author>
      <guid isPermaLink="true">https://devnote.tistory.com/1</guid>
      <comments>https://devnote.tistory.com/1#entry1comment</comments>
      <pubDate>Fri, 2 Jan 2026 19:41:52 +0900</pubDate>
    </item>
  </channel>
</rss>