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

<channel>
	<title>웹훅 보관 - 하우인포-IT·테크</title>
	<atom:link href="https://howinfo.kr/tag/%ec%9b%b9%ed%9b%85/feed/" rel="self" type="application/rss+xml" />
	<link>https://howinfo.kr/tag/웹훅/</link>
	<description>IT·AI 자동화 &#38; 인프라 전문 블로그 (하우인포)</description>
	<lastBuildDate>Wed, 25 Feb 2026 13:12:24 +0000</lastBuildDate>
	<language>ko-KR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://howinfo.kr/wp-content/uploads/2026/02/cropped-ChatGPT-Image-2026년-2월-12일-오후-05_39_40-32x32.png</url>
	<title>웹훅 보관 - 하우인포-IT·테크</title>
	<link>https://howinfo.kr/tag/웹훅/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Orange Pi 5로 “주식/ETF 트레일링 스탑 + 손절 + 리스크 레벨” 알림봇 만들기 (Synology Chat 웹훅, 10~30분 자동 주기)_버전 두번째 full 소스</title>
		<link>https://howinfo.kr/orange-pi-5%eb%a1%9c-%ec%a3%bc%ec%8b%9d-etf-%ed%8a%b8%eb%a0%88%ec%9d%bc%eb%a7%81-%ec%8a%a4%ed%83%91-%ec%86%90%ec%a0%88-%eb%a6%ac%ec%8a%a4%ed%81%ac-%eb%a0%88%eb%b2%a8-%ec%95%8c/</link>
					<comments>https://howinfo.kr/orange-pi-5%eb%a1%9c-%ec%a3%bc%ec%8b%9d-etf-%ed%8a%b8%eb%a0%88%ec%9d%bc%eb%a7%81-%ec%8a%a4%ed%83%91-%ec%86%90%ec%a0%88-%eb%a6%ac%ec%8a%a4%ed%81%ac-%eb%a0%88%eb%b2%a8-%ec%95%8c/#respond</comments>
		
		<dc:creator><![CDATA[hong]]></dc:creator>
		<pubDate>Wed, 25 Feb 2026 13:12:23 +0000</pubDate>
				<category><![CDATA[개발·코딩]]></category>
		<category><![CDATA[ETF]]></category>
		<category><![CDATA[yfinance]]></category>
		<category><![CDATA[리스크관리]]></category>
		<category><![CDATA[손절]]></category>
		<category><![CDATA[시놀로지chat]]></category>
		<category><![CDATA[시놀로지나스]]></category>
		<category><![CDATA[오렌지파이5]]></category>
		<category><![CDATA[웹훅]]></category>
		<category><![CDATA[주식자동화]]></category>
		<category><![CDATA[트레일링스탑]]></category>
		<category><![CDATA[파이썬 자동화]]></category>
		<category><![CDATA[포트폴리오관리]]></category>
		<guid isPermaLink="false">https://howinfo.kr/?p=2104</guid>

					<description><![CDATA[<p>한 줄 요약 오렌지파이5에서 **보유 종목(주식/ETF)**을 파일로 관리하고, ※ 가격은 yfinance 기반이라 “지연 Close”로 동작합니다(실시간 아님).실시간으로 바꾸려면 추후 키움 API...</p>
<p>게시물 <a href="https://howinfo.kr/orange-pi-5%eb%a1%9c-%ec%a3%bc%ec%8b%9d-etf-%ed%8a%b8%eb%a0%88%ec%9d%bc%eb%a7%81-%ec%8a%a4%ed%83%91-%ec%86%90%ec%a0%88-%eb%a6%ac%ec%8a%a4%ed%81%ac-%eb%a0%88%eb%b2%a8-%ec%95%8c/">Orange Pi 5로 “주식/ETF 트레일링 스탑 + 손절 + 리스크 레벨” 알림봇 만들기 (Synology Chat 웹훅, 10~30분 자동 주기)_버전 두번째 full 소스</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">한 줄 요약</h2>



<p>오렌지파이5에서 **보유 종목(주식/ETF)**을 파일로 관리하고,</p>



<ul class="wp-block-list">
<li><strong>수익 최고점(peak) 대비 -X% 트레일링</strong></li>



<li><strong>매수가 대비 -Y% 손절</strong></li>



<li><strong>손절/트레일링 근접 경고</strong></li>



<li><strong>1시간 요약 / 15:35 요약 / 16:00 리포트</strong></li>



<li><strong>리스크 HIGH일 때만 30분→10분 자동 단축</strong></li>



<li><strong>다음날 자동 재감시(active=1 복구)</strong><br>를 <strong>시놀로지 Chat 웹훅으로 자동 알림</strong>해주는 파이썬 봇입니다.</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>※ 가격은 yfinance 기반이라 “지연 Close”로 동작합니다(실시간 아님).<br>실시간으로 바꾸려면 추후 키움 API 등 연동이 필요합니다.</p>
</blockquote>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="734" height="639" src="https://howinfo.kr/wp-content/uploads/2026/02/sychat1.png" alt="" class="wp-image-2105" srcset="https://howinfo.kr/wp-content/uploads/2026/02/sychat1.png 734w, https://howinfo.kr/wp-content/uploads/2026/02/sychat1-300x261.png 300w" sizes="(max-width: 734px) 100vw, 734px" /></figure>



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



<h1 class="wp-block-heading">1) 동작 개념(핵심 로직)</h1>



<h3 class="wp-block-heading">✅ 트레일링(수익 구간)</h3>



<ul class="wp-block-list">
<li>내 매수금액(평가금액 기준) 대비 최고점(peak)을 계속 갱신</li>



<li>기준선 = <code>peak × (1 - trailing%)</code></li>



<li>현재가가 기준선 이하로 내려오면 <strong>매도 알림</strong></li>
</ul>



<h3 class="wp-block-heading">✅ 가변 트레일링(수익이 커질수록 더 타이트)</h3>



<p>예: base trailing이 10%인 종목이라면</p>



<ul class="wp-block-list">
<li>수익 0~20%: 10%</li>



<li>수익 20~50%: 7% (10%×0.7)</li>



<li>수익 50% 이상: 5% (10%×0.5)</li>
</ul>



<h3 class="wp-block-heading">✅ 손절(매수가 기준)</h3>



<ul class="wp-block-list">
<li>손절선 = <code>매수금액 × (1 - stop_loss%)</code></li>



<li>현재가가 손절선 이하로 내려오면 <strong>손절 알림</strong></li>
</ul>



<h3 class="wp-block-heading">✅ 근접 경고</h3>



<ul class="wp-block-list">
<li>트레일링 기준선까지 <strong>2% 이내</strong>로 접근하면 “근접 경고”</li>



<li>손절선까지 <strong>2% 이내</strong>로 접근하면 “손절 근접 경고”</li>
</ul>



<h3 class="wp-block-heading">✅ 리스크 레벨(LOW / MEDIUM / HIGH)</h3>



<ul class="wp-block-list">
<li>Trigger(기준선 이하) / Near(근접) 종목 수</li>



<li>위험 노출 비중(Trigger+Near 종목 평가금액 합 / 전체 평가금액)<br>을 기준으로 <strong>LOW/MEDIUM/HIGH</strong> 표시<br>→ 장중 요약, 16:00 리포트에 같이 출력</li>
</ul>



<h3 class="wp-block-heading">✅ 실행 주기 자동 변경(장중만)</h3>



<ul class="wp-block-list">
<li>평소 장중: 30분</li>



<li>리스크 HIGH: 10분(더 촘촘히 감시)</li>



<li>장외: 120분(불필요 호출 절약)</li>
</ul>



<h3 class="wp-block-heading">✅ 다음날 자동 재활성화</h3>



<ul class="wp-block-list">
<li>매도/손절 알림 후 active=0으로 자동 비활성화(옵션)</li>



<li>다음날 09:01에 자동으로 active=1로 복구(옵션)</li>
</ul>



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



<h1 class="wp-block-heading">2) 폴더 구성(그대로 만들기)</h1>



<p>아래 3개 파일만 있으면 됩니다.</p>



<pre class="wp-block-preformatted">jusik_top10/<br> ├─ main.py<br> ├─ config.json<br> ├─ positions.csv<br> └─ state.json   (없으면 자동 생성됨, 처음엔 {} 권장)</pre>



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



<h1 class="wp-block-heading">3) 설치(오렌지파이5)</h1>



<pre class="wp-block-preformatted">sudo apt update<br>sudo apt install -y python3-pip<br>python3 -m pip install --user requests yfinance</pre>



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



<h1 class="wp-block-heading">4) config.json (복붙용)</h1>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><code>synology_chat_webhook_url</code>만 본인 값으로 바꿔주세요.</p>
</blockquote>



<pre class="wp-block-preformatted">{<br>  "synology_chat_webhook_url": "여기에_시놀로지챗_웹훅_URL_붙여넣기",  "default_trailing_pct": 0.10,<br>  "default_stop_loss_pct": 0.08,  "market_open": "09:00",<br>  "market_close": "15:30",  "startup_notify_enabled": true,  "near_alert_enabled": true,<br>  "near_alert_distance_pct": 2.0,<br>  "near_alert_cooldown_minutes": 60,  "stoploss_near_enabled": true,<br>  "stoploss_near_distance_pct": 2.0,<br>  "stoploss_near_cooldown_minutes": 180,  "auto_disable_on_alert": true,  "hourly_summary_enabled": true,<br>  "hourly_summary_interval_minutes": 60,  "daily_summary_time": "15:35",  "after_close_summary_enabled": true,<br>  "after_close_summary_time": "16:00",<br>  "after_close_include_inactive": false,  "adaptive_trailing_enabled": true,<br>  "adaptive_trailing_factors": [<br>    { "min_profit_pct": 0,  "factor": 1.0 },<br>    { "min_profit_pct": 20, "factor": 0.7 },<br>    { "min_profit_pct": 50, "factor": 0.5 }<br>  ],  "risk_high_near_count": 3,<br>  "risk_high_near_exposure_pct": 30,  "adaptive_interval_enabled": true,<br>  "interval_minutes_market": 30,<br>  "interval_minutes_high_risk": 10,<br>  "interval_minutes_off_market": 120,  "daily_reactivate_enabled": true,<br>  "daily_reactivate_time": "09:01"<br>}</pre>



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



<h1 class="wp-block-heading">5) positions.csv (복붙용)</h1>



<ul class="wp-block-list">
<li><code>market</code>: KS(코스피), KQ(코스닥)</li>



<li><code>trailing_pct</code>: 0.10 = 10%</li>



<li><code>stop_loss_pct</code>: 0.08 = -8%</li>



<li><code>active</code>: 1(감시), 0(감시중지)</li>
</ul>



<pre class="wp-block-preformatted">code,name,market,qty,buy_price,trailing_pct,stop_loss_pct,active<br>005930,삼성전자,KS,10,70000,0.10,0.08,1<br>069500,KODEX 200,KS,20,35000,0.08,0.07,1</pre>



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



<h1 class="wp-block-heading">6) state.json 초기화(권장)</h1>



<p>처음 적용할 때 state가 꼬였으면 초기화가 안전합니다.</p>



<pre class="wp-block-preformatted">echo "{}" &gt; state.json</pre>



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



<h1 class="wp-block-heading">7) main.py (전체 소스 — 그대로 복붙)</h1>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>아래 코드를 <code>main.py</code>로 저장하세요.</p>
</blockquote>



<pre class="wp-block-preformatted"># (아래 전체 코드 그대로 복붙)<br>"""<br>Orange Pi 5 (Ubuntu/ARM) 주식/ETF 트레일링 스탑 알림 시스템 (yfinance 기반, 지연 Close OK)[핵심 기능]<br>- positions.csv 관리: 종목/수량/매수가/trailing_pct/stop_loss_pct/active<br>- state.json 유지: peak_value, last_alerted_peak, 근접 경고 시각, 요약 발송 기록 등<br>- Synology Chat Incoming Webhook으로 알림 전송[장중(09:00~15:30) 기능]<br>1) 손절(매수가 기준) 알림: entry*(1-stop_loss_pct) 이하<br>2) 손절 근접 경고: 손절선까지 +X% 남으면(0% 이하면 손절)<br>3) 트레일링(수익구간) 알림: peak*(1-eff_trailing) 이하<br>4) 트레일링 근접 경고: 기준선까지 +X% 남으면<br>5) 1시간 요약(장중): 총 수익률 + 종목별 요약 + 리스크 레벨[장마감 이후]<br>- 15:35 일일 요약(TOP3/WORST3)<br>- 16:00 리포트(섹션 분리: TRIGGER/NEAR/SAFE + 손절 정보 + 리스크)[추가 보강]<br>- 가변 트레일링(Adaptive Trailing): base_trailing_pct × factor(수익구간별)<br>- 리스크 HIGH일 때 장중 주기 자동 단축(예: 30→10분)<br>- 매도/손절 알림 후 auto_disable_on_alert=true면 active=0 자동 변경<br>- 다음날 지정 시간에 active=0 → active=1 자동 재활성화(옵션)<br>- state.json 깨짐 자동복구 + 원자저장<br>- positions.csv 원자저장<br>- 시작 1회 안내 메시지[주의]<br>- yfinance는 지연 Close 기반(실시간 아님)<br>"""import os<br>import json<br>import time<br>import csv<br>import tempfile<br>from dataclasses import dataclass<br>from datetime import datetime, time as dtimeimport requests<br>import yfinance as yfBASE = os.path.dirname(os.path.abspath(__file__))<br>CONFIG_PATH = os.path.join(BASE, "config.json")<br>POSITIONS_PATH = os.path.join(BASE, "positions.csv")<br>STATE_PATH = os.path.join(BASE, "state.json")def log(step: str, msg: str = "", level: str = "INFO"):<br>    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")<br>    if msg:<br>        print(f"[{now}] [{level}] [{step}] {msg}")<br>    else:<br>        print(f"[{now}] [{level}] [{step}]")def fmt_money(x) -&gt; str:<br>    return f"{x:,.0f}"def parse_hhmm(s: str) -&gt; dtime:<br>    hh, mm = s.split(":")<br>    return dtime(hour=int(hh), minute=int(mm))def is_market_open(now: datetime, open_t: dtime, close_t: dtime) -&gt; bool:<br>    t = now.time()<br>    return open_t &lt;= t &lt;= close_tdef load_json_safe(path, default):<br>    if not os.path.exists(path):<br>        return default<br>    try:<br>        with open(path, "r", encoding="utf-8") as f:<br>            return json.load(f)<br>    except json.JSONDecodeError as e:<br>        ts = datetime.now().strftime("%Y%m%d_%H%M%S")<br>        bak = f"{path}.broken_{ts}"<br>        try:<br>            os.replace(path, bak)<br>        except:<br>            try:<br>                import shutil<br>                shutil.copy2(path, bak)<br>            except:<br>                pass<br>        print(f"[WARN] JSON이 깨져 자동 복구했습니다. 백업: {bak} / 오류: {e}")<br>        return defaultdef save_json_atomic(path, data):<br>    tmp_dir = os.path.dirname(path) or "."<br>    fd, tmp_path = tempfile.mkstemp(prefix="state_", suffix=".json", dir=tmp_dir)<br>    os.close(fd)<br>    try:<br>        with open(tmp_path, "w", encoding="utf-8") as f:<br>            json.dump(data, f, ensure_ascii=False, indent=2)<br>        os.replace(tmp_path, path)<br>    finally:<br>        if os.path.exists(tmp_path):<br>            try:<br>                os.remove(tmp_path)<br>            except:<br>                passdef parse_float_optional(x):<br>    if x is None:<br>        return None<br>    s = str(x).strip()<br>    if s == "":<br>        return None<br>    return float(s)def parse_int_optional(x, default=1):<br>    if x is None:<br>        return default<br>    s = str(x).strip()<br>    if s == "":<br>        return default<br>    try:<br>        return int(float(s))<br>    except:<br>        return default@dataclass<br>class Position:<br>    code: str<br>    name: str<br>    market: str<br>    qty: float<br>    buy_price: float<br>    trailing_pct: float<br>    stop_loss_pct: float<br>    active: int    @property<br>    def ticker(self) -&gt; str:<br>        return f"{self.code}.{self.market.upper()}"def load_positions_csv(path, default_trailing_pct: float, default_stop_loss_pct: float) -&gt; list[Position]:<br>    if not os.path.exists(path):<br>        raise RuntimeError(f"positions.csv가 없습니다: {path}")    positions = []<br>    with open(path, "r", encoding="utf-8-sig") as f:<br>        reader = csv.DictReader(f)<br>        required = {"code", "qty", "buy_price"}<br>        if not required.issubset(set(reader.fieldnames or [])):<br>            raise RuntimeError(f"positions.csv 컬럼이 필요합니다: {sorted(required)}")        for row in reader:<br>            code = row["code"].strip()<br>            name = (row.get("name") or code).strip()<br>            market = (row.get("market") or "KS").strip().upper()<br>            if market not in ("KS", "KQ"):<br>                raise RuntimeError(f"{code} market 값 오류: {market} (KS/KQ만 허용)")            qty = float(row["qty"])<br>            buy_price = float(row["buy_price"])            trailing_pct = parse_float_optional(row.get("trailing_pct"))<br>            if trailing_pct is None:<br>                trailing_pct = default_trailing_pct<br>            if not (0 &lt; trailing_pct &lt; 1):<br>                raise RuntimeError(f"{code} trailing_pct 비정상: {trailing_pct}")            stop_loss_pct = parse_float_optional(row.get("stop_loss_pct"))<br>            if stop_loss_pct is None:<br>                stop_loss_pct = default_stop_loss_pct<br>            if not (0 &lt;= stop_loss_pct &lt; 1):<br>                raise RuntimeError(f"{code} stop_loss_pct 비정상: {stop_loss_pct}")            active = parse_int_optional(row.get("active"), default=1)<br>            active = 1 if active != 0 else 0            positions.append(Position(code, name, market, qty, buy_price, trailing_pct, stop_loss_pct, active))<br>    return positionsdef save_positions_csv_atomic(path, positions: list[Position]):<br>    fieldnames = ["code", "name", "market", "qty", "buy_price", "trailing_pct", "stop_loss_pct", "active"]<br>    tmp_dir = os.path.dirname(path) or "."<br>    fd, tmp_path = tempfile.mkstemp(prefix="positions_", suffix=".csv", dir=tmp_dir)<br>    os.close(fd)<br>    try:<br>        with open(tmp_path, "w", encoding="utf-8-sig", newline="") as f:<br>            w = csv.DictWriter(f, fieldnames=fieldnames)<br>            w.writeheader()<br>            for p in positions:<br>                w.writerow({<br>                    "code": p.code, "name": p.name, "market": p.market,<br>                    "qty": p.qty, "buy_price": p.buy_price,<br>                    "trailing_pct": p.trailing_pct, "stop_loss_pct": p.stop_loss_pct,<br>                    "active": p.active<br>                })<br>        os.replace(tmp_path, path)<br>    finally:<br>        if os.path.exists(tmp_path):<br>            try: os.remove(tmp_path)<br>            except: passdef send_synology_chat(webhook_url: str, text: str):<br>    payload_json = json.dumps({"text": text}, ensure_ascii=False)<br>    r = requests.post(webhook_url, data={"payload": payload_json}, timeout=10)<br>    r.raise_for_status()def get_last_price_yf(ticker: str) -&gt; float:<br>    hist = yf.Ticker(ticker).history(period="7d")<br>    if hist is None or hist.empty:<br>        raise RuntimeError(f"가격 데이터 비어있음: {ticker}")<br>    return float(hist["Close"].iloc[-1])def get_adaptive_factor(cfg: dict, peak_profit_pct: float) -&gt; float:<br>    if not bool(cfg.get("adaptive_trailing_enabled", True)):<br>        return 1.0<br>    tiers = cfg.get("adaptive_trailing_factors", [])<br>    if not isinstance(tiers, list) or len(tiers) == 0:<br>        return 1.0    valid = []<br>    for t in tiers:<br>        try:<br>            m = float(t.get("min_profit_pct", 0))<br>            f = float(t.get("factor", 1.0))<br>            if f &gt; 0:<br>                valid.append((m, f))<br>        except:<br>            pass    if not valid:<br>        return 1.0    valid.sort(key=lambda x: x[0])<br>    chosen = valid[0][1]<br>    for m, f in valid:<br>        if peak_profit_pct &gt;= m:<br>            chosen = f<br>        else:<br>            break<br>    return chosendef effective_trailing_pct(cfg: dict, base_trailing_pct: float, entry_value: float, peak_value: float) -&gt; float:<br>    if entry_value &lt;= 0:<br>        return base_trailing_pct<br>    peak_profit_pct = (peak_value - entry_value) / entry_value * 100.0<br>    factor = get_adaptive_factor(cfg, peak_profit_pct)<br>    eff = base_trailing_pct * factor<br>    eff = max(eff, 0.01)<br>    eff = min(eff, base_trailing_pct)<br>    return effdef cleanup_state(state: dict, active_codes: set[str]):<br>    for k in list(state.keys()):<br>        if k in ("_summary", "_hourly_summary", "_startup", "_after_close_summary", "_reactivate"):<br>            continue<br>        if k not in active_codes:<br>            del state[k]def should_send_daily_summary(state: dict, now: datetime, summary_time: dtime) -&gt; bool:<br>    if now.time() &lt; summary_time:<br>        return False<br>    today = now.strftime("%Y-%m-%d")<br>    s = state.setdefault("_summary", {})<br>    return s.get("last_date") != todaydef should_send_hourly_summary_market_only(state: dict, now: datetime, interval_minutes: int, market_open: bool) -&gt; bool:<br>    if not market_open:<br>        return False<br>    hourly = state.setdefault("_hourly_summary", {})<br>    last_str = hourly.get("last_sent")<br>    if not last_str:<br>        return True<br>    last_dt = datetime.strptime(last_str, "%Y-%m-%d %H:%M:%S")<br>    diff_min = (now - last_dt).total_seconds() / 60.0<br>    return diff_min &gt;= interval_minutesdef should_send_after_close_summary(state: dict, now: datetime, summary_time: dtime) -&gt; bool:<br>    if now.time() &lt; summary_time:<br>        return False<br>    today = now.strftime("%Y-%m-%d")<br>    s = state.setdefault("_after_close_summary", {})<br>    return s.get("last_date") != todaydef should_send_startup_notify(state: dict) -&gt; bool:<br>    st = state.setdefault("_startup", {})<br>    return not bool(st.get("sent", False))def mark_startup_notify_sent(state: dict):<br>    st = state.setdefault("_startup", {})<br>    st["sent"] = True<br>    st["sent_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")def should_reactivate_today(state: dict, now: datetime, hhmm: str) -&gt; bool:<br>    t = parse_hhmm(hhmm)<br>    if now.time() &lt; t:<br>        return False<br>    today = now.strftime("%Y-%m-%d")<br>    s = state.setdefault("_reactivate", {})<br>    return s.get("last_date") != todaydef reactivate_all_positions(positions_all: list[Position], state: dict) -&gt; bool:<br>    changed = False<br>    for p in positions_all:<br>        if p.active == 0:<br>            p.active = 1<br>            changed = True<br>    if changed:<br>        state.setdefault("_reactivate", {})["last_date"] = datetime.now().strftime("%Y-%m-%d")<br>    return changeddef calc_risk_level(detail_list: list[dict], total_current_active: float, cfg: dict, near_dist: float) -&gt; dict:<br>    trigger = [d for d in detail_list if d.get("active") == 1 and d.get("to_stop", 999) &lt;= 0]<br>    near = [d for d in detail_list if d.get("active") == 1 and 0 &lt; d.get("to_stop", 999) &lt;= near_dist]    trigger_cnt = len(trigger)<br>    near_cnt = len(near)    risk_exposure = sum(float(d.get("cur", 0.0)) for d in (trigger + near))<br>    exposure_pct = (risk_exposure / total_current_active * 100.0) if total_current_active &gt; 0 else 0.0    high_near_cnt = int(cfg.get("risk_high_near_count", 3))<br>    high_exposure_pct = float(cfg.get("risk_high_near_exposure_pct", 30))    if trigger_cnt &gt; 0 or near_cnt &gt;= high_near_cnt or exposure_pct &gt;= high_exposure_pct:<br>        level = "HIGH"<br>    elif near_cnt &gt; 0:<br>        level = "MEDIUM"<br>    else:<br>        level = "LOW"    return {<br>        "level": level,<br>        "trigger_cnt": trigger_cnt,<br>        "near_cnt": near_cnt,<br>        "risk_exposure": risk_exposure,<br>        "exposure_pct": exposure_pct<br>    }def top_worst_lines(rows, n=3):<br>    rows_desc = sorted(rows, key=lambda r: r["rate_now"], reverse=True)[:n]<br>    rows_asc = sorted(rows, key=lambda r: r["rate_now"], reverse=False)[:n]    def line(r):<br>        sign = "+" if r["profit_now"] &gt;= 0 else ""<br>        return f"- {r['name']}({r['code']}): {r['rate_now']:+.2f}% ({sign}{fmt_money(r['profit_now'])}원)"    return [line(r) for r in rows_desc], [line(r) for r in rows_asc]def run_once(cfg: dict, positions_all: list[Position], state: dict):<br>    now = datetime.now()<br>    now_str = now.strftime("%Y-%m-%d %H:%M:%S")    webhook_url = cfg["synology_chat_webhook_url"]    market_open_t = parse_hhmm(cfg.get("market_open", "09:00"))<br>    market_close_t = parse_hhmm(cfg.get("market_close", "15:30"))<br>    daily_summary_t = parse_hhmm(cfg.get("daily_summary_time", "15:35"))    hourly_enabled = bool(cfg.get("hourly_summary_enabled", True))<br>    hourly_interval = int(cfg.get("hourly_summary_interval_minutes", 60))    auto_disable = bool(cfg.get("auto_disable_on_alert", True))    near_enabled = bool(cfg.get("near_alert_enabled", True))<br>    near_dist = float(cfg.get("near_alert_distance_pct", 2.0))<br>    near_cooldown = int(cfg.get("near_alert_cooldown_minutes", 60))    sl_near_enabled = bool(cfg.get("stoploss_near_enabled", True))<br>    sl_near_dist = float(cfg.get("stoploss_near_distance_pct", 2.0))<br>    sl_near_cooldown = int(cfg.get("stoploss_near_cooldown_minutes", 180))    after_close_enabled = bool(cfg.get("after_close_summary_enabled", True))<br>    after_close_time = parse_hhmm(cfg.get("after_close_summary_time", "16:00"))<br>    include_inactive = bool(cfg.get("after_close_include_inactive", False))    positions_active = [p for p in positions_all if p.active == 1]<br>    active_codes = {p.code for p in positions_active}<br>    cleanup_state(state, active_codes)    market_open = is_market_open(now, market_open_t, market_close_t)<br>    log("MARKET", f"장중={market_open}")    price_targets = positions_all if include_inactive else positions_active<br>    price_cache: dict[str, float] = {}    log("PRICE", f"조회 대상={len(price_targets)}")<br>    for p in price_targets:<br>        if p.code in price_cache:<br>            continue<br>        try:<br>            price_cache[p.code] = get_last_price_yf(p.ticker)<br>        except Exception as e:<br>            log("PRICE", f"FAIL {p.name}({p.code}) {e}", level="WARN")    positions_ok = [p for p in positions_active if p.code in price_cache]<br>    if not positions_ok:<br>        return False, "LOW", market_open    total_entry = total_current = total_peak = 0.0<br>    rows = []    for p in positions_ok:<br>        entry_value = p.qty * p.buy_price<br>        current_value = p.qty * price_cache[p.code]<br>        s = state.setdefault(p.code, {})        peak_value = float(s.get("peak_value", entry_value))<br>        if current_value &gt; peak_value:<br>            peak_value = current_value<br>        s["peak_value"] = peak_value        profit_now = current_value - entry_value<br>        rate_now = (profit_now / entry_value * 100.0) if entry_value else 0.0        total_entry += entry_value<br>        total_current += current_value<br>        total_peak += peak_value        rows.append({"code": p.code, "name": p.name, "profit_now": profit_now, "rate_now": rate_now})    total_profit_now = total_current - total_entry<br>    total_rate_now = (total_profit_now / total_entry * 100.0) if total_entry else 0.0    changed_positions = False<br>    if market_open:<br>        pos_map = {p.code: p for p in positions_all}        for p in positions_ok:<br>            entry_value = p.qty * p.buy_price<br>            current_value = p.qty * price_cache[p.code]<br>            s = state.setdefault(p.code, {})<br>            peak_value = float(s.get("peak_value", entry_value))            # 손절 근접 + 손절 트리거<br>            if p.stop_loss_pct &gt; 0:<br>                stop_loss_value = entry_value * (1 - p.stop_loss_pct)<br>                to_stoploss = (current_value - stop_loss_value) / stop_loss_value * 100.0 if stop_loss_value &gt; 0 else None                if sl_near_enabled and to_stoploss is not None and (0 &lt; to_stoploss &lt;= sl_near_dist):<br>                    ns = state.setdefault(p.code, {})<br>                    last_sl_near = ns.get("last_stoploss_near_alert_at")<br>                    can_send = True<br>                    if last_sl_near:<br>                        last_dt = datetime.strptime(last_sl_near, "%Y-%m-%d %H:%M:%S")<br>                        can_send = (now - last_dt).total_seconds() &gt;= sl_near_cooldown * 60<br>                    if can_send:<br>                        msg = (<br>                            f"[손절 근접 경고/매수가 -{int(p.stop_loss_pct*100)}%]\n"<br>                            f"- {p.name}({p.code})\n"<br>                            f"- 손절선까지 거리: {to_stoploss:+.2f}% (0% 이하이면 손절)\n"<br>                            f"- 최근가(지연 Close): {fmt_money(price_cache[p.code])}원\n"<br>                            f"- 손절선: {fmt_money(stop_loss_value)}원\n"<br>                            f"- 평가금액: {fmt_money(current_value)}원\n"<br>                            f"- 시각: {now_str}"<br>                        )<br>                        send_synology_chat(webhook_url, msg)<br>                        ns["last_stoploss_near_alert_at"] = now_str                stop_loss_triggered = current_value &lt;= stop_loss_value<br>                last_sl = s.get("last_stop_loss_alert_at")<br>                already_today = bool(last_sl and last_sl[:10] == now_str[:10])<br>                if stop_loss_triggered and not already_today:<br>                    msg = (<br>                        f"[손절 알림/매수가 -{int(p.stop_loss_pct*100)}%]\n"<br>                        f"- {p.name}({p.code})\n"<br>                        f"- 최근가(지연 Close): {fmt_money(price_cache[p.code])}원\n"<br>                        f"- 평가금액: {fmt_money(current_value)}원\n"<br>                        f"- 손절선: {fmt_money(stop_loss_value)}원\n"<br>                        f"- 매수금액: {fmt_money(entry_value)}원\n"<br>                        f"- 시각: {now_str}"<br>                    )<br>                    send_synology_chat(webhook_url, msg)<br>                    s["last_stop_loss_alert_at"] = now_str<br>                    if auto_disable and pos_map[p.code].active != 0:<br>                        pos_map[p.code].active = 0<br>                        changed_positions = True            # 트레일링 근접 + 트리거<br>            last_alerted_peak = float(s.get("last_alerted_peak", 0))<br>            if peak_value &gt; entry_value:<br>                eff_trailing = effective_trailing_pct(cfg, p.trailing_pct, entry_value, peak_value)<br>                stop_value = peak_value * (1 - eff_trailing)<br>                to_stop = (current_value - stop_value) / stop_value * 100.0<br>                drop_from_peak = (peak_value - current_value) / peak_value * 100.0<br>                triggered = current_value &lt;= stop_value                if near_enabled and (0 &lt; to_stop &lt;= near_dist):<br>                    ns = state.setdefault(p.code, {})<br>                    last_near = ns.get("last_near_alert_at")<br>                    can_send = True<br>                    if last_near:<br>                        last_dt = datetime.strptime(last_near, "%Y-%m-%d %H:%M:%S")<br>                        can_send = (now - last_dt).total_seconds() &gt;= near_cooldown * 60<br>                    if can_send:<br>                        msg = (<br>                            f"[근접 경고/트레일링 {int(eff_trailing*100)}%]\n"<br>                            f"- {p.name}({p.code})\n"<br>                            f"- 기준선까지 거리: {to_stop:+.2f}% (0% 이하이면 트리거)\n"<br>                            f"- 피크 대비 하락률: {drop_from_peak:.2f}%\n"<br>                            f"- 최근가(지연 Close): {fmt_money(price_cache[p.code])}원\n"<br>                            f"- 시각: {now_str}"<br>                        )<br>                        send_synology_chat(webhook_url, msg)<br>                        ns["last_near_alert_at"] = now_str                if triggered and last_alerted_peak &lt; peak_value:<br>                    msg = (<br>                        f"[매도 알림/트레일링 {int(eff_trailing*100)}%]\n"<br>                        f"- {p.name}({p.code})\n"<br>                        f"- 기준선까지 거리: {to_stop:+.2f}% (트리거)\n"<br>                        f"- 피크 대비 하락률: {drop_from_peak:.2f}%\n"<br>                        f"- 최근가(지연 Close): {fmt_money(price_cache[p.code])}원\n"<br>                        f"- 시각: {now_str}"<br>                    )<br>                    send_synology_chat(webhook_url, msg)<br>                    s["last_alerted_peak"] = peak_value<br>                    if auto_disable and pos_map[p.code].active != 0:<br>                        pos_map[p.code].active = 0<br>                        changed_positions = True    # 리스크/리포트용 상세 리스트<br>    positions_for_report = positions_all if include_inactive else positions_active<br>    detail_list = []<br>    for p in positions_for_report:<br>        if p.code not in price_cache:<br>            continue<br>        entry_value = p.qty * p.buy_price<br>        current_value = p.qty * price_cache[p.code]<br>        s = state.get(p.code, {})<br>        peak_value = float(s.get("peak_value", entry_value))<br>        eff_trailing = effective_trailing_pct(cfg, p.trailing_pct, entry_value, peak_value) if peak_value &gt; entry_value else p.trailing_pct<br>        stop_value = peak_value * (1 - eff_trailing) if peak_value else 0.0<br>        to_stop = (current_value - stop_value) / stop_value * 100.0 if stop_value else 0.0        stop_loss_value = entry_value * (1 - p.stop_loss_pct) if p.stop_loss_pct &gt; 0 else 0.0<br>        to_stoploss = (current_value - stop_loss_value) / stop_loss_value * 100.0 if stop_loss_value &gt; 0 else None        profit_now = current_value - entry_value<br>        rate_now = (profit_now / entry_value * 100.0) if entry_value else 0.0        detail_list.append({<br>            "name": p.name, "code": p.code, "active": p.active,<br>            "cur": current_value, "entry": entry_value,<br>            "profit": profit_now, "rate": rate_now,<br>            "peak": peak_value, "eff_trailing": eff_trailing,<br>            "stop": stop_value, "to_stop": to_stop,<br>            "stop_loss_pct": p.stop_loss_pct, "stop_loss_value": stop_loss_value, "to_stoploss": to_stoploss<br>        })    risk = calc_risk_level(detail_list, total_current, cfg, near_dist)    # 장중 1시간 요약<br>    if bool(cfg.get("hourly_summary_enabled", True)):<br>        if should_send_hourly_summary_market_only(state, now, int(cfg.get("hourly_summary_interval_minutes", 60)), market_open):<br>            lines = []<br>            for r in rows:<br>                sign = "+" if r["profit_now"] &gt;= 0 else ""<br>                lines.append(f"- {r['name']}({r['code']}): {r['rate_now']:+.2f}% ({sign}{fmt_money(r['profit_now'])}원)")            risk_box = (<br>                f"[리스크 레벨: {risk['level']}]\n"<br>                f"- Trigger: {risk['trigger_cnt']} / Near: {risk['near_cnt']}\n"<br>                f"- 위험노출: {fmt_money(risk['risk_exposure'])}원 ({risk['exposure_pct']:.1f}%)\n"<br>            )            msg = (<br>                f"[1시간 포트폴리오 요약]\n"<br>                f"- 총 손익: {fmt_money(total_profit_now)}원\n"<br>                f"- 총 수익률: {total_rate_now:+.2f}%\n\n"<br>                + risk_box + "\n"<br>                + "\n".join(lines) +<br>                f"\n\n- 시각: {now_str}"<br>            )<br>            send_synology_chat(webhook_url, msg)<br>            state.setdefault("_hourly_summary", {})["last_sent"] = now_str    # 15:35 일일 요약<br>    if should_send_daily_summary(state, now, daily_summary_t):<br>        total_profit_peak = total_peak - total_entry<br>        total_rate_peak = (total_profit_peak / total_entry * 100.0) if total_entry else 0.0<br>        top3, worst3 = top_worst_lines(rows, n=3)        msg = (<br>            f"[포트폴리오 일일 요약]\n"<br>            f"- 총 매수금액: {fmt_money(total_entry)}원\n"<br>            f"- 총 평가금액(현재): {fmt_money(total_current)}원\n"<br>            f"- 총 손익(현재): {fmt_money(total_profit_now)}원 ({total_rate_now:+.2f}%)\n"<br>            f"- 총 최고평가(피크): {fmt_money(total_peak)}원\n"<br>            f"- 총 손익(피크): {fmt_money(total_profit_peak)}원 ({total_rate_peak:+.2f}%)\n"<br>            f"\n[손익률 TOP3]\n" + "\n".join(top3) +<br>            f"\n\n[손익률 WORST3]\n" + "\n".join(worst3) +<br>            f"\n\n- 시각: {now_str}"<br>        )<br>        send_synology_chat(webhook_url, msg)<br>        state.setdefault("_summary", {})["last_date"] = now.strftime("%Y-%m-%d")    # 16:00 리포트(섹션 분리)<br>    if bool(cfg.get("after_close_summary_enabled", True)) and should_send_after_close_summary(state, now, after_close_time):<br>        trigger = [d for d in detail_list if d["active"] == 1 and d["to_stop"] &lt;= 0]<br>        near = [d for d in detail_list if d["active"] == 1 and 0 &lt; d["to_stop"] &lt;= near_dist]<br>        safe = [d for d in detail_list if d["active"] == 1 and d["to_stop"] &gt; near_dist]        def line(d):<br>            a = "A1" if d["active"] == 1 else "A0"<br>            sl_part = ""<br>            if d["stop_loss_pct"] &gt; 0 and d["to_stoploss"] is not None:<br>                sl_part = f" / to_SL={d['to_stoploss']:+.2f}%(-{int(d['stop_loss_pct']*100)}%)"<br>            sign = "+" if d["profit"] &gt;= 0 else ""<br>            return (<br>                f"- {d['name']}({d['code']})[{a}] to_stop={d['to_stop']:+.2f}%{sl_part}\n"<br>                f"  cur={fmt_money(d['cur'])} stop={fmt_money(d['stop'])} eff_trailing={int(d['eff_trailing']*100)}% "<br>                f"pnl={sign}{fmt_money(d['profit'])}원 ({d['rate']:+.2f}%)"<br>            )        risk_box = (<br>            f"[리스크 레벨: {risk['level']}]\n"<br>            f"- Trigger: {risk['trigger_cnt']} / Near: {risk['near_cnt']}\n"<br>            f"- 위험노출: {fmt_money(risk['risk_exposure'])}원 ({risk['exposure_pct']:.1f}%)\n"<br>        )        msg = (<br>            f"[16:00 리포트/기준선+손절+리스크]\n"<br>            f"- 총 손익(active=1): {fmt_money(total_profit_now)}원\n"<br>            f"- 총 수익률(active=1): {total_rate_now:+.2f}%\n"<br>            f"- 설명: to_stop=+X%는 트레일 기준선까지 남은 비율(0% 이하 트리거)\n"<br>            f"- 설명: to_SL=+X%는 손절선까지 남은 비율(0% 이하 손절)\n\n"<br>            + risk_box + "\n"<br>            + "🚨 TRIGGER\n" + ("\n".join(line(d) for d in trigger) if trigger else "- 없음") + "\n\n"<br>            + f"⚠️ NEAR (&lt;= {near_dist:.1f}%)\n" + ("\n".join(line(d) for d in near) if near else "- 없음") + "\n\n"<br>            + "✅ SAFE\n" + ("\n".join(line(d) for d in safe) if safe else "- 없음") +<br>            f"\n\n- 시각: {now_str}"<br>        )<br>        send_synology_chat(webhook_url, msg)<br>        state.setdefault("_after_close_summary", {})["last_date"] = now.strftime("%Y-%m-%d")    if changed_positions:<br>        save_positions_csv_atomic(POSITIONS_PATH, positions_all)    return changed_positions, risk["level"], market_opendef main():<br>    cfg = load_json_safe(CONFIG_PATH, {})<br>    if not cfg:<br>        raise RuntimeError("config.json이 없습니다.")    default_trailing_pct = float(cfg.get("default_trailing_pct", 0.10))<br>    default_stop_loss_pct = float(cfg.get("default_stop_loss_pct", 0.08))    state = load_json_safe(STATE_PATH, {})    if bool(cfg.get("startup_notify_enabled", True)) and should_send_startup_notify(state):<br>        try:<br>            positions_all = load_positions_csv(POSITIONS_PATH, default_trailing_pct, default_stop_loss_pct)<br>            active_positions = [p for p in positions_all if p.active == 1]<br>            lines = [<br>                f"- {p.name}({p.code}.{p.market}) qty={p.qty:g} buy={fmt_money(p.buy_price)} "<br>                f"trail={int(p.trailing_pct*100)}% sl={int(p.stop_loss_pct*100)}%"<br>                for p in active_positions<br>            ]<br>            msg = "[주식 알림봇 시작]\n" + "\n".join(lines) + f"\n- 시각: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"<br>            send_synology_chat(cfg["synology_chat_webhook_url"], msg)<br>            mark_startup_notify_sent(state)<br>            save_json_atomic(STATE_PATH, state)<br>        except Exception as e:<br>            log("STARTUP", f"시작 안내 실패: {e}", level="WARN")    while True:<br>        now = datetime.now()<br>        try:<br>            positions_all = load_positions_csv(POSITIONS_PATH, default_trailing_pct, default_stop_loss_pct)            if bool(cfg.get("daily_reactivate_enabled", True)):<br>                if should_reactivate_today(state, now, cfg.get("daily_reactivate_time", "09:01")):<br>                    if reactivate_all_positions(positions_all, state):<br>                        save_positions_csv_atomic(POSITIONS_PATH, positions_all)<br>                        save_json_atomic(STATE_PATH, state)            changed, risk_level, market_open = run_once(cfg, positions_all, state)<br>            save_json_atomic(STATE_PATH, state)            sleep_min = int(cfg.get("interval_minutes_market", 30))<br>            if bool(cfg.get("adaptive_interval_enabled", True)):<br>                mkt_min = int(cfg.get("interval_minutes_market", sleep_min))<br>                high_min = int(cfg.get("interval_minutes_high_risk", 10))<br>                off_min = int(cfg.get("interval_minutes_off_market", 120))<br>                sleep_min = (high_min if risk_level == "HIGH" else mkt_min) if market_open else off_min            log("SLEEP", f"{sleep_min}분 후 재실행 (risk={risk_level}, market_open={market_open})")<br>            time.sleep(sleep_min * 60)        except Exception as e:<br>            log("ERROR", f"루프 오류: {e}", level="ERROR")<br>            time.sleep(120)if __name__ == "__main__":<br>    main()</pre>



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



<h1 class="wp-block-heading">8) 실행(백그라운드)</h1>



<pre class="wp-block-preformatted">cd ~/python/jusik_top10<br>nohup python3.9 main.py &gt; jusik.log 2&gt;&amp;1 &amp;<br>tail -f jusik.log</pre>



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



<h1 class="wp-block-heading">9) 운영 팁</h1>



<ul class="wp-block-list">
<li><code>positions.csv</code>에서 <code>active=0</code>으로 수동 비활성화 가능</li>



<li>손절/트레일링 트리거 후 <code>auto_disable_on_alert=true</code>면 자동으로 active=0</li>



<li>다음날 <code>09:01</code>에 자동으로 active=1 복구 (옵션)</li>
</ul>



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



<h1 class="wp-block-heading">10) 트러블슈팅</h1>



<h3 class="wp-block-heading">✅ config.json이 없습니다</h3>



<ul class="wp-block-list">
<li><code>main.py</code>와 같은 폴더에 <code>config.json</code>이 있는지 확인</li>
</ul>



<pre class="wp-block-preformatted">ls -al</pre>



<h3 class="wp-block-heading">✅ state.json JSONDecodeError(Extra data)</h3>



<ul class="wp-block-list">
<li>파일이 깨진 것 → <code>{}</code>로 초기화</li>
</ul>



<pre class="wp-block-preformatted">cp state.json state.json.bak<br>echo "{}" &gt; state.json</pre>



<h3 class="wp-block-heading">✅ 알림이 안 오면</h3>



<ul class="wp-block-list">
<li>webhook URL 오타/토큰 확인</li>



<li>방화벽/SSL 문제 여부 확인</li>



<li>logs 확인</li>
</ul>



<pre class="wp-block-preformatted">tail -n 200 jusik.log<br></pre>
<p>게시물 <a href="https://howinfo.kr/orange-pi-5%eb%a1%9c-%ec%a3%bc%ec%8b%9d-etf-%ed%8a%b8%eb%a0%88%ec%9d%bc%eb%a7%81-%ec%8a%a4%ed%83%91-%ec%86%90%ec%a0%88-%eb%a6%ac%ec%8a%a4%ed%81%ac-%eb%a0%88%eb%b2%a8-%ec%95%8c/">Orange Pi 5로 “주식/ETF 트레일링 스탑 + 손절 + 리스크 레벨” 알림봇 만들기 (Synology Chat 웹훅, 10~30분 자동 주기)_버전 두번째 full 소스</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://howinfo.kr/orange-pi-5%eb%a1%9c-%ec%a3%bc%ec%8b%9d-etf-%ed%8a%b8%eb%a0%88%ec%9d%bc%eb%a7%81-%ec%8a%a4%ed%83%91-%ec%86%90%ec%a0%88-%eb%a6%ac%ec%8a%a4%ed%81%ac-%eb%a0%88%eb%b2%a8-%ec%95%8c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>n8n으로 워드프레스 “하루 1건 자동 초안(draft) 업로드” 만들기 (운영 안정성 기준)</title>
		<link>https://howinfo.kr/n8n-%ec%9b%8c%eb%93%9c%ed%94%84%eb%a0%88%ec%8a%a4-%ec%9e%90%eb%8f%99-%ed%8f%ac%ec%8a%a4%ed%8c%85-%eb%a7%8c%eb%93%a4%ea%b8%b0-%eb%93%9c%eb%9e%98%ed%94%84%ed%8a%b8-%ec%97%85%eb%a1%9c%eb%93%9c/</link>
					<comments>https://howinfo.kr/n8n-%ec%9b%8c%eb%93%9c%ed%94%84%eb%a0%88%ec%8a%a4-%ec%9e%90%eb%8f%99-%ed%8f%ac%ec%8a%a4%ed%8c%85-%eb%a7%8c%eb%93%a4%ea%b8%b0-%eb%93%9c%eb%9e%98%ed%94%84%ed%8a%b8-%ec%97%85%eb%a1%9c%eb%93%9c/#respond</comments>
		
		<dc:creator><![CDATA[hong]]></dc:creator>
		<pubDate>Wed, 18 Feb 2026 10:02:52 +0000</pubDate>
				<category><![CDATA[자동화]]></category>
		<category><![CDATA[n8n]]></category>
		<category><![CDATA[REST API]]></category>
		<category><![CDATA[노코드]]></category>
		<category><![CDATA[로우코드]]></category>
		<category><![CDATA[블로그 자동 포스팅]]></category>
		<category><![CDATA[새산성]]></category>
		<category><![CDATA[업무자동화]]></category>
		<category><![CDATA[워드프레스 자동화]]></category>
		<category><![CDATA[웹훅]]></category>
		<category><![CDATA[콘텐츠 자동화]]></category>
		<guid isPermaLink="false">https://howinfo.kr/?p=1792</guid>

					<description><![CDATA[<p>오늘의 결론 내가 이 구조로 바꾸게 된 이유 처음에는 “자동 발행”을 목표로 잡았습니다.그런데 며칠 돌려보니 문제는 발행 자체가 아니라 품질과...</p>
<p>게시물 <a href="https://howinfo.kr/n8n-%ec%9b%8c%eb%93%9c%ed%94%84%eb%a0%88%ec%8a%a4-%ec%9e%90%eb%8f%99-%ed%8f%ac%ec%8a%a4%ed%8c%85-%eb%a7%8c%eb%93%a4%ea%b8%b0-%eb%93%9c%eb%9e%98%ed%94%84%ed%8a%b8-%ec%97%85%eb%a1%9c%eb%93%9c/">n8n으로 워드프레스 “하루 1건 자동 초안(draft) 업로드” 만들기 (운영 안정성 기준)</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">오늘의 결론</h2>



<ul class="wp-block-list">
<li>완전 자동 “발행”보다 <strong>드래프트 업로드 + 마지막 검수</strong>가 운영 안정성이 훨씬 높습니다.</li>



<li>핵심은 WordPress 인증을 <strong>Application Password로 단단히 고정</strong>하는 겁니다.</li>



<li>자동화는 “성공”보다 <strong>실패 알림/재시도/로그</strong>가 제대로 있어야 오래 갑니다.</li>
</ul>



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



<h2 class="wp-block-heading">내가 이 구조로 바꾸게 된 이유</h2>



<p>처음에는 “자동 발행”을 목표로 잡았습니다.<br>그런데 며칠 돌려보니 문제는 발행 자체가 아니라 <strong>품질과 리스크</strong>였어요.</p>



<ul class="wp-block-list">
<li>문장 한 줄이 어색해도 그대로 공개됨</li>



<li>태그가 쓸데없이 늘어남</li>



<li>링크/표현 문제가 생겨도 바로 노출됨</li>
</ul>



<p>그래서 결론은 단순했습니다.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>매일 자동으로 <strong>초안만 올려두고</strong>, 알림 받고 “최종 검수 후 발행”<br>이게 현실적으로 제일 덜 고장나고, 운영이 편했습니다.</p>
</blockquote>



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



<h2 class="wp-block-heading">내 환경</h2>



<ul class="wp-block-list">
<li>n8n: Docker 운영</li>



<li>WordPress: Docker 운영 (REST API 사용)</li>



<li>인증: Application Password (우선 추천)</li>



<li>목표: 매일 09:00 “드래프트 1건 생성”</li>



<li>알림: Slack/메일/시놀로지 챗 중 1개</li>



<li>로그 저장: Google Sheets 또는 DB(선택)</li>
</ul>



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



<h2 class="wp-block-heading">전체 아키텍처 한 장 요약</h2>



<pre class="wp-block-preformatted">Cron(매일 09:00)<br>→ 주제 선택(Topic Picker)<br>→ (옵션) 자료 포인트만 수집<br>→ 본문 생성/정리(템플릿 or AI)<br>→ WP REST API로 draft 생성<br>→ 성공/실패 알림<br>→ 로그 저장(제목/URL/상태/에러)</pre>



<p>운영 팁:<br>처음부터 “모든 걸 자동으로” 하려면 중간에 반드시 깨집니다.<br>저는 <strong>Topic Picker부터 고정 리스트로 시작</strong>해서 성공률을 먼저 올렸습니다.</p>



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



<h2 class="wp-block-heading">준비물 체크리스트</h2>



<ul class="wp-block-list">
<li>WordPress 계정(편집자 이상) + 미디어/글 권한 확인</li>



<li>Application Password 생성(추천) 또는 JWT 세팅</li>



<li>n8n에서 HTTP Request 노드 사용 가능</li>



<li>카테고리 ID 확정(고정 추천)</li>



<li>알림 채널 1개 이상</li>



<li>로그 저장 위치(시트/DB/파일 중 택1)</li>
</ul>



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



<h2 class="wp-block-heading">인증 방식 2가지 (내 결론은 A안)</h2>



<h3 class="wp-block-heading">A안) Application Password (추천)</h3>



<ul class="wp-block-list">
<li>WordPress 사용자 프로필에서 “애플리케이션 비밀번호” 생성</li>



<li>n8n에서 Basic Auth처럼 <code>username + app_password</code>로 호출</li>
</ul>



<p><strong>장점</strong></p>



<ul class="wp-block-list">
<li>설정이 단순하고 운영 안정적</li>



<li>서버 설정 변경이 거의 없음</li>
</ul>



<p><strong>주의</strong></p>



<ul class="wp-block-list">
<li>계정 권한을 최소화(가능하면 전용 계정)</li>



<li>앱 비번은 주기적으로 교체</li>
</ul>



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



<h3 class="wp-block-heading">B안) JWT Auth</h3>



<ul class="wp-block-list">
<li>Bearer Token 방식이라 자동화에 익숙하면 편합니다.</li>
</ul>



<p><strong>단점</strong></p>



<ul class="wp-block-list">
<li>플러그인 + 서버 설정이 필요한 경우가 있어 초기 삽질이 늘어납니다.</li>
</ul>



<p>초반엔 A안으로 성공 루틴 만들고,<br>필요하면 B안으로 넘어가는 게 덜 스트레스였습니다.</p>



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



<h2 class="wp-block-heading">n8n 워크플로우 설계 (디버깅이 쉬운 순서)</h2>



<h3 class="wp-block-heading">노드 1) Cron</h3>



<ul class="wp-block-list">
<li>매일 09:00 실행</li>
</ul>



<h3 class="wp-block-heading">노드 2) Set / Topic Picker</h3>



<ul class="wp-block-list">
<li>방식 1: 고정 리스트에서 랜덤 선택</li>



<li>방식 2: 스프레드시트/DB에서 “미작성” 주제 1개 가져오기</li>
</ul>



<p><strong>운영 팁</strong></p>



<ul class="wp-block-list">
<li>초반에는 <strong>고정 리스트</strong>가 무조건 좋습니다.</li>



<li>DB 연동은 성공률이 안정화된 뒤에 붙이세요.</li>
</ul>



<h3 class="wp-block-heading">노드 3) (옵션) 자료 수집</h3>



<ul class="wp-block-list">
<li>RSS/메모/내부 문서에서 “포인트만” 가져오기</li>



<li>원문 복붙 금지(저작권/품질 이슈)</li>
</ul>



<h3 class="wp-block-heading">노드 4) 본문 생성</h3>



<ul class="wp-block-list">
<li>A) 템플릿 기반: 품질 일정, 운영 안정</li>



<li>B) AI 기반: 생산성 높음, 검수 필수</li>
</ul>



<p>howinfo 자동화 글 톤이면<br><strong>A로 먼저 안정화 → B로 고도화</strong>가 제일 덜 고장납니다.</p>



<h3 class="wp-block-heading">노드 5) Function (정리/필터)</h3>



<p>여기서 하는 일:</p>



<ul class="wp-block-list">
<li>H2/H3 구조 정리</li>



<li>과장 표현 제거(무조건/완벽/최고 등)</li>



<li>중복 문장 제거</li>



<li>너무 긴 문장 쪼개기</li>
</ul>



<h3 class="wp-block-heading">노드 6) HTTP Request (Posts Create)</h3>



<ul class="wp-block-list">
<li><code>POST /wp-json/wp/v2/posts</code></li>



<li><code>status: draft</code> 고정</li>



<li>title/content/categories/tags 매핑</li>
</ul>



<h3 class="wp-block-heading">노드 7) IF (성공/실패)</h3>



<ul class="wp-block-list">
<li>성공: 알림 + 로그</li>



<li>실패: 재시도(1회) + 실패 알림 + 로그</li>
</ul>



<h3 class="wp-block-heading">노드 8) 알림(Slack/메일/시놀로지 챗)</h3>



<p>성공 알림엔 꼭:</p>



<ul class="wp-block-list">
<li>제목</li>



<li>드래프트 링크</li>



<li>실행 시간</li>
</ul>



<p>실패 알림엔 꼭:</p>



<ul class="wp-block-list">
<li>실패 단계</li>



<li>HTTP 코드</li>



<li>응답 요약</li>



<li>실행 ID(또는 n8n execution URL)</li>
</ul>



<h3 class="wp-block-heading">노드 9) 로그 저장(선택이지만 강추)</h3>



<p>저장 필드 추천:</p>



<ul class="wp-block-list">
<li>datetime</li>



<li>topic/title</li>



<li>post_id</li>



<li>draft_url</li>



<li>status(success/fail)</li>



<li>error_summary</li>



<li>duration_ms</li>
</ul>



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



<h2 class="wp-block-heading">WordPress 업로드 필드 매핑 (실무에서 자주 헷갈림)</h2>



<ul class="wp-block-list">
<li><code>title</code>: 글 제목</li>



<li><code>content</code>: 본문(HTML 권장)</li>



<li><code>status</code>: <code>draft</code></li>



<li><code>categories</code>: 카테고리 ID 배열(고정)</li>



<li><code>tags</code>: 태그 ID 배열 또는 태그 생성 로직</li>
</ul>



<p>태그 운영 팁:</p>



<ul class="wp-block-list">
<li>태그 자동 생성은 편하지만, 금방 <strong>태그가 폭발</strong>합니다.</li>



<li>저는 상위 20~30개 태그 풀을 만들어놓고 그 안에서만 고르게 했습니다.</li>
</ul>



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



<h2 class="wp-block-heading">실패 알림 / 재시도 (운영에서 제일 중요한 파트)</h2>



<p>재시도는 현실적으로 이렇게만:</p>



<ul class="wp-block-list">
<li>5xx / 429 → 2분 후 1회 재시도</li>



<li>401 / 403 → 재시도 금지(즉시 알림)</li>
</ul>



<p>무한 재시도는<br>“고장난 자동화가 매일 스스로를 때리는 구조”가 되어서 운영이 망가집니다.</p>



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



<h2 class="wp-block-heading">운영 체크리스트 (매일 자동으로 굴리는 기준)</h2>



<ul class="wp-block-list">
<li>status는 항상 draft</li>



<li>알림 채널 1개 이상</li>



<li>재시도는 최대 1회</li>



<li>카테고리 ID 고정</li>



<li>태그는 풀 관리(남발 금지)</li>



<li>성공 알림에 드래프트 링크 포함</li>



<li>Application Password 권한 점검(전용 계정 추천)</li>
</ul>



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



<h1 class="wp-block-heading">FAQ</h1>



<p><strong>Q1. 완전 자동 발행으로 해도 되나요?</strong><br>A. 가능하지만, 초반에는 드래프트가 훨씬 안전합니다. 검수 루틴이 잡힌 뒤 자동 발행으로 확장하는 게 좋습니다.</p>



<p><strong>Q2. 이미지(썸네일)까지 자동으로 가능해요?</strong><br>A. 가능합니다. 다만 “미디어 업로드 → media_id → featured_media 연결” 단계가 추가됩니다(별도 글로 분리 추천).</p>



<p><strong>Q3. 태그 ID를 모르면 어떻게 하죠?</strong><br>A. 태그 조회 API로 먼저 찾고, 없으면 생성한 뒤 그 ID를 post 생성 payload에 넣는 방식으로 구현합니다.</p>



<p></p>



<p><br></p>
<p>게시물 <a href="https://howinfo.kr/n8n-%ec%9b%8c%eb%93%9c%ed%94%84%eb%a0%88%ec%8a%a4-%ec%9e%90%eb%8f%99-%ed%8f%ac%ec%8a%a4%ed%8c%85-%eb%a7%8c%eb%93%a4%ea%b8%b0-%eb%93%9c%eb%9e%98%ed%94%84%ed%8a%b8-%ec%97%85%eb%a1%9c%eb%93%9c/">n8n으로 워드프레스 “하루 1건 자동 초안(draft) 업로드” 만들기 (운영 안정성 기준)</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://howinfo.kr/n8n-%ec%9b%8c%eb%93%9c%ed%94%84%eb%a0%88%ec%8a%a4-%ec%9e%90%eb%8f%99-%ed%8f%ac%ec%8a%a4%ed%8c%85-%eb%a7%8c%eb%93%a4%ea%b8%b0-%eb%93%9c%eb%9e%98%ed%94%84%ed%8a%b8-%ec%97%85%eb%a1%9c%eb%93%9c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>n8n은 왜 계속 쓰게 되는가 — “코드 없이 업무 흐름 자동화”를 실제로 써본 기록</title>
		<link>https://howinfo.kr/n8n-%ec%9e%90%eb%8f%99%ed%99%94-%ed%88%b4%ec%9d%b4-%eb%ad%90%ea%b8%b8%eb%9e%98-%eb%8b%a4%eb%93%a4-%ec%93%b0%eb%8a%94-%ea%b1%b8%ea%b9%8c-%ec%8b%a4%eb%ac%b4%ec%97%90%ec%84%9c-%eb%b0%94%eb%a1%9c/</link>
					<comments>https://howinfo.kr/n8n-%ec%9e%90%eb%8f%99%ed%99%94-%ed%88%b4%ec%9d%b4-%eb%ad%90%ea%b8%b8%eb%9e%98-%eb%8b%a4%eb%93%a4-%ec%93%b0%eb%8a%94-%ea%b1%b8%ea%b9%8c-%ec%8b%a4%eb%ac%b4%ec%97%90%ec%84%9c-%eb%b0%94%eb%a1%9c/#respond</comments>
		
		<dc:creator><![CDATA[hong]]></dc:creator>
		<pubDate>Wed, 18 Feb 2026 10:00:01 +0000</pubDate>
				<category><![CDATA[자동화]]></category>
		<category><![CDATA[api연동]]></category>
		<category><![CDATA[n8n]]></category>
		<category><![CDATA[노코드]]></category>
		<category><![CDATA[로우코드]]></category>
		<category><![CDATA[생산성]]></category>
		<category><![CDATA[서버 모니터링]]></category>
		<category><![CDATA[업무자동화]]></category>
		<category><![CDATA[워드프레스 자동화]]></category>
		<category><![CDATA[워크플로우]]></category>
		<category><![CDATA[웹훅]]></category>
		<guid isPermaLink="false">https://howinfo.kr/?p=1790</guid>

					<description><![CDATA[<p>오늘의 결론 내가 n8n을 계속 쓰게 된 이유 처음에는 단순히 “블로그 자동 초안 업로드” 때문에 시작했습니다. 그런데 쓰다 보니 깨달은...</p>
<p>게시물 <a href="https://howinfo.kr/n8n-%ec%9e%90%eb%8f%99%ed%99%94-%ed%88%b4%ec%9d%b4-%eb%ad%90%ea%b8%b8%eb%9e%98-%eb%8b%a4%eb%93%a4-%ec%93%b0%eb%8a%94-%ea%b1%b8%ea%b9%8c-%ec%8b%a4%eb%ac%b4%ec%97%90%ec%84%9c-%eb%b0%94%eb%a1%9c/">n8n은 왜 계속 쓰게 되는가 — “코드 없이 업무 흐름 자동화”를 실제로 써본 기록</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">오늘의 결론</h2>



<ul class="wp-block-list">
<li>n8n은 “코드 없이 자동화”라기보다 **“눈으로 보면서 흐름을 설계하는 자동화 도구”**에 가깝습니다.</li>



<li>핵심은 기능이 아니라 <strong>트리거 → 처리 → 액션 구조를 습관처럼 만드는 것</strong>입니다.</li>



<li>작은 자동화 1개를 성공시키면, 그 다음부터는 계속 확장하게 됩니다.</li>
</ul>



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



<h2 class="wp-block-heading">내가 n8n을 계속 쓰게 된 이유</h2>



<p>처음에는 단순히 “블로그 자동 초안 업로드” 때문에 시작했습니다.</p>



<p>그런데 쓰다 보니 깨달은 게 있습니다.</p>



<p>자동화의 진짜 장점은<br>기능이 아니라 <strong>흐름을 눈으로 볼 수 있다는 점</strong>이었습니다.</p>



<p>Zapier나 다른 툴도 써봤지만,<br>n8n은 노드를 연결하는 구조라 디버깅이 직관적입니다.</p>



<p>문제가 생기면 어디서 데이터가 깨졌는지 바로 보입니다.</p>



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



<h2 class="wp-block-heading">n8n을 한 문장으로 정의하면</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>여러 서비스(API/앱)를 연결해서 “업무 흐름”을 자동 실행하는 워크플로우 플랫폼</p>
</blockquote>



<p>엑셀 매크로가 파일 내부 자동화라면,<br>n8n은 <strong>웹/서버/API까지 묶는 매크로</strong>에 가깝습니다.</p>



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



<h2 class="wp-block-heading">n8n이 특히 강한 이유 (내 기준)</h2>



<h3 class="wp-block-heading">1️⃣ 반복 작업 자동화</h3>



<p>매일 리포트 생성 → 슬랙 발송<br>이거 한 번 만들면 손댈 일이 거의 없습니다.</p>



<h3 class="wp-block-heading">2️⃣ API 연동 후 가공</h3>



<p>API 호출 → 필요한 필드만 추출 → 시트 저장<br>Set 노드로 데이터 단순화하면 생각보다 어렵지 않습니다.</p>



<h3 class="wp-block-heading">3️⃣ 조건 분기(IF)</h3>



<p>“CPU 80% 넘으면 알림”<br>이 구조가 자동화의 핵심입니다.</p>



<h3 class="wp-block-heading">4️⃣ 웹훅 기반 실행</h3>



<p>“이벤트가 발생했을 때 즉시 반응”<br>이게 실무 자동화에서 체감이 큽니다.</p>



<h3 class="wp-block-heading">5️⃣ 실패 대응</h3>



<p>실패 시 재시도 + 실패 로그 저장<br>이걸 안 넣으면 자동화는 금방 망가집니다.</p>



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



<h2 class="wp-block-heading">기본 구조는 항상 이 3단계</h2>



<pre class="wp-block-preformatted">Trigger(시작)<br>→ Process(처리/변환/조건)<br>→ Action(전송/저장/게시)</pre>



<p>이 틀만 이해하면<br>n8n은 거의 레고처럼 조립됩니다.</p>



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



<h2 class="wp-block-heading">내가 실제로 돌려본 자동화 예시</h2>



<h3 class="wp-block-heading">1️⃣ 블로그 초안 자동 업로드</h3>



<ul class="wp-block-list">
<li>Cron → 주제 선택 → 본문 생성 → WP draft 업로드</li>



<li>완전 자동 발행은 안 합니다.<br>드래프트 + 최종 검수 구조가 훨씬 안정적입니다.</li>
</ul>



<h3 class="wp-block-heading">2️⃣ 서버 상태 체크 → 슬랙 알림</h3>



<ul class="wp-block-list">
<li>5분마다 Cron</li>



<li>CPU/메모리 확인</li>



<li>임계치 초과 시 요약 알림</li>
</ul>



<p>이건 운영에서 체감이 큽니다.<br>“몰라서 늦게 대응”이 사라집니다.</p>



<h3 class="wp-block-heading">3️⃣ 메일 자동 분류</h3>



<ul class="wp-block-list">
<li>새 메일 감지</li>



<li>키워드 분류</li>



<li>담당자 자동 전달</li>
</ul>



<h3 class="wp-block-heading">4️⃣ RSS 수집 → 요약 → 노션 저장</h3>



<p>자료 정리에 생각보다 도움이 됩니다.</p>



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



<h2 class="wp-block-heading">처음 시작할 때 내가 했던 실수</h2>



<h3 class="wp-block-heading">❌ 너무 큰 자동화를 한 번에 만들려 함</h3>



<p>→ 반드시 깨집니다.</p>



<h3 class="wp-block-heading">❌ 실패 로그를 안 남김</h3>



<p>→ 왜 실패했는지 모르게 됩니다.</p>



<h3 class="wp-block-heading">❌ 완전 자동 발행</h3>



<p>→ 품질 관리가 안 됩니다.</p>



<p>지금은 이렇게 운영합니다:</p>



<ul class="wp-block-list">
<li>작은 단위 자동화부터 성공</li>



<li>재시도는 1회만</li>



<li>실패는 무조건 알림</li>



<li>민감정보는 Credential에 저장</li>
</ul>



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



<h2 class="wp-block-heading">자주 막히는 지점 &amp; 해결법</h2>



<h3 class="wp-block-heading">JSON 구조가 너무 복잡</h3>



<p>→ Set 노드로 필요한 필드만 추출</p>



<h3 class="wp-block-heading">API 401/403</h3>



<p>→ 인증/권한 문제. 재시도하지 말고 즉시 알림</p>



<h3 class="wp-block-heading">429</h3>



<p>→ 딜레이 넣고 1회 재시도</p>



<h3 class="wp-block-heading">자동 게시가 부담</h3>



<p>→ draft + 알림 구조로 변경</p>



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



<h2 class="wp-block-heading">운영 관점에서 중요한 4가지</h2>



<ol class="wp-block-list">
<li>워크플로우 이름 규칙 만들기</li>



<li>실패 알림 반드시 연결</li>



<li>로그 저장(나중에 품질 개선에 도움)</li>



<li>작은 성공을 먼저 만들기</li>
</ol>



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



<h1 class="wp-block-heading">FAQ</h1>



<p><strong>Q. Zapier/Make와 뭐가 다르나요?</strong><br>A. 기능은 비슷하지만, n8n은 “내 서버에서 돌릴 수 있다”는 점이 가장 큽니다.</p>



<p><strong>Q. 코드를 꼭 써야 하나요?</strong><br>A. 대부분은 노드만으로 됩니다. 다만 복잡한 데이터 가공은 Function 노드가 훨씬 편합니다.</p>



<p><strong>Q. 초보가 첫 자동화로 뭐부터?</strong><br>A. Cron → 데이터 수집 → 슬랙/메일 발송. 이게 성공 확률이 가장 높습니다.</p>



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



<h2 class="wp-block-heading">정리</h2>



<p>n8n은 단순히 “자동화 툴”이라기보다<br><strong>업무 흐름을 설계하는 도구</strong>에 가깝습니다.</p>



<p>작은 자동화 하나를 성공시키면<br>그 다음부터는 계속 확장하게 됩니다.</p>



<p>howinfo 기준으로는</p>



<ul class="wp-block-list">
<li>블로그 자동 초안</li>



<li>모니터링 알림</li>



<li>리포트 자동화</li>
</ul>



<p>이 세 가지가 가장 체감이 큰 시작점이었습니다.</p>



<p></p>
<p>게시물 <a href="https://howinfo.kr/n8n-%ec%9e%90%eb%8f%99%ed%99%94-%ed%88%b4%ec%9d%b4-%eb%ad%90%ea%b8%b8%eb%9e%98-%eb%8b%a4%eb%93%a4-%ec%93%b0%eb%8a%94-%ea%b1%b8%ea%b9%8c-%ec%8b%a4%eb%ac%b4%ec%97%90%ec%84%9c-%eb%b0%94%eb%a1%9c/">n8n은 왜 계속 쓰게 되는가 — “코드 없이 업무 흐름 자동화”를 실제로 써본 기록</a>이 <a href="https://howinfo.kr">하우인포-IT·테크</a>에 처음 등장했습니다.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://howinfo.kr/n8n-%ec%9e%90%eb%8f%99%ed%99%94-%ed%88%b4%ec%9d%b4-%eb%ad%90%ea%b8%b8%eb%9e%98-%eb%8b%a4%eb%93%a4-%ec%93%b0%eb%8a%94-%ea%b1%b8%ea%b9%8c-%ec%8b%a4%eb%ac%b4%ec%97%90%ec%84%9c-%eb%b0%94%eb%a1%9c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
