instruction.md
# 오늘의집 오늘의딜 조회
## What this skill does
오늘의집 공개 오늘의딜 페이지(`https://ohou.se/commerces/today_deals`)의 서버 렌더링 초기 데이터(`__NEXT_DATA__`)를 읽어 특가 상품을 조회한다.
- 오늘의딜/스페셜딜 상품 목록 조회
- 할인율, 원가, 판매가, 쿠폰/결제혜택 반영 최저가 정리
- 브랜드, 리뷰 수, 평점, 무료배송 여부, 상품 링크 확인
- 키워드, 최소 할인율, 무료배송 필터
## When to use
- "오늘의집 오늘의딜 뭐 있어?"
- "오늘의집에서 할인율 높은 특가 상품 3개 보여줘"
- "오늘의집 무료배송 특가만 골라줘"
- "오늘의집에서 러그 특가 찾아줘"
## When not to use
- 로그인, 장바구니, 구매, 결제 자동화 — 이 스킬은 의도적으로 구매 플로우를 포함하지 않는다.
- 개인화 추천, 사용자별 쿠폰 적용 확정, 실시간 재고 보장.
- 법적 증빙 수준의 가격 확정 — 조회 시점 기준 참고용이다.
- 차단 우회, CAPTCHA 우회 — 표준 라이브러리 `urllib` 한 호출로 안 되면 실패 모드로 처리한다.
## Required inputs
별도 입력 없이 실행 가능. 선택적으로 아래를 지정할 수 있다:
- `--query`: 상품명/브랜드 키워드
- `--min-discount`: 최소 할인율 (0~100 정수)
- `--free-delivery`: 무료배송 상품만
- `--sort`: 정렬 기준 (`discount`, `price`, `review`, `annual-sales`)
- `--limit`: 결과 개수 (양의 정수, 기본 10)
- `--html-file`: 오프라인 HTML/JSON fixture 경로
## Official/public surface
- 오늘의집 오늘의딜 페이지: `https://ohou.se/commerces/today_deals`
- 현재 웹 페이지는 canonical/OG URL로 `https://store.ohou.se/today_deals`를 노출하지만, 브라우저 접근용 공개 URL은 `ohou.se/commerces/today_deals`다.
- 응답 HTML의 Next.js `__NEXT_DATA__` 안 React Query `dehydratedState`에서 `today-deal-feed`, `special-today-deal-feed` queryKey 두 곳의 `todayDealFeed.slots`만 명시적으로 읽는다. 다른 페이지 모듈(navigation, banner 등)에 `type: DEAL` 노드가 있어도 무시한다.
- HTTP 요청은 `User-Agent: k-skill-ohou-today-deal/1.0 (+https://github.com/NomaDamas/k-skill)`로 보낸다. ohou.se 앞단 Akamai bot manager는 익명/단축 UA를 차단하지만 봇 이름 + contact URL이 포함된 well-formed UA는 통과시키므로 우회/조작 없이 정직한 자기소개로 요청한다.
## Prerequisites
- `python3`
- 별도 로그인/API 키 없음
## Workflow
### 1. 오늘의딜 상품 조회
오늘의집 오늘의딜 공개 페이지에서 상품 목록을 가져온다. 기본 정렬은 할인율 높은 순이다.
```bash
npx -y @nomadamas/k-skill@0 exec ohou-today-deal scripts/ohou_today_deal.py -- list --limit 10
```
응답 예시:
```json
{
"source": {
"name": "ohou-today-deal",
"url": "https://ohou.se/commerces/today_deals",
"fetched_at": "2026-05-18T01:44:16+00:00",
"surface": "__NEXT_DATA__ today-deal-feed + special-today-deal-feed"
},
"filters": {"query": null, "min_discount": null, "free_delivery": false, "sort": "discount", "limit": 10},
"count": 10,
"total_count": 72,
"filtered_count": 72,
"items": [
{
"id": "823405",
"title": "삼익가구 BEST상품 총집합",
"brand": "삼익가구",
"url": "https://ohou.se/productions/823405/selling",
"original_price": 449000,
"selling_price": 132000,
"discount_rate": 70,
"best_price": 118800,
"best_discount_rate": 73,
"best_discount_description": "쿠폰 할인가",
"review_count": 53818,
"review_average": 4.7,
"free_delivery": false,
"sold_out": false
}
]
}
```
### 2. 할인율 높은 순 정렬
`bestDiscountPrice.discountRate`(쿠폰/결제혜택 반영 할인율)가 있으면 우선 사용하고, 없으면 상품 기본 `discountRate`를 사용한다.
```bash
npx -y @nomadamas/k-skill@0 exec ohou-today-deal scripts/ohou_today_deal.py -- list \
--sort discount \
--limit 5
```
정렬 옵션: `discount`(할인율), `price`(낮은 가격), `review`(리뷰 많은 순), `annual-sales`(연간 판매량).
### 3. 키워드·할인율·무료배송 필터
상품명 또는 브랜드에 키워드가 포함된 상품만 걸러내고, 최소 할인율과 무료배송 조건을 조합할 수 있다.
```bash
npx -y @nomadamas/k-skill@0 exec ohou-today-deal scripts/ohou_today_deal.py -- list \
--query 러그 \
--min-discount 30 \
--free-delivery \
--limit 5
```
### 4. 오프라인 fixture로 검증
실제 네트워크 없이 저장된 HTML/JSON 파일로 동일한 파싱을 테스트한다.
```bash
npx -y @nomadamas/k-skill@0 exec ohou-today-deal scripts/ohou_today_deal.py -- list \
--html-file ./today-deals.html \
--limit 3
```
## Output format
기본 출력은 들여쓰기 JSON (`indent=2`). 파이프/스크립트에서 사용할 때는 출력을 `jq` 등으로 후처리한다.
주요 필드:
| 필드 | 설명 |
|---|---|
| `source.fetched_at` | 조회 시각 (UTC ISO 8601) |
| `count` | 반환된 상품 수 |
| `total_count` | 전체 오늘의딜 상품 수 |
| `filtered_count` | 필터 적용 후 상품 수 |
| `items[].best_price` | 쿠폰/결제혜택 반영 최저가 (없으면 null) |
| `items[].best_discount_rate` | 혜택 반영 할인율 (없으면 null) |
| `items[].free_delivery` | 무료배송 여부 |
| `items[].sold_out` | 품절 여부 |
## Endpoints used
이 스킬이 호출하는 공개 endpoint:
| Method | URL | 용도 |
|---|---|---|
| GET | `https://ohou.se/commerces/today_deals` | 오늘의딜 공개 HTML (서버 렌더링) |
비로그인 / 무인증. 헤더는 `User-Agent` + `Accept` 만.
## Response policy
- 상위 3~5개만 먼저 보여준다.
- 상품명, 브랜드, 할인가, 원가, 할인율, 평점/리뷰 수, 무료배송 여부, 링크를 정리한다.
- 가격, 할인, 품절, 쿠폰/결제혜택은 "조회 시각 기준"으로 변동 가능하다고 명시한다.
- 구매/장바구니/결제는 자동화하지 말고 상품 링크만 제공한다.
- "지금 사라" 같은 행위 유도 금지 — 사용자가 직접 페이지에서 구매한다.
## Done when
- 오늘의딜 상품 후보가 JSON 또는 요약 목록으로 반환된다.
- 할인율/가격 기준과 조회 시점이 분리되어 설명된다.
- 로그인, 구매, 결제, 개인화 기능을 시도하지 않았다.
## Failure modes
- **`__NEXT_DATA__` 미발견**: 오늘의집이 Next.js SSR 구조를 변경하거나, 서버 렌더링 대신 클라이언트 렌더링으로 전환하면 `ValueError` 발생. 스킬 파서 수정이 필요하다.
- **today-deal-feed queryKey 미발견**: React Query 키 이름이 바뀌면 `extract_deals()`는 빈 리스트를 반환한다 (`total_count: 0`). `TODAY_DEAL_FEED_KEYS` 상수를 새 키 이름으로 업데이트해야 한다.
- **HTTP 403**: ohou.se 앞단 Akamai bot manager가 요청을 차단한 경우. `User-Agent` 헤더가 변경되어 봇 자기소개 + contact URL 시그니처를 잃었을 가능성이 높다. 우회 시도하지 않고 에러 출력 후 종료한다.
- **HTTP 4xx/5xx (기타)**: 일시 장애. 우회 시도하지 않고 에러 출력 후 종료.
- **빈 응답 (`total_count: 0`)**: 오늘의딜이 아직 업데이트되지 않았거나, 페이지 구조가 바뀐 경우. 브라우저에서 직접 확인을 안내한다.
- **가격/쿠폰 변동**: `best_price`는 조회 시점 기준이며, 사용자별 쿠폰/결제수단에 따라 실제 결제가는 다를 수 있다.
- **필드 누락**: 일부 상품에 `bestDiscountPrice`, `badgeProperties.isFreeDelivery`, `scrapInfo` 등이 없을 수 있다. null로 처리된다.
## Notes
- read-only 스킬이다.
- 화면 선택자보다 서버 렌더링 초기 JSON을 우선한다.
- 새 dependency 없이 Python 표준 라이브러리만 사용한다.
references/DISCLAIMER.md
# DISCLAIMER — `ohou-today-deal`
이 스킬은 오늘의집 또는 판매자의 공식 기능 또는 공식 지원 도구가 아니며, 공식 제휴·후원·승인·인증 또는 협업한 사실이 전혀 없습니다. 상표와 서비스명은 상품·특가 조회 기능과 대상을 설명하기 위해서만 사용합니다.
대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)(소니용 리모컨 사건)은 기능 설명용 표장과 출처표시를 구별했습니다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)에 따른 출처 혼동 판단은 별도입니다.
대법원 2022. 5. 12. 선고 [2021도1533 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=221765)은 공개정보 수집만으로 곧바로 [정보통신망법 제48조](https://www.law.go.kr/법령/정보통신망이용촉진및정보보호등에관한법률/제48조) 위반이 되는 것은 아닌 사정을 제시했지만, [저작권법 제93조](https://www.law.go.kr/법령/저작권법/제93조)의 상품·리뷰 DB 권리와 [형법 제314조 제2항](https://www.law.go.kr/법령/형법/제314조)은 별도입니다.
- 공개 상품정보 자동 수집은 반드시 개인의 정보 조회용으로만 사용합니다.
- 조직적·대량 크롤링, 상품·리뷰 DB 구축·재판매, 이미지·상세설명 원본 재배포를 하지 않습니다.
- bot control·접근통제·차단·rate limit을 우회하거나 정상 영업을 방해하지 않습니다.
이 문서는 적법성을 보증하는 법률 자문이 아닙니다.
references/TRADEMARK-LEGAL-STATEMENT.md
# 상표 사용 법적 고지 — `ohou-today-deal`
이 스킬에서 `오늘의집` 명칭은 오늘의딜 상품·할인·리뷰 조회의 **대상 서비스**를 식별하기 위해 사용한다. k-skill의 출처를 오늘의집 운영사로 표시하려는 사용이 아니다.
대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)은 타인의 표장을 출처표시가 아니라 상품 기능 또는 적용 기종을 밝히기 위해 사용하고 상표 사용으로 인식될 수 없는 경우 침해가 아니라고 판시했다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)와 대법원 [2011다18802](https://www.law.go.kr/LSW/precInfoP.do?precSeq=167457), [2019후10418](https://law.go.kr/LSW/precInfoP.do?mode=0&precSeq=230725) 판결에 따라 실제 거래계에서 출처표시로 기능하는지는 표시 태양과 사용 경위 등을 종합해 판단해야 한다.
따라서 이 명칭은 필요한 범위의 평문으로만 사용하고, 별도 근거 없이 로고, 공식·제휴·후원·인증·파트너 표현을 사용하지 않는다. 각 상표의 권리는 해당 권리자에게 있다. 이 문서는 상품정보의 저작권·데이터베이스권, 부정경쟁방지법, 약관·계약 또는 접근·재사용의 적법성을 판단하지 않는다.
전체 검토: [제3자 상표의 기능 설명·호환 대상 표시 검토](https://github.com/NomaDamas/k-skill/blob/dev/docs/legal/trademark-use-review.md)
scripts/ohou_today_deal.py
#!/usr/bin/env python3
"""ohou-today-deal — 오늘의집 공개 오늘의딜 특가 상품 조회 CLI.
조회 전용. 로그인·장바구니·구매·결제 자동화 없음.
__NEXT_DATA__ 서버 렌더링 초기 데이터만 읽는 read-only 스킬.
Usage:
ohou-today-deal list [--limit N] [--sort discount|price|review|annual-sales]
ohou-today-deal list --query 러그 --min-discount 30 --free-delivery
ohou-today-deal list --html-file ./fixture.html
Supported surface:
https://ohou.se/commerces/today_deals (공개 HTML)
"""
from __future__ import annotations
import argparse
import html
import json
import re
import sys
import urllib.request
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
DEFAULT_URL = "https://ohou.se/commerces/today_deals"
DEFAULT_USER_AGENT = (
"k-skill-ohou-today-deal/1.0 (+https://github.com/NomaDamas/k-skill)"
)
TODAY_DEAL_FEED_KEYS: tuple[tuple[str, ...], ...] = (
("today-deal-feed",),
("special-today-deal-feed",),
)
@dataclass(frozen=True)
class OhouDeal:
id: str
title: str
brand: str | None
url: str
image_url: str | None
original_price: int | None
selling_price: int | None
discount_rate: int | None
best_price: int | None
best_discount_rate: int | None
best_discount_description: str | None
review_count: int
review_average: float | None
scrap_count: int | None
annual_sales: int | None
free_delivery: bool
sold_out: bool
start_at: str | None
end_at: str | None
def to_dict(self) -> dict[str, Any]:
return asdict(self)
def _to_int(value: Any) -> int | None:
if value is None or value == "":
return None
try:
return int(value)
except (TypeError, ValueError):
return None
def _to_float(value: Any) -> float | None:
if value is None or value == "":
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def fetch_html(url: str = DEFAULT_URL, timeout: int = 20) -> str:
# ohou.se Akamai 정책: 익명 UA(`python-urllib`, `Mozilla/5.0` 단독)는 403,
# 봇 이름+contact URL이 포함된 well-formed UA는 허용. 우회가 아닌 자기소개.
request = urllib.request.Request(
url,
headers={
"User-Agent": DEFAULT_USER_AGENT,
"Accept": "text/html,application/json",
},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
charset = response.headers.get_content_charset() or "utf-8"
return response.read().decode(charset, errors="replace")
def extract_next_data(document: str) -> dict[str, Any]:
stripped = document.lstrip()
if stripped.startswith("{"):
return json.loads(stripped)
match = re.search(
r'<script\b[^>]*\bid=["\']__NEXT_DATA__[^>]*>(.*?)</script>',
document,
re.DOTALL,
)
if not match:
raise ValueError("Could not find __NEXT_DATA__ in Today Deal HTML")
return json.loads(html.unescape(match.group(1)))
def _walk(value: Any):
"""스택 기반 DFS로 JSON 트리의 모든 dict 노드를 순회한다.
__NEXT_DATA__는 깊고 거대한 트리 구조를 가질 수 있어
재귀 대신 반복문을 사용해 sys.getrecursionlimit() 제한을 회피한다.
"""
stack = [value]
while stack:
curr = stack.pop()
if isinstance(curr, dict):
yield curr
stack.extend(curr.values())
elif isinstance(curr, list):
stack.extend(reversed(curr))
def _looks_like_deal_node(node: dict[str, Any]) -> bool:
deal = node.get("deal")
return (
node.get("type") == "DEAL"
and isinstance(deal, dict)
and bool(deal.get("id"))
and bool(deal.get("name"))
)
def _iter_today_deal_feeds(payload: dict[str, Any]):
"""__NEXT_DATA__에서 today-deal-feed / special-today-deal-feed 쿼리만 골라낸다.
React Query dehydrated state는 `props.pageProps.dehydratedState.queries`에
`[{queryKey, state: {data: {...}}}]` 형태로 저장된다. queryKey가 일치하는
엔트리의 `state.data.todayDealFeed.slots`만 today-deal 콘텐츠로 인정한다.
"""
allowed = {tuple(key) for key in TODAY_DEAL_FEED_KEYS}
queries = (
payload.get("props", {})
.get("pageProps", {})
.get("dehydratedState", {})
.get("queries", [])
)
if not isinstance(queries, list):
return
for entry in queries:
if not isinstance(entry, dict):
continue
query_key = entry.get("queryKey")
if not isinstance(query_key, list):
continue
if tuple(query_key) not in allowed:
continue
state = entry.get("state") or {}
data = state.get("data") or {}
feed = data.get("todayDealFeed") or {}
slots = feed.get("slots")
if isinstance(slots, list):
yield tuple(query_key), slots
def _normalize_deal(node: dict[str, Any]) -> OhouDeal:
deal = node.get("deal", {})
price = deal.get("price") or {}
best_price = node.get("bestDiscountPrice") or {}
brand = deal.get("brand") or {}
review = deal.get("reviewStatistic") or {}
scrap = deal.get("scrapInfo") or {}
badge = deal.get("badgeProperties") or {}
annual_sales = node.get("salesStats", {}).get("annualCumulativeSales")
deal_id = str(deal.get("id", ""))
return OhouDeal(
id=deal_id,
title=str(deal.get("name") or node.get("title") or ""),
brand=brand.get("name"),
url=f"https://ohou.se/productions/{deal_id}/selling",
image_url=deal.get("imageUrl"),
original_price=_to_int(price.get("representativeOriginalPrice")),
selling_price=_to_int(price.get("representativeSellingPrice")),
discount_rate=_to_int(price.get("discountRate")),
best_price=_to_int(best_price.get("price")),
best_discount_rate=_to_int(best_price.get("discountRate")),
best_discount_description=best_price.get("discountPlanDescription"),
review_count=_to_int(review.get("reviewCount")) or 0,
review_average=_to_float(review.get("reviewAverage")),
scrap_count=_to_int(scrap.get("scrapCount")),
annual_sales=_to_int(annual_sales),
free_delivery=bool(badge.get("isFreeDelivery")),
sold_out=bool(deal.get("isSoldOut")),
start_at=node.get("startAt"),
end_at=node.get("endAt"),
)
def extract_deals(payload: dict[str, Any]) -> list[OhouDeal]:
seen: set[str] = set()
deals: list[OhouDeal] = []
found_feed = False
for _query_key, slots in _iter_today_deal_feeds(payload):
found_feed = True
for node in slots:
if not isinstance(node, dict) or not _looks_like_deal_node(node):
continue
deal = _normalize_deal(node)
if deal.id in seen:
continue
seen.add(deal.id)
deals.append(deal)
# Fixture/legacy fallback: payload에 React Query dehydratedState가 없거나
# queryKey 구조가 다른 경우(테스트 fixture, 단순화된 페이로드)에 한해서만
# 전체 트리를 DFS로 훑어 DEAL 노드를 수집한다. 라이브 페이지는 항상
# 위쪽 명시적 분기에서 잡히므로 이 fallback은 영향받지 않는다.
if not found_feed:
for node in _walk(payload):
if not _looks_like_deal_node(node):
continue
deal = _normalize_deal(node)
if deal.id in seen:
continue
seen.add(deal.id)
deals.append(deal)
return deals
def filter_deals(
deals: list[OhouDeal],
*,
query: str | None = None,
min_discount: int | None = None,
free_delivery: bool = False,
include_sold_out: bool = False,
) -> list[OhouDeal]:
"""단일 루프로 모든 필터 조건을 검사한다."""
needle = query.casefold() if query else None
filtered: list[OhouDeal] = []
for deal in deals:
if not include_sold_out and deal.sold_out:
continue
if needle and needle not in deal.title.casefold() and needle not in (deal.brand or "").casefold():
continue
if min_discount is not None and (deal.best_discount_rate or deal.discount_rate or 0) < min_discount:
continue
if free_delivery and not deal.free_delivery:
continue
filtered.append(deal)
return filtered
def sort_deals(deals: list[OhouDeal], sort_key: str) -> list[OhouDeal]:
if sort_key == "discount":
return sorted(
deals,
key=lambda deal: (deal.best_discount_rate or deal.discount_rate or -1, deal.review_count),
reverse=True,
)
if sort_key == "price":
return sorted(deals, key=lambda deal: deal.best_price or deal.selling_price or sys.maxsize)
if sort_key == "review":
return sorted(deals, key=lambda deal: (deal.review_count, deal.review_average or 0), reverse=True)
if sort_key == "annual-sales":
return sorted(deals, key=lambda deal: deal.annual_sales or -1, reverse=True)
return deals
def build_payload(args: argparse.Namespace) -> dict[str, Any]:
document = Path(args.html_file).read_text(encoding="utf-8") if args.html_file else fetch_html(args.url)
payload = extract_next_data(document)
deals = extract_deals(payload)
filtered = filter_deals(
deals,
query=args.query,
min_discount=args.min_discount,
free_delivery=args.free_delivery,
include_sold_out=args.include_sold_out,
)
sorted_deals = sort_deals(filtered, args.sort)
limited_deals = sorted_deals[: args.limit]
kst = timezone(timedelta(hours=9))
now_utc = datetime.now(timezone.utc)
return {
"source": {
"name": "ohou-today-deal",
"url": args.url,
"fetched_at": now_utc.isoformat(),
"fetched_at_kst": now_utc.astimezone(kst).strftime("%Y-%m-%d %H:%M:%S KST"),
"surface": "__NEXT_DATA__ today-deal-feed + special-today-deal-feed",
},
"filters": {
"query": args.query,
"min_discount": args.min_discount,
"free_delivery": args.free_delivery,
"include_sold_out": args.include_sold_out,
"sort": args.sort,
"limit": args.limit,
},
"count": len(limited_deals),
"total_count": len(deals),
"filtered_count": len(filtered),
"items": [deal.to_dict() for deal in limited_deals],
}
def _positive_int(value: str) -> int:
try:
parsed = int(value)
except ValueError as exc:
raise argparse.ArgumentTypeError(f"expected integer, got {value!r}") from exc
if parsed <= 0:
raise argparse.ArgumentTypeError(f"must be a positive integer, got {parsed}")
return parsed
def _discount_rate(value: str) -> int:
try:
parsed = int(value)
except ValueError as exc:
raise argparse.ArgumentTypeError(f"expected integer, got {value!r}") from exc
if not 0 <= parsed <= 100:
raise argparse.ArgumentTypeError(
f"discount rate must be between 0 and 100, got {parsed}"
)
return parsed
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Read Ohouse today deal products from public HTML.")
subparsers = parser.add_subparsers(dest="command", required=True)
list_parser = subparsers.add_parser("list", help="오늘의집 오늘의딜 상품 목록")
list_parser.add_argument("--url", default=DEFAULT_URL)
list_parser.add_argument("--html-file", help="테스트/오프라인 검증용 HTML 또는 JSON 파일")
list_parser.add_argument("--query", help="상품명 또는 브랜드 키워드")
list_parser.add_argument("--min-discount", type=_discount_rate, help="최소 할인율 (0~100)")
list_parser.add_argument("--free-delivery", action="store_true", help="무료배송 상품만")
list_parser.add_argument("--include-sold-out", action="store_true", help="품절 상품 포함")
list_parser.add_argument(
"--sort",
choices=["default", "discount", "price", "review", "annual-sales"],
default="discount",
)
list_parser.add_argument("--limit", type=_positive_int, default=10, help="결과 개수 (양의 정수)")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> None:
args = parse_args(argv)
if args.command == "list":
print(json.dumps(build_payload(args), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
skill.json
{
"name": "ohou-today-deal",
"description": "오늘의집 공개 오늘의딜 페이지에서 로그인 없이 특가 상품을 조회하고 할인율, 가격, 리뷰, 링크를 정리하는 읽기 전용 스킬. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
"profiles": [
"browser",
"action:commerce"
],
"frontmatter": "name: ohou-today-deal\ndescription: 오늘의집 공개 오늘의딜 페이지에서 로그인 없이 특가 상품을 조회하고 할인율, 가격, 리뷰, 링크를 정리하는 읽기 전용 스킬. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: retail\n locale: ko-KR\n phase: v1"
}
SKILL.md
---
name: ohou-today-deal
description: 오늘의집 공개 오늘의딜 페이지에서 로그인 없이 특가 상품을 조회하고 할인율, 가격, 리뷰, 링크를 정리하는 읽기 전용 스킬. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.
license: MIT
metadata:
category: retail
locale: ko-KR
phase: v1
---
# ohou-today-deal
<!-- k-skill:cli-stub — generated by scripts/generate-skill-stubs.js; edit skill.json / instruction.md instead -->
## Get the full instructions (required first step)
Run this and follow its output as the primary instructions for this skill:
```bash
npx -y @nomadamas/k-skill@0 instruct ohou-today-deal
```
The CLI detects the current runtime (Dolshoi vault/CloakBrowser vs generic) and prints only the applicable instructions, always up to date. Helper files bundled with the CLI are listed by:
```bash
npx -y @nomadamas/k-skill@0 files ohou-today-deal
```
Keep the CLI and every coding-agent skill install current (including Vercel Agent Skills) with:
```bash
npx -y @nomadamas/k-skill@0 update
```
If `npx` is unavailable, install Node.js 18+ or follow https://github.com/NomaDamas/k-skill#readme, or read the source instructions at https://github.com/NomaDamas/k-skill/blob/main/ohou-today-deal/instruction.md.
## Legal disclaimer (required)
This skill is not an official feature of, officially supported by, affiliated with, sponsored by, approved by, or developed in collaboration with any third-party trademark owner or service operator it identifies. Third-party names are used only to describe the skill's function, lookup target, or compatibility.
Any automated collection of publicly accessible information must be limited to personal, non-organizational lookup. Do not use this skill for systematic or bulk crawling, database building, access-control or block circumvention, or conduct that interferes with a third party's business or service.
Read the full Korean legal disclaimer, including the cited Korean Supreme Court precedents and statutory limits, before use:
```bash
npx -y @nomadamas/k-skill@0 read ohou-today-deal references/DISCLAIMER.md
```
## Hard rules even without the CLI
- Never execute payment, message/email delivery, final submission, cancellation, or public posting without the user's explicit approval immediately beforehand.
- Never ask for, print, or store plaintext credentials in chat, files, or shell arguments.
- Never bypass legal, physical-presence, CAPTCHA, identity-proofing, or electronic-signature boundaries.