mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Prepare k-skill for packaged releases and broader skill discovery
This snapshots the current repository updates as a coherent release-prep baseline: workspace/package scaffolding, release automation docs and workflows, refreshed skill/setup documentation, roadmap expansion, and the README thumbnail polish. Constraint: Node packages in this repo must use npm workspaces and Changesets for releases Constraint: Python release automation stays scaffold-only until a real package exists Rejected: Split the current work into multiple commits | user asked to commit the current changes together Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep release docs, workflows, and package metadata aligned when adding future packages Tested: npm run ci Not-tested: GitHub Actions execution on remote after push
This commit is contained in:
parent
ec41ff08dc
commit
720964cf49
40 changed files with 2456 additions and 68 deletions
5
.changeset/calm-melons-smoke.md
Normal file
5
.changeset/calm-melons-smoke.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"k-lotto": minor
|
||||
---
|
||||
|
||||
Add the initial official dhlottery-backed k-lotto package.
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": false,
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
3
.github/release-please/python-config.json
vendored
Normal file
3
.github/release-please/python-config.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"packages": {}
|
||||
}
|
||||
1
.github/release-please/python-manifest.json
vendored
Normal file
1
.github/release-please/python-manifest.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
21
.github/workflows/ci.yml
vendored
Normal file
21
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run ci
|
||||
45
.github/workflows/release-npm.yml
vendored
Normal file
45
.github/workflows/release-npm.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
name: Release npm packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".changeset/**"
|
||||
- ".github/workflows/release-npm.yml"
|
||||
- "package-lock.json"
|
||||
- "package.json"
|
||||
- "packages/**"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- run: npm ci
|
||||
- run: npm run ci
|
||||
|
||||
- name: Create npm release PR or publish changed packages
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
version: npm run version-packages
|
||||
publish: npm run release:npm
|
||||
commit: "chore: version packages"
|
||||
title: "chore: version packages"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
41
.github/workflows/release-python.yml
vendored
Normal file
41
.github/workflows/release-python.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
name: Release Python packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ".github/release-please/**"
|
||||
- ".github/workflows/release-python.yml"
|
||||
- "python-packages/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
scaffold-only:
|
||||
if: ${{ hashFiles('python-packages/**/pyproject.toml') == '' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "No Python package exists yet. release-please remains scaffold-only."
|
||||
|
||||
release:
|
||||
if: ${{ hashFiles('python-packages/**/pyproject.toml') != '' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: release
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
config-file: .github/release-please/python-config.json
|
||||
manifest-file: .github/release-please/python-manifest.json
|
||||
|
||||
- name: Reminder
|
||||
if: ${{ steps.release.outputs.releases_created == 'true' }}
|
||||
run: |
|
||||
echo "Python package release metadata was created."
|
||||
echo "Wire package-specific build/publish steps here when the first python package is added."
|
||||
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# k-skill repository instructions
|
||||
|
||||
This repository inherits the broader oh-my-codex guidance from the parent environment.
|
||||
These rules are repo-specific and apply to everything under this directory.
|
||||
|
||||
## Release automation rules
|
||||
|
||||
- Node packages live under `packages/*` and use npm workspaces.
|
||||
- Node package releases use **Changesets**. Do not hand-edit package versions only to cut a release; add a `.changeset/*.md` file instead.
|
||||
- npm publish is automated from GitHub Actions and should happen only after the bot-generated **Version Packages** PR is merged into `main`.
|
||||
- Python packages live under `python-packages/*` and use **release-please**. Until a real Python package exists, keep the Python release workflow as scaffold-only.
|
||||
- PyPI publish should run only when release-please reports `release_created=true` for a concrete package path.
|
||||
- Prefer trusted publishing via OIDC for npm and PyPI. Do not introduce long-lived registry tokens unless trusted publishing is unavailable.
|
||||
|
||||
## Verification rules
|
||||
|
||||
- For release or packaging changes, run `npm run ci`.
|
||||
- Keep release docs, workflow files, and package metadata aligned in the same change.
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
# k-skill
|
||||
|
||||

|
||||
|
||||
한국 서비스와 한국 생활 맥락에 맞춰 바로 쓸 수 있는 에이전트 스킬 모음집입니다.
|
||||
|
||||
## 어떤 걸 할 수 있나
|
||||
|
|
@ -16,8 +18,9 @@
|
|||
|
||||
1. [설치 방법](docs/install.md)부터 보고 필요한 스킬만 설치합니다.
|
||||
2. SRT/KTX/서울 지하철처럼 인증이 필요한 기능을 쓸 거라면 [공통 설정 가이드](docs/setup.md)를 먼저 따라갑니다.
|
||||
3. 시크릿을 안전하게 관리하려면 [보안/시크릿 정책](docs/security-and-secrets.md)을 확인합니다.
|
||||
4. 각 기능 문서를 열어 입력값, 예시, 제한사항을 확인합니다.
|
||||
3. 시크릿이 비어 있으면 값을 채팅에 붙여 넣지 말고, [공통 설정 가이드](docs/setup.md)와 [보안/시크릿 정책](docs/security-and-secrets.md)에 따라 로컬에 안전하게 등록합니다.
|
||||
4. Node/Python 패키지가 없으면 먼저 전역 설치를 기본으로 진행합니다.
|
||||
5. 각 기능 문서를 열어 입력값, 예시, 제한사항을 확인합니다.
|
||||
|
||||
## 문서
|
||||
|
||||
|
|
@ -26,6 +29,7 @@
|
|||
| [설치 방법](docs/install.md) | 패키지 설치, 선택 설치, 로컬 테스트 방법 |
|
||||
| [공통 설정 가이드](docs/setup.md) | `sops + age` 설치, age key 생성, 공통 secrets 파일 준비 |
|
||||
| [보안/시크릿 정책](docs/security-and-secrets.md) | 인증 정보 저장 원칙, 금지 패턴, 표준 환경변수 이름 |
|
||||
| [릴리스/배포 가이드](docs/releasing.md) | npm Changesets, Python release-please, trusted publishing 운영 규칙 |
|
||||
| [로드맵](docs/roadmap.md) | 현재 포함 기능과 다음 후보 |
|
||||
| [출처/참고 표면](docs/sources.md) | 설계 시 참고한 공개 라이브러리와 공식 문서 |
|
||||
|
||||
|
|
@ -36,5 +40,6 @@
|
|||
- [서울 지하철 도착정보 조회](docs/features/seoul-subway-arrival.md)
|
||||
- [KBO 경기 결과 조회](docs/features/kbo-results.md)
|
||||
- [로또 당첨 확인](docs/features/lotto-results.md)
|
||||
- [릴리스/배포 가이드](docs/releasing.md)
|
||||
|
||||
인증이 필요한 기능은 모두 [공통 설정 가이드](docs/setup.md)를 먼저 보는 것을 기준으로 합니다.
|
||||
|
|
|
|||
BIN
docs/assets/k-skill-thumbnail.png
Normal file
BIN
docs/assets/k-skill-thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
|
|
@ -9,7 +9,7 @@
|
|||
## 먼저 필요한 것
|
||||
|
||||
- Node.js 18+
|
||||
- `npm install kbo-game`
|
||||
- `npm install -g kbo-game`
|
||||
|
||||
## 입력값
|
||||
|
||||
|
|
@ -18,15 +18,22 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. 날짜 기준으로 경기 데이터를 조회합니다.
|
||||
2. 홈팀/원정팀, 경기 상태, 스코어를 사람 읽기 좋은 형태로 정리합니다.
|
||||
3. 팀 요청이 있으면 해당 팀 경기만 남깁니다.
|
||||
1. 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 전역 설치합니다.
|
||||
2. 날짜 기준으로 경기 데이터를 조회합니다.
|
||||
3. 홈팀/원정팀, 경기 상태, 스코어를 사람 읽기 좋은 형태로 정리합니다.
|
||||
4. 팀 요청이 있으면 해당 팀 경기만 남깁니다.
|
||||
|
||||
## 예시
|
||||
|
||||
```bash
|
||||
node --input-type=module - <<'JS'
|
||||
import { getGame } from "kbo-game";
|
||||
GLOBAL_NPM_ROOT="$(npm root -g)" node --input-type=module - <<'JS'
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const entry = pathToFileURL(
|
||||
path.join(process.env.GLOBAL_NPM_ROOT, "kbo-game", "dist", "index.js"),
|
||||
).href;
|
||||
const { getGame } = await import(entry);
|
||||
|
||||
const date = "2026-03-25";
|
||||
const games = await getGame(new Date(`${date}T00:00:00+09:00`));
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
## 먼저 필요한 것
|
||||
|
||||
- Python 3.10+
|
||||
- `python -m pip install korail2`
|
||||
- `python3 -m pip install korail2`
|
||||
- [공통 설정 가이드](../setup.md) 완료
|
||||
- [보안/시크릿 정책](../security-and-secrets.md) 확인
|
||||
|
||||
|
|
@ -31,16 +31,18 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. 먼저 열차를 조회합니다.
|
||||
2. 후보 열차의 출발/도착 시각, KTX 여부, 좌석 여부, 가격을 보여줍니다.
|
||||
3. 대상 열차가 명확할 때만 예약합니다.
|
||||
4. 예약 확인/취소는 대상 예약을 다시 식별한 뒤 진행합니다.
|
||||
1. `korail2` 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 전역 설치합니다.
|
||||
2. `KSKILL_KTX_ID`, `KSKILL_KTX_PASSWORD` 가 없으면 채팅에 붙여 넣게 하지 말고 로컬 secrets 등록 절차를 안내합니다.
|
||||
3. 먼저 열차를 조회합니다.
|
||||
4. 후보 열차의 출발/도착 시각, KTX 여부, 좌석 여부, 가격을 보여줍니다.
|
||||
5. 대상 열차가 명확할 때만 예약합니다.
|
||||
6. 예약 확인/취소는 대상 예약을 다시 식별한 뒤 진행합니다.
|
||||
|
||||
## 예시
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from korail2 import Korail, TrainType
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
## 먼저 필요한 것
|
||||
|
||||
- Node.js 18+
|
||||
- `npm install korean-lotto`
|
||||
- 배포 후: `npm install -g k-lotto`
|
||||
- 실행 전: `export NODE_PATH="$(npm root -g)"`
|
||||
- 이 저장소에서 개발할 때: 루트에서 `npm install`
|
||||
|
||||
## 입력값
|
||||
|
||||
|
|
@ -19,20 +21,21 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. 최신 회차 또는 요청 회차를 확인합니다.
|
||||
2. 당첨번호와 추첨일, 당첨금 분포를 요약합니다.
|
||||
3. 사용자 번호가 있으면 일치 번호와 등수를 계산합니다.
|
||||
1. 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 전역 설치합니다.
|
||||
2. 최신 회차 또는 요청 회차를 확인합니다.
|
||||
3. 당첨번호와 추첨일, 당첨금 분포를 요약합니다.
|
||||
4. 사용자 번호가 있으면 일치 번호와 등수를 계산합니다.
|
||||
|
||||
## 예시
|
||||
|
||||
```bash
|
||||
node - <<'JS'
|
||||
const lotto = require("korean-lotto");
|
||||
lotto.getDetailResult(861).then((result) => console.log(JSON.stringify(result, null, 2)));
|
||||
NODE_PATH="$(npm root -g)" node - <<'JS'
|
||||
const lotto = require("k-lotto");
|
||||
lotto.getDetailResult(1216).then((result) => console.log(JSON.stringify(result, null, 2)));
|
||||
JS
|
||||
```
|
||||
|
||||
## 주의할 점
|
||||
|
||||
- 사용자 번호는 영구 저장하지 않는 것을 기준으로 합니다.
|
||||
- 최신 회차 반영은 upstream 사정에 따라 늦을 수 있습니다.
|
||||
- 최신 회차는 결과 페이지 HTML에서 읽고, 상세 결과는 공식 JSON 응답을 사용합니다.
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. API key가 안전하게 주입되는지 확인합니다.
|
||||
2. 역명 기준으로 실시간 도착정보를 조회합니다.
|
||||
3. 호선, 진행 방향, 도착 메시지, 조회 시점을 함께 요약합니다.
|
||||
1. `SEOUL_OPEN_API_KEY` 가 없으면 채팅에 붙여 넣게 하지 말고 로컬 secrets 등록 절차를 안내합니다.
|
||||
2. API key가 안전하게 주입되는지 확인합니다.
|
||||
3. 역명 기준으로 실시간 도착정보를 조회합니다.
|
||||
4. 호선, 진행 방향, 도착 메시지, 조회 시점을 함께 요약합니다.
|
||||
|
||||
## 예시
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
## 먼저 필요한 것
|
||||
|
||||
- Python 3.10+
|
||||
- `python -m pip install SRTrain`
|
||||
- `python3 -m pip install SRTrain`
|
||||
- [공통 설정 가이드](../setup.md) 완료
|
||||
- [보안/시크릿 정책](../security-and-secrets.md) 확인
|
||||
|
||||
|
|
@ -31,16 +31,18 @@
|
|||
|
||||
## 기본 흐름
|
||||
|
||||
1. 먼저 열차를 조회합니다.
|
||||
2. 후보 열차의 출발/도착 시각, 좌석 여부, 운임을 보여줍니다.
|
||||
3. 대상 열차가 명확할 때만 예약합니다.
|
||||
4. 예약 확인/취소는 예약을 다시 식별한 뒤 진행합니다.
|
||||
1. `SRTrain` 패키지가 없으면 다른 방법으로 우회하지 말고 먼저 전역 설치합니다.
|
||||
2. `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD` 가 없으면 채팅에 붙여 넣게 하지 말고 로컬 secrets 등록 절차를 안내합니다.
|
||||
3. 먼저 열차를 조회합니다.
|
||||
4. 후보 열차의 출발/도착 시각, 좌석 여부, 운임을 보여줍니다.
|
||||
5. 대상 열차가 명확할 때만 예약합니다.
|
||||
6. 예약 확인/취소는 예약을 다시 식별한 뒤 진행합니다.
|
||||
|
||||
## 예시
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from SRT import SRT
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,32 @@ npx --yes skills add <owner/repo> \
|
|||
npx --yes skills add . --list
|
||||
```
|
||||
|
||||
유지보수자가 패키지/릴리스 설정까지 같이 검증하려면:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run ci
|
||||
```
|
||||
|
||||
## 패키지가 없을 때의 기본 동작
|
||||
|
||||
스킬 실행에 필요한 Node/Python 패키지가 없으면 다른 방법으로 우회하지 말고 전역 설치를 먼저 시도하는 것을 기본으로 합니다.
|
||||
|
||||
### Node 패키지
|
||||
|
||||
```bash
|
||||
npm install -g kbo-game k-lotto
|
||||
export NODE_PATH="$(npm root -g)"
|
||||
```
|
||||
|
||||
### Python 패키지
|
||||
|
||||
```bash
|
||||
python3 -m pip install SRTrain korail2
|
||||
```
|
||||
|
||||
운영체제 정책이나 권한 때문에 전역 설치가 막히면, 임의의 대체 구현으로 넘어가지 말고 그 차단 사유를 사용자에게 설명한 뒤 다음 설치 단계를 정합니다.
|
||||
|
||||
## npx도 없으면
|
||||
|
||||
`npx`, `pnpm dlx`, `bunx` 중 아무것도 없으면 먼저 Node.js 계열 런타임을 설치해야 한다.
|
||||
|
|
|
|||
41
docs/releasing.md
Normal file
41
docs/releasing.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# 릴리스와 자동 배포
|
||||
|
||||
이 저장소는 **npm은 Changesets**, **Python은 release-please**로 관리한다.
|
||||
|
||||
## Node / npm 패키지
|
||||
|
||||
- 위치: `packages/*`
|
||||
- 버전 관리: Changesets
|
||||
- 배포 workflow 파일: `.github/workflows/release-npm.yml`
|
||||
- 실제 publish 시점: **Version Packages PR을 merge한 뒤 `main`에 push가 발생했을 때**
|
||||
- 기본 규칙: 패키지 버전을 직접 손으로 올리지 말고 `.changeset/*.md` 파일을 추가한다.
|
||||
|
||||
### 흐름
|
||||
|
||||
1. 기능 PR에서 `.changeset/*.md` 추가
|
||||
2. PR merge
|
||||
3. Changesets가 Version Packages PR 생성
|
||||
4. Version Packages PR merge
|
||||
5. GitHub Actions가 변경된 npm 패키지만 publish
|
||||
|
||||
## Python 패키지
|
||||
|
||||
- 위치: `python-packages/*`
|
||||
- 버전 관리: release-please
|
||||
- 배포 workflow 파일: `.github/workflows/release-python.yml`
|
||||
- 실제 publish 시점: **release-please가 `release_created=true`를 만든 run**
|
||||
- 현재 상태: 실제 Python 패키지가 없어 scaffold only
|
||||
|
||||
## Trusted publishing 원칙
|
||||
|
||||
- npm과 PyPI 모두 OIDC trusted publishing을 우선 사용한다.
|
||||
- 장기 토큰(`NPM_TOKEN`, `PYPI_API_TOKEN`)은 fallback이 아니면 만들지 않는다.
|
||||
- npm trusted publisher 등록 시 workflow filename은 `release-npm.yml`이다.
|
||||
- PyPI trusted publisher 등록 시 workflow filename은 `release-python.yml`이다.
|
||||
|
||||
## Maintainer 확인 명령
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run ci
|
||||
```
|
||||
|
|
@ -12,27 +12,113 @@
|
|||
|
||||
## v1.5 candidates
|
||||
|
||||
### 네이버 스마트스토어
|
||||
### 진짜 우선순위 높은 후보
|
||||
|
||||
#### 정부24 조회/발급/신청
|
||||
|
||||
- 장점: 등본·초본, 전입신고, 자동차등록원부, 건축물대장, 토지대장, 각종 사실확인, 보조금24까지 한 축으로 묶을 수 있다
|
||||
- 이유: “한국 생활 운영체제”에 가장 가까운 범용 허브 후보다
|
||||
|
||||
#### 홈택스/손택스 도우미
|
||||
|
||||
- 장점: 세금 납부, 종합소득세·양도소득세 신고 보조, 연말정산 계산, 각종 세무 증명까지 커버할 수 있다
|
||||
- 이유: 사용 빈도는 낮아도 필요할 때 고통이 커서 만족도가 높다
|
||||
|
||||
#### 토스 생활금융 스킬
|
||||
|
||||
- 장점: 계좌·카드 모아보기, 송금, 사기계좌 조회, 자동이체 예약, 신용점수 확인, 세금 납부, 등초본 발급까지 이어질 수 있다
|
||||
- 이유: 금융 + 생활 민원을 한곳에서 묶는 허브 포지션이 좋다
|
||||
|
||||
#### 카카오페이 송금/결제/청구서
|
||||
|
||||
- 장점: 송금, 결제, 멤버십, 자산관리, 청구서 등 생활 결제 액션이 넓다
|
||||
- 이유: 카톡과 연결되는 체감 가치가 커서 대중성이 높다
|
||||
|
||||
#### 카카오 T 이동 허브
|
||||
|
||||
- 장점: 택시, 대리, 주차, 기차, 시외버스, 퀵·택배까지 이동 관련 수요를 넓게 묶을 수 있다
|
||||
- 이유: 이동/귀가/주차/예약을 하나의 생활 축으로 만들기 좋다
|
||||
|
||||
#### 네이버지도 / TMAP 길찾기·장소저장·교통
|
||||
|
||||
- 장점: 길찾기에서 출발해 장소 저장, 리뷰, 추천, 대중교통, 주차, 대리까지 확장 폭이 넓다
|
||||
- 이유: 네이버 생활 서비스와 TMAP 이동 서비스 모두로 확장 가능한 교통 베이스다
|
||||
|
||||
#### 배달의민족 / 요기요 주문
|
||||
|
||||
- 장점: 음식배달뿐 아니라 장보기·쇼핑·선물까지 붙여서 즉시 생활구매 축으로 키울 수 있다
|
||||
- 이유: “배달”보다 넓은 실사용 구매 액션으로 이어진다
|
||||
|
||||
#### 병원 접수/예약 스킬
|
||||
|
||||
- 장점: 사전 접수, 주변 병원 찾기, 지금 문 연 병원 찾기 흐름이 명확하다
|
||||
- 이유: 실사용 가치가 높고 특히 부모층 체감이 크다
|
||||
|
||||
#### 택배 조회/예약
|
||||
|
||||
- 장점: 배송조회, 일반 택배예약, 국제특송조회까지 범용 작업 빈도가 높다
|
||||
- 이유: 한국 생활에서 예상보다 자주 반복되는 작업이다
|
||||
|
||||
#### 미세먼지/황사/대기질 알림
|
||||
|
||||
- 장점: 오늘/내일/모레 대기정보와 예보, 하루 4회 수준의 예보 갱신 같은 한국형 수요에 잘 맞는다
|
||||
- 이유: 한국 로컬 생활 스킬로 차별화가 쉽다
|
||||
|
||||
### 그다음으로 좋은 후보
|
||||
|
||||
#### 모바일 신분증 발급/재발급/분실 가이드
|
||||
|
||||
- 장점: 모바일 주민등록증·운전면허증 발급 흐름 정리에 특화할 수 있다
|
||||
- 이유: 한국 특화성이 강하고 가이드형 스킬로 출발하기 좋다
|
||||
|
||||
#### 버스/지하철 도착정보 조회
|
||||
|
||||
- 장점: 주변 정류소, 지하철, 공항버스, 버스정보 조회까지 출퇴근 수요가 강하다
|
||||
- 이유: 이미 검증된 반복 조회 패턴이라 확장하기 쉽다
|
||||
|
||||
#### 네이버 생활 허브
|
||||
|
||||
- 장점: 날씨, 뉴스, 스포츠, 네이버페이, 가격비교, 배송, 지도, QR, 전자증명서까지 한 축으로 확장 가능하다
|
||||
- 이유: 네이버 한 축만 잘 잡아도 생활 플랫폼 허브가 된다
|
||||
|
||||
#### 공과금/청구서 납부 정리
|
||||
|
||||
- 장점: 카카오페이/토스와 연결해 전기·가스·통신·카드 청구서 조회/납부/알림으로 묶을 수 있다
|
||||
- 이유: 생활 결제 자동화의 실용성이 높다
|
||||
|
||||
#### 네이버페이/포인트/가격비교
|
||||
|
||||
- 장점: 결제/포인트/배송/가격비교를 묶는 쇼핑형 허브 후보가 된다
|
||||
- 이유: 스마트스토어와 별도의 큰 축으로 키울 수 있다
|
||||
|
||||
#### 한국 기상청 날씨/특보
|
||||
|
||||
- 장점: 국내 날씨, 특보, 단기 예보를 한국형 생활 정보로 직접 연결하기 좋다
|
||||
- 이유: 미세먼지 스킬과 함께 묶었을 때 생활 밀착도가 더 올라간다
|
||||
|
||||
### 기존 탐색 후보
|
||||
|
||||
#### 네이버 스마트스토어
|
||||
|
||||
- 장점: 실제 수요가 크다
|
||||
- 보류 이유: 공식 Commerce API auth/setup이 가볍지 않다
|
||||
|
||||
### 다나와 가격 비교
|
||||
#### 다나와 가격 비교
|
||||
|
||||
- 장점: 검색 수요가 명확하다
|
||||
- 보류 이유: 안정적인 공개 CLI를 아직 못 찾았다
|
||||
|
||||
### 카카오톡 조회/전송
|
||||
#### 카카오톡 조회/전송
|
||||
|
||||
- 장점: 어그로가 매우 강하다
|
||||
- 보류 이유: 계정/정책 리스크가 크다
|
||||
|
||||
### HWP 문서 편집
|
||||
#### HWP 문서 편집
|
||||
|
||||
- 장점: 한국 로컬리티가 매우 강하다
|
||||
- 보류 이유: 믿을 만한 자동화 표면이 아직 얇다
|
||||
|
||||
### 당근 자동 거래
|
||||
#### 당근 자동 거래
|
||||
|
||||
- 장점: 바이럴 포텐셜이 높다
|
||||
- 보류 이유: 계정 제재 리스크와 UI automation 의존도가 높다
|
||||
|
|
|
|||
|
|
@ -2,6 +2,25 @@
|
|||
|
||||
`k-skill`은 인증이 필요한 스킬에서 비밀번호나 토큰을 채팅창에 직접 붙여 넣는 방식을 허용하지 않는다. 기본 원칙은 "비밀값은 암호화된 파일로 보관하고, 런타임에만 주입"이다.
|
||||
|
||||
## Missing secret handling policy
|
||||
|
||||
인증이 필요한 스킬에서 필요한 값이 없으면 우회하지 않는다.
|
||||
|
||||
- 어떤 값이 비어 있는지 정확한 환경변수 이름으로 사용자에게 알려준다
|
||||
- 그 값을 채팅창에 붙여 넣으라고 하지 않는다
|
||||
- 대체 사이트, 대체 API, 하드코딩, 임시 평문 `.env` 파일 같은 우회 경로를 찾지 않는다
|
||||
- 사용자가 직접 로컬에 안전하게 등록하도록 안내한 뒤 다시 진행한다
|
||||
|
||||
안내 기본형:
|
||||
|
||||
1. 필요한 값 이름을 짚는다. 예: `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD`
|
||||
2. `~/.config/k-skill/secrets.env.plain` 에 값을 적고
|
||||
3. `sops`로 `~/.config/k-skill/secrets.env` 로 암호화한 뒤
|
||||
4. plaintext 파일을 지우고
|
||||
5. `bash scripts/check-setup.sh` 로 다시 확인하게 한다
|
||||
|
||||
즉, "시크릿이 없으면 사용자에게 필요한 정보를 요청하고, 안전한 등록 절차를 안내한 뒤 멈춘다"가 기본 동작이다.
|
||||
|
||||
## Required
|
||||
|
||||
- `sops`
|
||||
|
|
@ -56,6 +75,7 @@ sops exec-env "$HOME/.config/k-skill/secrets.env" '<command>'
|
|||
- 실제 비밀값이 들어있는 plaintext `.env` 파일을 git에 두기
|
||||
- 셸 히스토리에 남는 `export PASSWORD=...`
|
||||
- 스킬 문서 안에 예시용 실비밀번호를 쓰기
|
||||
- 시크릿이 없다는 이유로 다른 서비스나 비공식 우회 수단을 자동 채택하기
|
||||
|
||||
## Threat model notes
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ sudo pacman -S sops age
|
|||
winget install Mozilla.SOPS FiloSottile.age
|
||||
```
|
||||
|
||||
도구가 없으면 다른 비밀 관리 방식으로 우회하지 말고, 이 도구들을 먼저 설치하는 것을 기본으로 합니다.
|
||||
|
||||
## 2) age key 만들기
|
||||
|
||||
```bash
|
||||
|
|
@ -81,6 +83,23 @@ sops --encrypt --input-type dotenv --output-type dotenv \
|
|||
rm secrets.env.plain
|
||||
```
|
||||
|
||||
## 시크릿이 없을 때의 기본 응답
|
||||
|
||||
인증이 필요한 스킬에서 값이 비어 있으면 다음 식으로 안내하는 것을 기본으로 합니다.
|
||||
|
||||
- 어떤 값이 필요한지 정확한 변수 이름으로 알려주기
|
||||
- 그 값을 채팅에 보내지 말라고 안내하기
|
||||
- 아래 절차로 로컬에 직접 등록하게 하기
|
||||
|
||||
예:
|
||||
|
||||
```text
|
||||
이 작업에는 KSKILL_SRT_ID, KSKILL_SRT_PASSWORD 가 필요합니다.
|
||||
값을 채팅창에 붙여 넣지 말고, ~/.config/k-skill/secrets.env.plain 에 채운 뒤
|
||||
sops 로 ~/.config/k-skill/secrets.env 로 암호화해 주세요.
|
||||
끝나면 plaintext 파일은 지우고 bash scripts/check-setup.sh 로 다시 확인해 주세요.
|
||||
```
|
||||
|
||||
## 5) 런타임 주입 확인
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
- `SRTrain` / `ryanking13/SRT`: https://github.com/ryanking13/SRT
|
||||
- `korail2` / `carpedm20/korail2`: https://github.com/carpedm20/korail2
|
||||
- `kbo-game`: https://github.com/vkehfdl1/kbo-game
|
||||
- `korean-lotto`: https://github.com/hs85jeong/korean-lotto
|
||||
- 동행복권 로또 결과 페이지: https://www.dhlottery.co.kr/lt645/result
|
||||
- 동행복권 지난 회차 JSON 표면: https://www.dhlottery.co.kr/lt645/selectPstLt645InfoNew.do
|
||||
- 서울특별시 지하철 실시간 도착정보: https://www.data.go.kr/data/15058052/openapi.do
|
||||
- SOPS docs: https://getsops.io/docs/
|
||||
- age: https://github.com/FiloSottile/age
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ metadata:
|
|||
- 암호화 확인
|
||||
- 런타임 주입 확인
|
||||
|
||||
이 스킬의 기본 정책:
|
||||
|
||||
- 시크릿이 없으면 필요한 값 이름을 사용자에게 정확히 알려준다
|
||||
- 값을 채팅창에 붙여 넣으라고 하지 않는다
|
||||
- 로컬에 안전하게 등록하는 절차를 안내한 뒤 다시 진행한다
|
||||
- 필요한 패키지가 없으면 대체 구현을 찾기보다 전역 설치를 먼저 시도한다
|
||||
|
||||
## Why this is the default setup path
|
||||
|
||||
- 계정 가입이 필요 없다
|
||||
|
|
@ -118,6 +125,25 @@ sops --encrypt --input-type dotenv --output-type dotenv \
|
|||
rm secrets.env.plain
|
||||
```
|
||||
|
||||
### Missing secret response template
|
||||
|
||||
인증 스킬에서 값이 빠졌을 때는 다음 식으로 안내한다.
|
||||
|
||||
```text
|
||||
이 작업에는 <REQUIRED_SECRET_NAMES> 이 필요합니다.
|
||||
값을 채팅창에 보내지 말고 ~/.config/k-skill/secrets.env.plain 에 직접 채운 뒤
|
||||
sops 로 ~/.config/k-skill/secrets.env 로 암호화해 주세요.
|
||||
암호화가 끝나면 plaintext 파일은 지우고 bash scripts/check-setup.sh 로 다시 확인해 주세요.
|
||||
```
|
||||
|
||||
예를 들면:
|
||||
|
||||
- SRT: `KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD`
|
||||
- KTX: `KSKILL_KTX_ID`, `KSKILL_KTX_PASSWORD`
|
||||
- 서울 지하철: `SEOUL_OPEN_API_KEY`
|
||||
|
||||
시크릿이 비어 있다는 이유로 다른 서비스나 비공식 우회 경로를 자동 선택하지 않는다.
|
||||
|
||||
### 5. Verify runtime injection
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ metadata:
|
|||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- `npm install kbo-game`
|
||||
- `npm install -g kbo-game`
|
||||
|
||||
## Inputs
|
||||
|
||||
|
|
@ -32,11 +32,27 @@ metadata:
|
|||
|
||||
## Workflow
|
||||
|
||||
### 0. Install the package globally when missing
|
||||
|
||||
`npm root -g` 아래에 `kbo-game` 이 없으면 다른 구현으로 우회하지 말고 전역 Node 패키지 설치를 먼저 시도한다.
|
||||
|
||||
```bash
|
||||
npm install -g kbo-game
|
||||
```
|
||||
|
||||
패키지가 없다는 이유로 다른 비공식 scoreboard 소스를 자동 채택하지 않는다.
|
||||
|
||||
### 1. Fetch the date
|
||||
|
||||
```bash
|
||||
node --input-type=module - <<'JS'
|
||||
import { getGame } from "kbo-game";
|
||||
GLOBAL_NPM_ROOT="$(npm root -g)" node --input-type=module - <<'JS'
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const entry = pathToFileURL(
|
||||
path.join(process.env.GLOBAL_NPM_ROOT, "kbo-game", "dist", "index.js"),
|
||||
).href;
|
||||
const { getGame } = await import(entry);
|
||||
|
||||
const date = "2026-03-25";
|
||||
const games = await getGame(new Date(`${date}T00:00:00+09:00`));
|
||||
|
|
@ -44,7 +60,7 @@ console.log(JSON.stringify(games, null, 2));
|
|||
JS
|
||||
```
|
||||
|
||||
`kbo-game@0.0.2` 기준 실제 export는 `getGame` 하나이며, 문자열 날짜(`"2026-03-25"`)를 직접 넘기면 실패한다. 항상 `Date` 객체로 변환해서 호출한다.
|
||||
`kbo-game@0.0.2` 기준 실제 export는 `getGame` 하나이며, 문자열 날짜(`"2026-03-25"`)를 직접 넘기면 실패한다. 항상 `Date` 객체로 변환해서 호출한다. 전역 설치를 기본으로 쓰므로 inline snippet에서는 전역 npm root 아래 entry file을 직접 import 한다.
|
||||
|
||||
### 2. Normalize for humans
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ metadata:
|
|||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- `python -m pip install korail2`
|
||||
- `python3 -m pip install korail2`
|
||||
- `sops` and `age` installed
|
||||
- common setup reviewed in `../k-skill-setup/SKILL.md`
|
||||
- secret policy reviewed in `../docs/security-and-secrets.md`
|
||||
|
|
@ -51,11 +51,32 @@ metadata:
|
|||
|
||||
## Workflow
|
||||
|
||||
### 1. Search first
|
||||
### 0. Install the package globally when missing
|
||||
|
||||
`python3 -c 'import korail2'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Python 패키지 설치를 먼저 시도한다.
|
||||
|
||||
```bash
|
||||
python3 -m pip install korail2
|
||||
```
|
||||
|
||||
### 1. Stop for secure registration when secrets are missing
|
||||
|
||||
`KSKILL_KTX_ID`, `KSKILL_KTX_PASSWORD`, `~/.config/k-skill/secrets.env`, `~/.config/k-skill/age/keys.txt` 중 하나라도 없으면 다음 식으로 안내하고 멈춘다.
|
||||
|
||||
```text
|
||||
이 작업에는 KSKILL_KTX_ID, KSKILL_KTX_PASSWORD 가 필요합니다.
|
||||
값을 채팅창에 붙여 넣지 말고 ~/.config/k-skill/secrets.env.plain 에 직접 채운 뒤
|
||||
sops 로 ~/.config/k-skill/secrets.env 로 암호화해 주세요.
|
||||
암호화가 끝나면 plaintext 파일은 지우고 bash scripts/check-setup.sh 로 다시 확인해 주세요.
|
||||
```
|
||||
|
||||
시크릿이 없다는 이유로 웹사이트를 직접 긁거나 다른 비공식 경로를 찾지 않는다.
|
||||
|
||||
### 2. Search first
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from korail2 import Korail, TrainType
|
||||
|
||||
|
|
@ -77,7 +98,7 @@ PY
|
|||
'
|
||||
```
|
||||
|
||||
### 2. Present the shortlist
|
||||
### 3. Present the shortlist
|
||||
|
||||
예매 전에 항상 아래를 확인한다.
|
||||
|
||||
|
|
@ -86,11 +107,11 @@ PY
|
|||
- 좌석 가능 여부
|
||||
- 가격
|
||||
|
||||
### 3. Reserve only after the target train is unambiguous
|
||||
### 4. Reserve only after the target train is unambiguous
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from korail2 import AdultPassenger, Korail, ReserveOption, TrainType
|
||||
|
||||
|
|
@ -115,13 +136,13 @@ PY
|
|||
'
|
||||
```
|
||||
|
||||
### 4. Inspect or cancel
|
||||
### 5. Inspect or cancel
|
||||
|
||||
취소는 대상 예약을 다시 조회해 식별한 뒤에만 진행한다.
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from korail2 import Korail
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: lotto-results
|
||||
description: Check Korean Lotto draw results, latest rounds, and ticket matches with the korean-lotto npm package. Use when the user asks for winning numbers, payout details, or whether their numbers matched.
|
||||
description: Check Korean Lotto draw results, latest rounds, and ticket matches with the k-lotto npm package. Use when the user asks for winning numbers, payout details, or whether their numbers matched.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: utility
|
||||
|
|
@ -12,7 +12,7 @@ metadata:
|
|||
|
||||
## What this skill does
|
||||
|
||||
`korean-lotto` 패키지로 동행복권 로또 최신 회차, 특정 회차, 상세 당첨 결과, 번호 대조를 처리한다.
|
||||
`k-lotto` 패키지로 동행복권 로또 최신 회차, 특정 회차, 상세 당첨 결과, 번호 대조를 처리한다.
|
||||
|
||||
## When to use
|
||||
|
||||
|
|
@ -23,7 +23,9 @@ metadata:
|
|||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- `npm install korean-lotto`
|
||||
- 배포 후: `npm install -g k-lotto`
|
||||
- 실행 전: `export NODE_PATH="$(npm root -g)"`
|
||||
- 이 저장소에서 개발할 때: 루트에서 `npm install`
|
||||
|
||||
## Inputs
|
||||
|
||||
|
|
@ -32,11 +34,22 @@ metadata:
|
|||
|
||||
## Workflow
|
||||
|
||||
### 0. Install the package globally when missing
|
||||
|
||||
`node -e 'require("k-lotto")'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Node 패키지 설치를 먼저 시도한다.
|
||||
|
||||
```bash
|
||||
npm install -g k-lotto
|
||||
export NODE_PATH="$(npm root -g)"
|
||||
```
|
||||
|
||||
패키지가 없다는 이유로 HTML 파서를 다시 짜거나 다른 비공식 소스를 찾지 않는다.
|
||||
|
||||
### 1. Get the latest round when needed
|
||||
|
||||
```bash
|
||||
node - <<'JS'
|
||||
const lotto = require("korean-lotto");
|
||||
NODE_PATH="$(npm root -g)" node - <<'JS'
|
||||
const lotto = require("k-lotto");
|
||||
lotto.getLatestRound().then((round) => console.log(round));
|
||||
JS
|
||||
```
|
||||
|
|
@ -44,18 +57,18 @@ JS
|
|||
### 2. Fetch result or detailed payout data
|
||||
|
||||
```bash
|
||||
node - <<'JS'
|
||||
const lotto = require("korean-lotto");
|
||||
lotto.getDetailResult(861).then((result) => console.log(JSON.stringify(result, null, 2)));
|
||||
NODE_PATH="$(npm root -g)" node - <<'JS'
|
||||
const lotto = require("k-lotto");
|
||||
lotto.getDetailResult(1216).then((result) => console.log(JSON.stringify(result, null, 2)));
|
||||
JS
|
||||
```
|
||||
|
||||
### 3. Check user's numbers when provided
|
||||
|
||||
```bash
|
||||
node - <<'JS'
|
||||
const lotto = require("korean-lotto");
|
||||
lotto.checkNumber(862, ["10", "32", "38", "40", "42", "43"])
|
||||
NODE_PATH="$(npm root -g)" node - <<'JS'
|
||||
const lotto = require("k-lotto");
|
||||
lotto.checkNumber(1216, ["3", "10", "14", "15", "23", "24"])
|
||||
.then((result) => console.log(JSON.stringify(result, null, 2)));
|
||||
JS
|
||||
```
|
||||
|
|
@ -68,8 +81,8 @@ JS
|
|||
|
||||
## Failure modes
|
||||
|
||||
- 패키지가 오래되어 upstream HTML 변경에 취약할 수 있다
|
||||
- 최신 회차 반영이 늦을 수 있다
|
||||
- 최신 회차는 결과 페이지 HTML에서 읽기 때문에 upstream HTML 변경의 영향을 받을 수 있다
|
||||
- 상세 회차 정보는 동행복권 JSON 응답 스키마 변경의 영향을 받을 수 있다
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
|
|||
1314
package-lock.json
generated
Normal file
1314
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
25
package.json
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "k-skill",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"lint": "npm run lint --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
|
||||
"pack:dry-run": "npm pack --workspace k-lotto --dry-run",
|
||||
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
|
||||
"version-packages": "changeset version",
|
||||
"release:npm": "changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"@changesets/cli": "^2.29.5",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
44
packages/k-lotto/README.md
Normal file
44
packages/k-lotto/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# k-lotto
|
||||
|
||||
동행복권 공식 결과 페이지와 JSON 응답을 이용해 한국 로또 6/45 당첨 결과를 조회하는 Node.js 패키지입니다.
|
||||
|
||||
## 설치
|
||||
|
||||
배포 후:
|
||||
|
||||
```bash
|
||||
npm install k-lotto
|
||||
```
|
||||
|
||||
이 저장소에서 개발할 때:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## 사용 예시
|
||||
|
||||
```js
|
||||
const lotto = require("k-lotto");
|
||||
|
||||
async function main() {
|
||||
const latestRound = await lotto.getLatestRound();
|
||||
const detail = await lotto.getDetailResult(latestRound);
|
||||
const checked = await lotto.checkNumber(latestRound, [3, 10, 14, 15, 23, 24]);
|
||||
|
||||
console.log({ latestRound, detail, checked });
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
```
|
||||
|
||||
## 공개 API
|
||||
|
||||
- `getLatestRound()`
|
||||
- `getResult(round)`
|
||||
- `getDetailResult(round)`
|
||||
- `checkNumber(round, ticketNumbers)`
|
||||
- `evaluateTicket(detailResult, ticketNumbers)`
|
||||
27
packages/k-lotto/package.json
Normal file
27
packages/k-lotto/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "k-lotto",
|
||||
"version": "0.1.0",
|
||||
"description": "Official dhlottery-backed client for Korean Lotto results",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"README.md"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"keywords": [
|
||||
"k-skill",
|
||||
"korea",
|
||||
"lotto",
|
||||
"dhlottery"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "node --check src/index.js && node --check src/parse.js && node --check test/index.test.js",
|
||||
"test": "node --test"
|
||||
}
|
||||
}
|
||||
107
packages/k-lotto/src/index.js
Normal file
107
packages/k-lotto/src/index.js
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
const {
|
||||
evaluateTicket,
|
||||
extractLatestRoundFromHtml,
|
||||
normalizeRoundItem,
|
||||
selectRoundItem
|
||||
} = require("./parse");
|
||||
|
||||
const DEFAULT_HEADERS = {
|
||||
accept: "application/json, text/html;q=0.9",
|
||||
"user-agent": "k-skill/k-lotto"
|
||||
};
|
||||
|
||||
const LATEST_RESULT_URL = "https://www.dhlottery.co.kr/lt645/result";
|
||||
const ROUND_DETAIL_URL = "https://www.dhlottery.co.kr/lt645/selectPstLt645InfoNew.do";
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function fetchText(url) {
|
||||
const response = await fetch(url, { headers: DEFAULT_HEADERS });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`dhlottery request failed with ${response.status} for ${url}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url, { headers: DEFAULT_HEADERS });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`dhlottery request failed with ${response.status} for ${url}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function getLatestRound() {
|
||||
const html = await fetchText(LATEST_RESULT_URL);
|
||||
return extractLatestRoundFromHtml(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} round
|
||||
* @returns {Promise<ReturnType<typeof normalizeRoundItem>>}
|
||||
*/
|
||||
async function getDetailResult(round) {
|
||||
assertValidRound(round);
|
||||
|
||||
const url = new URL(ROUND_DETAIL_URL);
|
||||
url.searchParams.set("srchDir", "center");
|
||||
url.searchParams.set("srchLtEpsd", String(round));
|
||||
|
||||
const payload = await fetchJson(url.toString());
|
||||
const item = selectRoundItem(payload, round);
|
||||
|
||||
return normalizeRoundItem(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} round
|
||||
*/
|
||||
async function getResult(round) {
|
||||
const detail = await getDetailResult(round);
|
||||
|
||||
return {
|
||||
round: detail.round,
|
||||
drawDate: detail.drawDate,
|
||||
numbers: [...detail.numbers],
|
||||
bonus: detail.bonus
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} round
|
||||
* @param {ReadonlyArray<number>} ticketNumbers
|
||||
*/
|
||||
async function checkNumber(round, ticketNumbers) {
|
||||
const detail = await getDetailResult(round);
|
||||
return evaluateTicket(detail, ticketNumbers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} round
|
||||
*/
|
||||
function assertValidRound(round) {
|
||||
if (!Number.isInteger(round) || round < 1) {
|
||||
throw new Error("round must be a positive integer.");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkNumber,
|
||||
evaluateTicket,
|
||||
getDetailResult,
|
||||
getLatestRound,
|
||||
getResult
|
||||
};
|
||||
209
packages/k-lotto/src/parse.js
Normal file
209
packages/k-lotto/src/parse.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
const LATEST_ROUND_PATTERN = /id="opt_val"[^>]*value="(\d+)"/i;
|
||||
|
||||
/**
|
||||
* @param {string} html
|
||||
* @returns {number}
|
||||
*/
|
||||
function extractLatestRoundFromHtml(html) {
|
||||
const match = html.match(LATEST_ROUND_PATTERN);
|
||||
|
||||
if (!match) {
|
||||
throw new Error("Unable to locate the latest round on the dhlottery result page.");
|
||||
}
|
||||
|
||||
return Number.parseInt(match[1], 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatWon(value) {
|
||||
return `${value.toLocaleString("ko-KR")}원`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} raw
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatYmd(raw) {
|
||||
if (!/^\d{8}$/.test(raw)) {
|
||||
throw new Error(`Unexpected date format: ${raw}`);
|
||||
}
|
||||
|
||||
return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} payload
|
||||
* @param {number} round
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
function selectRoundItem(payload, round) {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
throw new Error("Expected a JSON object from dhlottery.");
|
||||
}
|
||||
|
||||
const data = /** @type {{ data?: { list?: Array<Record<string, any>> } }} */ (payload).data;
|
||||
const list = data?.list;
|
||||
|
||||
if (!Array.isArray(list) || list.length === 0) {
|
||||
throw new Error(`No lotto result items were returned for round ${round}.`);
|
||||
}
|
||||
|
||||
const item = list.find((entry) => Number(entry.ltEpsd) === round);
|
||||
|
||||
if (!item) {
|
||||
throw new Error(`Round ${round} was not present in the dhlottery response.`);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>} item
|
||||
* @returns {{
|
||||
* round: number,
|
||||
* drawDate: string,
|
||||
* numbers: number[],
|
||||
* bonus: number,
|
||||
* totalSalesAmount: number,
|
||||
* carryoverSalesAmount: number,
|
||||
* winnersByPurchaseType: null | { auto: number, manual: number, semiAuto: number },
|
||||
* payouts: Array<{ rank: number, winners: number, prizeAmount: number, totalPrizeAmount: number, prizeAmountLabel: string, totalPrizeAmountLabel: string }>,
|
||||
* raw: Record<string, any>
|
||||
* }}
|
||||
*/
|
||||
function normalizeRoundItem(item) {
|
||||
const numbers = [item.tm1WnNo, item.tm2WnNo, item.tm3WnNo, item.tm4WnNo, item.tm5WnNo, item.tm6WnNo]
|
||||
.map((value) => Number(value));
|
||||
|
||||
const bonus = Number(item.bnsWnNo);
|
||||
|
||||
return {
|
||||
round: Number(item.ltEpsd),
|
||||
drawDate: formatYmd(String(item.ltRflYmd)),
|
||||
numbers,
|
||||
bonus,
|
||||
totalSalesAmount: Number(item.wholEpsdSumNtslAmt),
|
||||
carryoverSalesAmount: Number(item.rlvtEpsdSumNtslAmt),
|
||||
winnersByPurchaseType: Number(item.winType0) === 0
|
||||
? {
|
||||
auto: Number(item.winType1),
|
||||
manual: Number(item.winType2),
|
||||
semiAuto: Number(item.winType3)
|
||||
}
|
||||
: null,
|
||||
payouts: [
|
||||
buildPayoutRow(1, item.rnk1WnNope, item.rnk1WnAmt, item.rnk1SumWnAmt),
|
||||
buildPayoutRow(2, item.rnk2WnNope, item.rnk2WnAmt, item.rnk2SumWnAmt),
|
||||
buildPayoutRow(3, item.rnk3WnNope, item.rnk3WnAmt, item.rnk3SumWnAmt),
|
||||
buildPayoutRow(4, item.rnk4WnNope, item.rnk4WnAmt, item.rnk4SumWnAmt),
|
||||
buildPayoutRow(5, item.rnk5WnNope, item.rnk5WnAmt, item.rnk5SumWnAmt)
|
||||
],
|
||||
raw: item
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} rank
|
||||
* @param {unknown} winners
|
||||
* @param {unknown} prizeAmount
|
||||
* @param {unknown} totalPrizeAmount
|
||||
*/
|
||||
function buildPayoutRow(rank, winners, prizeAmount, totalPrizeAmount) {
|
||||
const normalizedPrize = Number(prizeAmount);
|
||||
const normalizedTotal = Number(totalPrizeAmount);
|
||||
|
||||
return {
|
||||
rank,
|
||||
winners: Number(winners),
|
||||
prizeAmount: normalizedPrize,
|
||||
totalPrizeAmount: normalizedTotal,
|
||||
prizeAmountLabel: formatWon(normalizedPrize),
|
||||
totalPrizeAmountLabel: formatWon(normalizedTotal)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ReadonlyArray<number>} ticketNumbers
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function normalizeTicket(ticketNumbers) {
|
||||
if (!Array.isArray(ticketNumbers) || ticketNumbers.length !== 6) {
|
||||
throw new Error("ticketNumbers must contain exactly 6 values.");
|
||||
}
|
||||
|
||||
const normalized = ticketNumbers.map((value) => Number(value));
|
||||
|
||||
if (normalized.some((value) => !Number.isInteger(value) || value < 1 || value > 45)) {
|
||||
throw new Error("ticketNumbers must be integers between 1 and 45.");
|
||||
}
|
||||
|
||||
const uniqueCount = new Set(normalized).size;
|
||||
if (uniqueCount !== 6) {
|
||||
throw new Error("ticketNumbers must not contain duplicates.");
|
||||
}
|
||||
|
||||
return normalized.sort((left, right) => left - right);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ReturnType<typeof normalizeRoundItem>} detail
|
||||
* @param {ReadonlyArray<number>} ticketNumbers
|
||||
*/
|
||||
function evaluateTicket(detail, ticketNumbers) {
|
||||
const normalizedTicket = normalizeTicket(ticketNumbers);
|
||||
const matchedNumbers = normalizedTicket.filter((number) => detail.numbers.includes(number));
|
||||
const bonusMatched = normalizedTicket.includes(detail.bonus);
|
||||
const rank = determineRank(matchedNumbers.length, bonusMatched);
|
||||
|
||||
return {
|
||||
round: detail.round,
|
||||
drawDate: detail.drawDate,
|
||||
ticketNumbers: normalizedTicket,
|
||||
winningNumbers: [...detail.numbers],
|
||||
bonus: detail.bonus,
|
||||
matchedNumbers,
|
||||
bonusMatched,
|
||||
matchCount: matchedNumbers.length,
|
||||
rank,
|
||||
outcome: rank === null ? "낙첨" : `${rank}등`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} matchCount
|
||||
* @param {boolean} bonusMatched
|
||||
* @returns {number | null}
|
||||
*/
|
||||
function determineRank(matchCount, bonusMatched) {
|
||||
if (matchCount === 6) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (matchCount === 5 && bonusMatched) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (matchCount === 5) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (matchCount === 4) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
if (matchCount === 3) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
evaluateTicket,
|
||||
extractLatestRoundFromHtml,
|
||||
normalizeRoundItem,
|
||||
selectRoundItem
|
||||
};
|
||||
6
packages/k-lotto/test/fixtures/latest-result.html
vendored
Normal file
6
packages/k-lotto/test/fixtures/latest-result.html
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<body>
|
||||
<input type="hidden" class="opt_val" id="opt_val" value="1216">
|
||||
</body>
|
||||
</html>
|
||||
43
packages/k-lotto/test/fixtures/round-1216.json
vendored
Normal file
43
packages/k-lotto/test/fixtures/round-1216.json
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"resultCode": null,
|
||||
"resultMessage": null,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"winType0": 0,
|
||||
"winType1": 8,
|
||||
"winType2": 5,
|
||||
"winType3": 1,
|
||||
"gmSqNo": 5133,
|
||||
"ltEpsd": 1216,
|
||||
"tm1WnNo": 3,
|
||||
"tm2WnNo": 10,
|
||||
"tm3WnNo": 14,
|
||||
"tm4WnNo": 15,
|
||||
"tm5WnNo": 23,
|
||||
"tm6WnNo": 24,
|
||||
"bnsWnNo": 25,
|
||||
"ltRflYmd": "20260321",
|
||||
"rnk1WnNope": 14,
|
||||
"rnk1WnAmt": 2148654000,
|
||||
"rnk1SumWnAmt": 30081156000,
|
||||
"rnk2WnNope": 87,
|
||||
"rnk2WnAmt": 57626736,
|
||||
"rnk2SumWnAmt": 5013526032,
|
||||
"rnk3WnNope": 3181,
|
||||
"rnk3WnAmt": 1576085,
|
||||
"rnk3SumWnAmt": 5013526385,
|
||||
"rnk4WnNope": 168089,
|
||||
"rnk4WnAmt": 50000,
|
||||
"rnk4SumWnAmt": 8404450000,
|
||||
"rnk5WnNope": 2853121,
|
||||
"rnk5WnAmt": 5000,
|
||||
"rnk5SumWnAmt": 14265605000,
|
||||
"sumWnNope": 3024492,
|
||||
"rlvtEpsdSumNtslAmt": 62778263417,
|
||||
"wholEpsdSumNtslAmt": 125556526000,
|
||||
"excelRnk": "1등"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
95
packages/k-lotto/test/index.test.js
Normal file
95
packages/k-lotto/test/index.test.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const {
|
||||
evaluateTicket,
|
||||
getDetailResult,
|
||||
getLatestRound,
|
||||
getResult
|
||||
} = require("../src/index");
|
||||
const {
|
||||
extractLatestRoundFromHtml,
|
||||
normalizeRoundItem,
|
||||
selectRoundItem
|
||||
} = require("../src/parse");
|
||||
|
||||
const fixturesDir = path.join(__dirname, "fixtures");
|
||||
const latestResultHtml = fs.readFileSync(path.join(fixturesDir, "latest-result.html"), "utf8");
|
||||
const round1216Payload = JSON.parse(
|
||||
fs.readFileSync(path.join(fixturesDir, "round-1216.json"), "utf8")
|
||||
);
|
||||
|
||||
test("extractLatestRoundFromHtml parses the latest round from the official result page", () => {
|
||||
assert.equal(extractLatestRoundFromHtml(latestResultHtml), 1216);
|
||||
});
|
||||
|
||||
test("normalizeRoundItem maps official JSON into the public detail shape", () => {
|
||||
const detail = normalizeRoundItem(selectRoundItem(round1216Payload, 1216));
|
||||
|
||||
assert.equal(detail.round, 1216);
|
||||
assert.equal(detail.drawDate, "2026-03-21");
|
||||
assert.deepEqual(detail.numbers, [3, 10, 14, 15, 23, 24]);
|
||||
assert.equal(detail.bonus, 25);
|
||||
assert.equal(detail.payouts[0].rank, 1);
|
||||
assert.equal(detail.payouts[0].winners, 14);
|
||||
assert.equal(detail.winnersByPurchaseType?.auto, 8);
|
||||
});
|
||||
|
||||
test("evaluateTicket returns the right rank for an exact match", () => {
|
||||
const detail = normalizeRoundItem(selectRoundItem(round1216Payload, 1216));
|
||||
const checked = evaluateTicket(detail, [3, 10, 14, 15, 23, 24]);
|
||||
|
||||
assert.equal(checked.rank, 1);
|
||||
assert.equal(checked.outcome, "1등");
|
||||
assert.deepEqual(checked.matchedNumbers, [3, 10, 14, 15, 23, 24]);
|
||||
});
|
||||
|
||||
test("evaluateTicket rejects duplicate numbers", () => {
|
||||
const detail = normalizeRoundItem(selectRoundItem(round1216Payload, 1216));
|
||||
|
||||
assert.throws(() => {
|
||||
evaluateTicket(detail, [3, 3, 14, 15, 23, 24]);
|
||||
}, /duplicates/);
|
||||
});
|
||||
|
||||
test("public fetchers can consume injected fixtures via mocked fetch", async () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
global.fetch = async (url) => {
|
||||
if (String(url).includes("/lt645/result")) {
|
||||
return makeResponse(true, latestResultHtml);
|
||||
}
|
||||
|
||||
return makeResponse(true, round1216Payload);
|
||||
};
|
||||
|
||||
try {
|
||||
assert.equal(await getLatestRound(), 1216);
|
||||
assert.deepEqual(await getResult(1216), {
|
||||
round: 1216,
|
||||
drawDate: "2026-03-21",
|
||||
numbers: [3, 10, 14, 15, 23, 24],
|
||||
bonus: 25
|
||||
});
|
||||
|
||||
const detail = await getDetailResult(1216);
|
||||
assert.equal(detail.payouts[1].rank, 2);
|
||||
} finally {
|
||||
global.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {boolean} ok
|
||||
* @param {string|object} body
|
||||
*/
|
||||
function makeResponse(ok, body) {
|
||||
return new Response(typeof body === "string" ? body : JSON.stringify(body), {
|
||||
status: ok ? 200 : 500,
|
||||
headers: {
|
||||
"content-type": typeof body === "string" ? "text/html" : "application/json"
|
||||
}
|
||||
});
|
||||
}
|
||||
17
python-packages/README.md
Normal file
17
python-packages/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Python package release scaffold
|
||||
|
||||
이 저장소의 Python 패키지는 `python-packages/*` 아래에 둘 계획이다.
|
||||
|
||||
현재는 실제 패키지가 없어서 release-please workflow만 껍데기 상태로 남겨 둔다.
|
||||
|
||||
첫 Python 패키지를 추가할 때 해야 할 일:
|
||||
|
||||
1. `python-packages/<package-name>/pyproject.toml` 생성
|
||||
2. `.github/release-please/python-config.json`에 해당 path와 `release-type: "python"` 추가
|
||||
3. `.github/release-please/python-manifest.json`에 시작 버전 추가
|
||||
4. `release-python.yml`에 build + `pypa/gh-action-pypi-publish` publish job 연결
|
||||
|
||||
주의:
|
||||
|
||||
- PyPI trusted publishing은 현재 reusable workflow 안에서 쓰지 않는 것이 안전하다.
|
||||
- 실제 `pypi-publish` job은 지금처럼 top-level workflow에 두는 기준으로 유지한다.
|
||||
|
|
@ -28,6 +28,14 @@ if [[ ! -f "$secrets_file" ]]; then
|
|||
fi
|
||||
|
||||
if [[ "$missing" -ne 0 ]]; then
|
||||
cat <<EOF
|
||||
next steps:
|
||||
1. follow k-skill-setup / docs/setup.md
|
||||
2. register required secrets in ~/.config/k-skill/secrets.env.plain
|
||||
3. encrypt it to ~/.config/k-skill/secrets.env with sops
|
||||
4. delete the plaintext file
|
||||
5. run this check again
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -37,8 +37,13 @@ while IFS= read -r -d '' skill_dir; do
|
|||
done < <(
|
||||
find "$root" -mindepth 1 -maxdepth 1 -type d \
|
||||
! -name .git \
|
||||
! -name .github \
|
||||
! -name .omx \
|
||||
! -name .changeset \
|
||||
! -name docs \
|
||||
! -name node_modules \
|
||||
! -name packages \
|
||||
! -name python-packages \
|
||||
! -name scripts \
|
||||
! -name examples \
|
||||
-print0
|
||||
|
|
|
|||
|
|
@ -39,10 +39,21 @@ metadata:
|
|||
|
||||
## Workflow
|
||||
|
||||
### 1. Load the API key securely
|
||||
### 1. Stop for secure registration when the API key is missing
|
||||
|
||||
평문 key를 붙여 넣지 않는다.
|
||||
|
||||
`SEOUL_OPEN_API_KEY`, `~/.config/k-skill/secrets.env`, `~/.config/k-skill/age/keys.txt` 중 하나라도 없으면 다음 식으로 안내하고 멈춘다.
|
||||
|
||||
```text
|
||||
이 작업에는 SEOUL_OPEN_API_KEY 가 필요합니다.
|
||||
값을 채팅창에 붙여 넣지 말고 ~/.config/k-skill/secrets.env.plain 에 직접 채운 뒤
|
||||
sops 로 ~/.config/k-skill/secrets.env 로 암호화해 주세요.
|
||||
암호화가 끝나면 plaintext 파일은 지우고 bash scripts/check-setup.sh 로 다시 확인해 주세요.
|
||||
```
|
||||
|
||||
시크릿이 없다는 이유로 비공식 미러 API나 다른 출처로 자동 우회하지 않는다.
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'test -n "$SEOUL_OPEN_API_KEY"'
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ metadata:
|
|||
## Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- `python -m pip install SRTrain`
|
||||
- `python3 -m pip install SRTrain`
|
||||
- `sops` and `age` installed
|
||||
- common setup reviewed in `../k-skill-setup/SKILL.md`
|
||||
- secret policy reviewed in `../docs/security-and-secrets.md`
|
||||
|
|
@ -53,17 +53,36 @@ metadata:
|
|||
|
||||
## Workflow
|
||||
|
||||
### 1. Validate secrets path
|
||||
### 0. Install the package globally when missing
|
||||
|
||||
`python3 -c 'import SRT'` 가 실패하면 다른 구현으로 우회하지 말고 전역 Python 패키지 설치를 먼저 시도한다.
|
||||
|
||||
```bash
|
||||
python3 -m pip install SRTrain
|
||||
```
|
||||
|
||||
### 1. Validate secrets path and stop for secure registration when missing
|
||||
|
||||
비밀번호를 직접 받지 않는다. 필요한 경우 encrypted secrets file 경로와 변수 이름만 확인한다.
|
||||
|
||||
`KSKILL_SRT_ID`, `KSKILL_SRT_PASSWORD`, `~/.config/k-skill/secrets.env`, `~/.config/k-skill/age/keys.txt` 중 하나라도 없으면 다음 식으로 안내하고 멈춘다.
|
||||
|
||||
```text
|
||||
이 작업에는 KSKILL_SRT_ID, KSKILL_SRT_PASSWORD 가 필요합니다.
|
||||
값을 채팅창에 붙여 넣지 말고 ~/.config/k-skill/secrets.env.plain 에 직접 채운 뒤
|
||||
sops 로 ~/.config/k-skill/secrets.env 로 암호화해 주세요.
|
||||
암호화가 끝나면 plaintext 파일은 지우고 bash scripts/check-setup.sh 로 다시 확인해 주세요.
|
||||
```
|
||||
|
||||
시크릿이 없다는 이유로 웹사이트를 직접 긁거나 다른 비공식 경로를 찾지 않는다.
|
||||
|
||||
### 2. Search first
|
||||
|
||||
먼저 조회해서 후보를 요약한다.
|
||||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from SRT import SRT
|
||||
|
||||
|
|
@ -90,7 +109,7 @@ PY
|
|||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from SRT import Adult, SRT, SeatType
|
||||
|
||||
|
|
@ -112,7 +131,7 @@ PY
|
|||
|
||||
```bash
|
||||
SOPS_AGE_KEY_FILE="$HOME/.config/k-skill/age/keys.txt" \
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python - <<'"'"'PY'"'"'
|
||||
sops exec-env "$HOME/.config/k-skill/secrets.env" 'python3 - <<'"'"'PY'"'"'
|
||||
import os
|
||||
from SRT import SRT
|
||||
|
||||
|
|
|
|||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node10",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"packages/k-lotto/src/**/*.js",
|
||||
"packages/k-lotto/test/**/*.js"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue