
Ohou Today Deal
- 1.7k installs
- 7k repo stars
- Updated August 2, 2026
- nomadamas/k-skill
ohou-today-deal is an agent skill that lists Ohou today-deal specials with discounts, prices, reviews, and filters from public __NEXT_DATA__.
About
The ohou-today-deal skill queries the public Ohou today deals page at ohou.se/commerces/today_deals without login. It parses server-rendered __NEXT_DATA__ dehydratedState for today-deal-feed and special-today-deal-feed slots only, ignoring other DEAL nodes elsewhere on the page. The Python script returns product title, brand, URLs, original and selling prices, discount rates, coupon-adjusted best price, review counts, ratings, free delivery flags, and sold-out status. Filters support query keywords, minimum discount, free delivery only, sort by discount price review or annual sales, and result limits with optional offline HTML fixtures. Requests use a well-formed k-skill User-Agent to pass Akamai bot checks without CAPTCHA bypass. The skill is read-only and excludes cart, checkout, personalized coupons, or guaranteed real-time inventory.
- Reads Ohou today-deal and special-today-deal feeds from __NEXT_DATA__ only.
- Supports keyword, min discount, free delivery, sort, and limit filters.
- Reports coupon-adjusted best price plus review count and rating fields.
- Uses honest k-skill User-Agent instead of CAPTCHA or blocking bypass tricks.
- Explicitly excludes login, purchase automation, and guaranteed live stock counts.
Ohou Today Deal by the numbers
- 1,743 all-time installs (skills.sh)
- +237 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #91 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ohou-today-deal capabilities & compatibility
- Capabilities
- __next_data__ deal feed parsing · discount and best price normalization · keyword and free delivery filtering · review and brand metadata extraction · read only public page access
- Use cases
- research · orchestration
What ohou-today-deal says it does
로그인, 장바구니, 구매, 결제 자동화 — 이 스킬은 의도적으로 구매 플로우를 포함하지 않는다.
npx skills add https://github.com/nomadamas/k-skill --skill ohou-today-dealAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.7k |
|---|---|
| repo stars | ★ 7k |
| Last updated | August 2, 2026 |
| Repository | nomadamas/k-skill ↗ |
What discounted Ohou today-deal products are available now with price, review, and delivery filters?
List read-only Ohou today-deal specials with discount, price, review, and filter options from public __NEXT_DATA__.
Who is it for?
Agents answering Korean home-goods deal lookup questions from the public Ohou today deals page.
Skip if: Skip for cart, checkout, login flows, or legally binding price guarantees.
When should I use this skill?
User asks for Ohou today deals, high-discount specials, or filtered free-delivery promotions.
What you get
A JSON or summarized list of deal items with discount rates, best prices, reviews, links, and fetch metadata.
Files
오늘의집 오늘의딜 조회
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 QuerydehydratedState에서today-deal-feed,special-today-deal-feedqueryKey 두 곳의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. 오늘의딜 상품 조회
오늘의집 오늘의딜 공개 페이지에서 상품 목록을 가져온다. 기본 정렬은 할인율 높은 순이다.
python3 ohou-today-deal/scripts/ohou_today_deal.py list --limit 10응답 예시:
{
"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를 사용한다.
python3 ohou-today-deal/scripts/ohou_today_deal.py list \
--sort discount \
--limit 5정렬 옵션: discount(할인율), price(낮은 가격), review(리뷰 많은 순), annual-sales(연간 판매량).
3. 키워드·할인율·무료배송 필터
상품명 또는 브랜드에 키워드가 포함된 상품만 걸러내고, 최소 할인율과 무료배송 조건을 조합할 수 있다.
python3 ohou-today-deal/scripts/ohou_today_deal.py list \
--query 러그 \
--min-discount 30 \
--free-delivery \
--limit 54. 오프라인 fixture로 검증
실제 네트워크 없이 저장된 HTML/JSON 파일로 동일한 파싱을 테스트한다.
python3 ohou-today-deal/scripts/ohou_today_deal.py list \
--html-file ./today-deals.html \
--limit 3Output 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 표준 라이브러리만 사용한다.
#!/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()
Related skills
FAQ
Does ohou-today-deal support purchasing?
No. It is read-only and intentionally excludes cart, checkout, and payment automation.
Where does it read deal data?
From __NEXT_DATA__ dehydratedState slots for today-deal-feed and special-today-deal-feed on the public page.
How are best prices calculated?
It prefers bestDiscountPrice.discountRate when present, otherwise falls back to the product discountRate.